mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
At last, composing messages with attachments is possible!!
It is a really simple implementation but I think it will be enough for a begining. Users can add multiple attachments with a message, each attachment must not be larger than a customizable limit. see #38
This commit is contained in:
@@ -9,3 +9,6 @@ class ComposeMailForm(forms.Form):
|
||||
|
||||
class FolderForm(forms.Form):
|
||||
name = forms.CharField()
|
||||
|
||||
class AttachmentForm(forms.Form):
|
||||
attachment = forms.FileField(label="")
|
||||
|
||||
+224
-1
@@ -12,10 +12,12 @@ import ssl
|
||||
from datetime import datetime, timedelta
|
||||
import email
|
||||
import lxml
|
||||
from django.core.files.uploadhandler import FileUploadHandler, StopUpload, SkipFile
|
||||
from django.http import HttpResponse
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.conf import settings
|
||||
from modoboa.lib import u2u_decode, tables, imap_utf7, parameters
|
||||
from modoboa.lib.webutils import static_url
|
||||
from modoboa.lib.webutils import static_url, size2integer
|
||||
from modoboa.lib.connections import ConnectionsManager
|
||||
from modoboa.lib.email_listing import MBconnector, EmailListing
|
||||
from modoboa.lib.emailutils import *
|
||||
@@ -611,4 +613,225 @@ def find_images_in_body(body):
|
||||
parts.append(p)
|
||||
|
||||
return lxml.html.tostring(html), parts
|
||||
|
||||
def set_compose_session(request):
|
||||
"""Initialize a new "compose" session.
|
||||
|
||||
It is used to keep track of attachments defined with a new
|
||||
message. Each new message will be associated with a unique ID (in
|
||||
order to avoid conflicts between users).
|
||||
|
||||
:param request: a Request object.
|
||||
:return: the new unique ID.
|
||||
"""
|
||||
import uuid
|
||||
randid = str(uuid.uuid4()).replace("-", "")
|
||||
request.session["compose_mail"] = {"id" : randid, "attachments" : []}
|
||||
return randid
|
||||
|
||||
def save_attachment(f):
|
||||
"""Save a new attachment to the filesystem.
|
||||
|
||||
The attachment is not saved using its own name to the
|
||||
filesystem. To avoid conflicts, a random name is generated and
|
||||
used instead.
|
||||
|
||||
:param f: an uploaded file object (see Django's documentation)
|
||||
:return: the new random name
|
||||
"""
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
dstdir = os.path.join(settings.MEDIA_ROOT, "tmp")
|
||||
try:
|
||||
fp = NamedTemporaryFile(dir=dstdir, delete=False)
|
||||
except Exception, e:
|
||||
raise WebmailError(str(e))
|
||||
for chunk in f.chunks():
|
||||
fp.write(chunk)
|
||||
fp.close()
|
||||
return fp.name
|
||||
|
||||
def clean_attachments(attlist):
|
||||
"""Remove all attachments from the filesystem
|
||||
|
||||
:param attlist: a list of 2-uple. Each element must contain the following information :
|
||||
(random name, real name).
|
||||
"""
|
||||
for att in attlist:
|
||||
fullpath = os.path.join(settings.MEDIA_ROOT, "tmp", att["tmpname"])
|
||||
try:
|
||||
os.remove(fullpath)
|
||||
except OSError, e:
|
||||
pass
|
||||
|
||||
def html2plaintext(content):
|
||||
"""HTML to plain text translation
|
||||
|
||||
:param content: some HTML content
|
||||
"""
|
||||
html = lxml.html.fromstring(content)
|
||||
plaintext = ""
|
||||
for ch in html.iter():
|
||||
p = None
|
||||
if ch.text is not None:
|
||||
p = ch.text.strip('\r\t\n')
|
||||
if ch.tag == "img":
|
||||
p = ch.get("alt")
|
||||
if p is None:
|
||||
continue
|
||||
plaintext += p + "\n"
|
||||
|
||||
return plaintext
|
||||
|
||||
def get_current_url(request):
|
||||
res = "%s?page=%s" % (request.session["folder"], request.session["page"])
|
||||
for p in ["criteria", "pattern", "order"]:
|
||||
if p in request.session.keys():
|
||||
res += "&%s=%s" % (p, request.session[p])
|
||||
return res
|
||||
|
||||
def create_mail_attachment(attdef):
|
||||
"""Create the MIME part corresponding to the given attachment.
|
||||
|
||||
Mandatory keys: 'fname', 'tmpname', 'content-type'
|
||||
|
||||
:param attdef: a dictionary containing the attachment definition
|
||||
:return: a MIMEBase object
|
||||
"""
|
||||
from email import Encoders
|
||||
from email.mime.base import MIMEBase
|
||||
|
||||
maintype, subtype = attdef["content-type"].split("/")
|
||||
res = MIMEBase(maintype, subtype)
|
||||
fp = open(os.path.join(settings.MEDIA_ROOT, "tmp", attdef["tmpname"]), "rb")
|
||||
res.set_payload(fp.read())
|
||||
fp.close()
|
||||
Encoders.encode_base64(res)
|
||||
res.add_header("Content-Disposition", "attachment; filename='%s'" % attdef["fname"])
|
||||
return res
|
||||
|
||||
def send_mail(request, origmsg=None, posturl=None):
|
||||
"""Email verification and sending.
|
||||
|
||||
If the form does not present any error, a new MIME message is
|
||||
constructed. Then, a connection is established with the defined
|
||||
SMTP server and the message is finally sent.
|
||||
|
||||
:param request: a Request object
|
||||
:param origmsg: the eventual original message (in case of a reply)
|
||||
:param posturl: the url to post the message form to
|
||||
:return: a 2-uple (True|False, HttpResponse)
|
||||
"""
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from forms import ComposeMailForm
|
||||
from modoboa.lib.webutils import getctx, ajax_simple_response
|
||||
from modoboa.admin.models import Mailbox
|
||||
from modoboa.auth.lib import get_password
|
||||
|
||||
form = ComposeMailForm(request.POST)
|
||||
editormode = parameters.get_user(request.user, "EDITOR")
|
||||
if form.is_valid():
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import make_msgid, formatdate
|
||||
import smtplib
|
||||
|
||||
body = request.POST["id_body"]
|
||||
charset = "utf-8"
|
||||
if editormode == "html":
|
||||
msg = MIMEMultipart(_subtype="related")
|
||||
submsg = MIMEMultipart(_subtype="alternative")
|
||||
textbody = html2plaintext(body)
|
||||
submsg.attach(MIMEText(textbody.encode(charset),
|
||||
_subtype="plain", _charset=charset))
|
||||
body, images = find_images_in_body(body)
|
||||
submsg.attach(MIMEText(body.encode(charset), _subtype=editormode,
|
||||
_charset=charset))
|
||||
msg.attach(submsg)
|
||||
for img in images:
|
||||
msg.attach(img)
|
||||
else:
|
||||
text = MIMEText(body.encode(charset), _subtype=editormode)
|
||||
if len(request.session["compose_mail"]["attachments"]):
|
||||
msg = MIMEMultipart()
|
||||
msg.attach(text)
|
||||
else:
|
||||
msg = text
|
||||
|
||||
for attdef in request.session["compose_mail"]["attachments"]:
|
||||
msg.attach(create_mail_attachment(attdef))
|
||||
|
||||
msg["Subject"] = form.cleaned_data["subject"]
|
||||
address, domain = split_mailbox(form.cleaned_data["from_"])
|
||||
try:
|
||||
mb = Mailbox.objects.get(address=address, domain__name=domain)
|
||||
msg["From"] = "%s <%s>" % (mb.name, form.cleaned_data["from_"])
|
||||
except Mailbox.DoesNotExist:
|
||||
msg["From"] = form.cleaned_data["from_"]
|
||||
msg["To"] = form.cleaned_data["to"]
|
||||
msg["Message-ID"] = make_msgid()
|
||||
msg["User-Agent"] = "Modoboa"
|
||||
msg["Date"] = formatdate(time.time(), True)
|
||||
if origmsg and origmsg.has_key("Message-ID"):
|
||||
msg["References"] = msg["In-Reply-To"] = origmsg["Message-ID"]
|
||||
rcpts = msg['To'].split(',')
|
||||
if form.cleaned_data["cc"] != "":
|
||||
msg["Cc"] = form.cleaned_data["cc"]
|
||||
rcpts += msg["Cc"].split(",")
|
||||
|
||||
try:
|
||||
secmode = parameters.get_admin("SMTP_SECURED_MODE")
|
||||
if secmode == "ssl":
|
||||
s = smtplib.SMTP_SSL(parameters.get_admin("SMTP_SERVER"),
|
||||
int(parameters.get_admin("SMTP_PORT")))
|
||||
else:
|
||||
s = smtplib.SMTP(parameters.get_admin("SMTP_SERVER"),
|
||||
int(parameters.get_admin("SMTP_PORT")))
|
||||
if secmode == "starttls":
|
||||
s.starttls()
|
||||
except Exception, text:
|
||||
raise WebmailError(str(text))
|
||||
|
||||
if parameters.get_admin("SMTP_AUTHENTICATION") == "yes":
|
||||
try:
|
||||
s.login(request.user.username, get_password(request))
|
||||
except smtplib.SMTPAuthenticationError, e:
|
||||
raise WebmailError(str(e))
|
||||
s.sendmail(msg['From'], rcpts, msg.as_string())
|
||||
s.quit()
|
||||
sentfolder = parameters.get_user(request.user, "SENT_FOLDER")
|
||||
IMAPconnector(user=request.user.username,
|
||||
password=request.session["password"]).push_mail(sentfolder, msg)
|
||||
clean_attachments(request.session["compose_mail"]["attachments"])
|
||||
del request.session["compose_mail"]
|
||||
return True, ajax_simple_response(getctx("ok", url=get_current_url(request)))
|
||||
|
||||
|
||||
listing = render_to_string("webmail/compose.html",
|
||||
{"form" : form,
|
||||
"body" : request.POST["id_body"].strip(),
|
||||
"posturl" : posturl})
|
||||
return False, ajax_simple_response(
|
||||
getctx("ko", level=2, listing=listing, editor=editormode)
|
||||
)
|
||||
|
||||
class AttachmentUploadHandler(FileUploadHandler):
|
||||
"""
|
||||
Simple upload handler to limit the size of the attachments users
|
||||
can upload.
|
||||
"""
|
||||
|
||||
def __init__(self, request=None):
|
||||
super(AttachmentUploadHandler, self).__init__(request)
|
||||
self.total_upload = 0
|
||||
self.toobig = False
|
||||
self.maxsize = size2integer(parameters.get_admin("MAX_ATTACHMENT_SIZE"))
|
||||
|
||||
def receive_data_chunk(self, raw_data, start):
|
||||
self.total_upload += len(raw_data)
|
||||
if self.total_upload >= self.maxsize:
|
||||
self.toobig = True
|
||||
raise SkipFile()
|
||||
return raw_data
|
||||
|
||||
def file_complete(self, file_size):
|
||||
return None
|
||||
|
||||
@@ -31,6 +31,8 @@ def init():
|
||||
parameters.register_admin("SMTP_AUTHENTICATION", type="list_yesno",
|
||||
deflt="no",
|
||||
help=_("Server needs authentication"))
|
||||
parameters.register_admin("MAX_ATTACHMENT_SIZE", type="int", deflt=2048,
|
||||
help=_("Maximum attachment size in KB"))
|
||||
|
||||
|
||||
parameters.register_user("MESSAGES_PER_PAGE", type="int", deflt=40,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
#attachment_list {
|
||||
border: 1px solid #ccc;
|
||||
background-color: #eee;
|
||||
position: absolute;
|
||||
top: 140px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
bottom: 35px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
#upload_status {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
font-size: 0.9em;
|
||||
width: 120px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#done_container {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
img {
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
}
|
||||
@@ -12,6 +12,29 @@ html > body #listing {
|
||||
bottom: 35px;
|
||||
}
|
||||
|
||||
.clearfix:after {
|
||||
content: ".";
|
||||
display: block;
|
||||
clear: both;
|
||||
visibility: hidden;
|
||||
overflow: hidden;
|
||||
line-height: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.clearfix {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
html[xmlns] .clearfix {
|
||||
display: block;
|
||||
}
|
||||
|
||||
* html .clearfix {
|
||||
height: 1%;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/*thead tr {
|
||||
position: relative;
|
||||
top: expression(offsetParent.scrollTop);
|
||||
@@ -66,27 +89,63 @@ ul.visible {
|
||||
|
||||
/* Compose mail page */
|
||||
#compose-container {
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
/*margin: 0 auto;*/
|
||||
/*width: 100%;*/
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
right: 10px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#attachments {
|
||||
position: relative;
|
||||
border: 1px solid #cccccc;
|
||||
background-color: #eee;
|
||||
margin-bottom: 5px;
|
||||
padding: 5px 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#attachments img {
|
||||
float: left;
|
||||
}
|
||||
|
||||
#list {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 130px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.shortlist {
|
||||
font-weight: normal !important;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
div.row {
|
||||
margin: 3px;
|
||||
clear: both;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
div.row label {
|
||||
float: left;
|
||||
width: 120px;
|
||||
font-size: small;
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
div.input_container {
|
||||
position: absolute;
|
||||
left: 130px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
textarea {
|
||||
border: 1px solid #cccccc;
|
||||
background-color: #eee;
|
||||
width: 80%;
|
||||
height: 30px;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
@@ -96,15 +155,22 @@ textarea:hover {
|
||||
|
||||
#id_body {
|
||||
background: white;
|
||||
width: 98%;
|
||||
/*width: 98%;*/
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
#mailheader input {
|
||||
width: 80%;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
height: 20px;
|
||||
clear: both;
|
||||
|
||||
}
|
||||
|
||||
#mailheader {
|
||||
position: relative;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,8 @@ var FdMenu = new Class({
|
||||
evt.stop();
|
||||
SqueezeBox.open(evt.target.get("href"), {
|
||||
handler: "iframe",
|
||||
size: {x: 350, y: 400}
|
||||
size: {x: 350, y: 400},
|
||||
closeBtn: true
|
||||
});
|
||||
$(document.body).fireEvent("click");
|
||||
});
|
||||
|
||||
@@ -86,6 +86,38 @@ function resize_window_callback(event) {
|
||||
setDivHeight("id_body", $("mailheader").getSize().y, 0);
|
||||
}
|
||||
|
||||
/*
|
||||
* Callback of the 'sendmail' action.
|
||||
*/
|
||||
function sendmail_callback(evt) {
|
||||
evt.stop();
|
||||
infobox.show(gettext("Sending..."), {
|
||||
profile : "gray",
|
||||
spinner : true
|
||||
});
|
||||
disable_link(this);
|
||||
$("composemail").set("send", {
|
||||
onSuccess: function(resp) {
|
||||
resp = JSON.decode(resp);
|
||||
if (resp.status == "ko") {
|
||||
current_anchor.get_callback("compose")(resp);
|
||||
if ($defined(resp.respmsg)) {
|
||||
infobox.error(resp.respmsg);
|
||||
}
|
||||
enable_link(this, sendmail_callback);
|
||||
return;
|
||||
}
|
||||
current_anchor.parse_string(resp.url, true).setparams(navparams);
|
||||
current_anchor.update();
|
||||
this.eliminate("editormode");
|
||||
}.bind(this)
|
||||
});
|
||||
if (this.retrieve("editormode") == "html") {
|
||||
CKEDITOR.instances[editorid].updateElement();
|
||||
}
|
||||
$("composemail").send();
|
||||
}
|
||||
|
||||
/*
|
||||
* Callback of the 'compose' action
|
||||
*/
|
||||
@@ -108,33 +140,24 @@ function compose_callback(resp) {
|
||||
height: $(editorid).getSize().y
|
||||
});
|
||||
}
|
||||
if (resp.id) {
|
||||
current_anchor.setparam("id", resp.id).update(0, 1);
|
||||
}
|
||||
$("attachments").addEvent("click", function(evt) {
|
||||
SqueezeBox.open(this.get("name"), {
|
||||
handler: "iframe",
|
||||
size: {x: 350, y: 400},
|
||||
closeBtn: false
|
||||
});
|
||||
});
|
||||
|
||||
$$("a[name=back]")
|
||||
.removeEvents("click")
|
||||
.addEvent("click", loadFolder);
|
||||
$$("a[name=sendmail]")
|
||||
.store("editormode", editormode)
|
||||
.removeEvents("click")
|
||||
.addEvent("click", function(evt) {
|
||||
evt.stop();
|
||||
$("composemail").set("send", {
|
||||
onSuccess: function(resp) {
|
||||
resp = JSON.decode(resp);
|
||||
if (resp.status == "ko") {
|
||||
current_anchor.get_callback("compose")(resp);
|
||||
|
||||
if ($defined(resp.error)) {
|
||||
infobox.error(resp.error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
current_anchor.parse_string(resp.url, true).setparams(navparams);
|
||||
current_anchor.update();
|
||||
}
|
||||
});
|
||||
if (editormode == "html") {
|
||||
CKEDITOR.instances[editorid].updateElement();
|
||||
}
|
||||
$("composemail").send();
|
||||
});
|
||||
.addEvent("click", sendmail_callback);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
{% extends "layout_simple.html" %}
|
||||
{% load i18n %}
|
||||
{% block html_head %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}webmail/css/attachments.css" />
|
||||
<script src="{{ MEDIA_URL }}webmail/js/webmail.js" type="text/javascript"></script>
|
||||
<script type="text/javascript">
|
||||
function del_attachment(evt) {
|
||||
evt.stop();
|
||||
new Request.JSON({
|
||||
url: this.get("href"),
|
||||
onSuccess: function(resp) {
|
||||
this.getParent().dispose();
|
||||
}.bind(this)
|
||||
}).get();
|
||||
}
|
||||
|
||||
function upload_cb(fname, tmpname) {
|
||||
$("upload_status").setStyle("display", "none");
|
||||
$("submit").set("disabled", false);
|
||||
$("error").set("html", "");
|
||||
|
||||
var ndiv = new Element("div");
|
||||
var delbtn = new Element("a")
|
||||
.set("name", "delattachment")
|
||||
.set("href", "{% url extensions.webmail.views.delattachment %}?name=" + tmpname)
|
||||
.set("html", "<img src='{{MEDIA_URL}}pics/remove.png' />")
|
||||
.inject(ndiv);
|
||||
var label = new Element("label").set("html", fname).inject(ndiv);
|
||||
|
||||
delbtn.addEvent("click", del_attachment);
|
||||
$("id_attachment").set("value", "");
|
||||
ndiv.inject($("attachment_list"));
|
||||
}
|
||||
|
||||
function upload_error(msg) {
|
||||
$("upload_status").setStyle("display", "none");
|
||||
$("error").set("html", msg);
|
||||
$("submit").set("disabled", false);
|
||||
}
|
||||
|
||||
window.addEvent("domready", function() {
|
||||
$("uploadfile").addEvent("submit", function(evt) {
|
||||
if ($("id_attachment").value == "") {
|
||||
evt.stop();
|
||||
return;
|
||||
}
|
||||
$("upload_status").setStyle("display", "block");
|
||||
$("submit").set("disabled", true);
|
||||
});
|
||||
$$("a[name=delattachment]").addEvent("click", del_attachment);
|
||||
$("done").addEvent("click", function(evt) {
|
||||
var nfiles = 0;
|
||||
var shortlist = "";
|
||||
var html;
|
||||
|
||||
$$("a[name=delattachment]").each(function(item) {
|
||||
var label = item.getNext();
|
||||
|
||||
nfiles++;
|
||||
if (nfiles <= 2) {
|
||||
if (shortlist != "") {
|
||||
shortlist += ", ";
|
||||
} else {
|
||||
shortlist = "(";
|
||||
}
|
||||
shortlist += label.get("html");
|
||||
return;
|
||||
}
|
||||
if (nfiles == 3) {
|
||||
shortlist += ", ...";
|
||||
}
|
||||
});
|
||||
if (shortlist != "") {
|
||||
shortlist += ")";
|
||||
}
|
||||
html = interpolate(ngettext("%s file", "%s files", nfiles), [nfiles]);
|
||||
html += " <span class='shortlist'>" + shortlist + "</span>";
|
||||
parent.document.getElementById("list")
|
||||
.set("html", html);
|
||||
parent.SqueezeBox.close();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="title"><h1>{% trans "Attachments" %}</h1></div>
|
||||
<div id="error" class="error">{{ error }}</div>
|
||||
<form enctype="multipart/form-data" method="post" name="uploadfile" id="uploadfile"
|
||||
target="upload_target"
|
||||
action="{% url extensions.webmail.views.attachments %}">
|
||||
{% include "common/genericform.html" %}
|
||||
<div class="row">
|
||||
<input type="submit" id="submit" name="submit" value="{% trans 'Attach' %}" />
|
||||
</div>
|
||||
</form>
|
||||
<div id="upload_status" style="display:none">
|
||||
<img src="{{MEDIA_URL}}css/spinner.gif" /> {% trans "Uploading.." %}
|
||||
</div>
|
||||
<iframe id="upload_target" name="upload_target" src="#"
|
||||
style="width:0;height:0;border:0px solid #fff;"></iframe>
|
||||
<div id="attachment_list">
|
||||
{% for att in attachments %}
|
||||
<div>
|
||||
<a href="{% url extensions.webmail.views.delattachment %}?name={{ att.tmpname }}"
|
||||
name="delattachment">
|
||||
<img src="{{ MEDIA_URL }}pics/remove.png" />
|
||||
</a>
|
||||
<label>{{ att.fname }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div id="done_container">
|
||||
<input type="button" id="done" name="done" value="{% trans 'Done' %}" />
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -3,6 +3,20 @@
|
||||
<div id="compose-container">
|
||||
<div id="mailheader">
|
||||
{% include "common/genericform.html" %}
|
||||
<div id="attachments" name="{% url extensions.webmail.views.attachments %}">
|
||||
<div>
|
||||
<img src="{{ MEDIA_URL }}pics/attachment.png" />
|
||||
{% trans "Attachments" %}
|
||||
</div>
|
||||
<div id="list">
|
||||
{% blocktrans count attachments|length as counter %}
|
||||
{{ counter }} file
|
||||
{% plural %}
|
||||
{{ counter }} files
|
||||
{% endblocktrans %}
|
||||
<span class="shortlist">{{ short_att_list }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
{% if body %}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% if titlebar %}
|
||||
<div class="boxtitle">
|
||||
<a href="{% url extensions.webmail.views.newfolder %}" class="boxed"
|
||||
rel="{handler: 'iframe', size: {x: 350, y: 400}}">
|
||||
rel="{handler: 'iframe', size: {x: 350, y: 400}, closeBtn: true}">
|
||||
<img src="{{MEDIA_URL}}pics/add.png"
|
||||
alt="{% trans 'Add a new folder' %}"/>
|
||||
</a>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<!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" />
|
||||
<script type="text/javascript">
|
||||
{% if status == "ok" %}
|
||||
parent.upload_cb("{{ fname }}", "{{ tmpname }}");
|
||||
{% else %}
|
||||
parent.upload_error("{{ error }}");
|
||||
{% endif %}
|
||||
</script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
@@ -1,5 +1,10 @@
|
||||
from django.conf.urls.defaults import *
|
||||
|
||||
#
|
||||
# FIXME: there is a big issue with this url mapping. If a folder's
|
||||
# name matches one of the first entries (from "compose" to
|
||||
# "attachments"), the associated folder will not be accessible.
|
||||
#
|
||||
urlpatterns = patterns('modoboa.extensions.webmail.views',
|
||||
(r'^$', "index"),
|
||||
(r'^compose/$', "compose"),
|
||||
@@ -10,6 +15,10 @@ urlpatterns = patterns('modoboa.extensions.webmail.views',
|
||||
(r'^newfolder/$', "newfolder"),
|
||||
(r'^editfolder/$', 'editfolder'),
|
||||
(r'^delfolder/$', 'delfolder'),
|
||||
|
||||
(r'^attachments/$', 'attachments'),
|
||||
(r'^delattachment/$', 'delattachment'),
|
||||
|
||||
(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'),
|
||||
|
||||
+92
-118
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import time
|
||||
import sys
|
||||
from django.http import HttpResponse, Http404
|
||||
from django.http import HttpResponse, Http404, HttpResponseRedirect
|
||||
from django.template import Template, Context
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils import simplejson
|
||||
@@ -21,13 +21,6 @@ from lib import *
|
||||
from forms import *
|
||||
from templatetags.webextras import *
|
||||
|
||||
def __get_current_url(request):
|
||||
res = "%s?page=%s" % (request.session["folder"], request.session["page"])
|
||||
for p in ["criteria", "pattern", "order"]:
|
||||
if p in request.session.keys():
|
||||
res += "&%s=%s" % (p, request.session[p])
|
||||
return res
|
||||
|
||||
def __render_common_components(request, folder_name, lst=None, content=None, menu=None):
|
||||
"""Render all components that are common to all pages in the webmail
|
||||
|
||||
@@ -153,7 +146,7 @@ def viewmail(request, folder, mail_id=None):
|
||||
content = Template("""
|
||||
<iframe width="100%" frameBorder="0" src="{{ url }}" id="mailcontent"></iframe>
|
||||
""").render(Context({"url" : url}))
|
||||
menu = viewm_menu("", __get_current_url(request), folder, mail_id,
|
||||
menu = viewm_menu("", get_current_url(request), folder, mail_id,
|
||||
request.user.get_all_permissions())
|
||||
mbc = IMAPconnector(user=request.user.username,
|
||||
password=request.session["password"])
|
||||
@@ -230,7 +223,7 @@ def delete(request, fdname, mail_id):
|
||||
mbc = IMAPconnector(user=request.user.username,
|
||||
password=request.session["password"])
|
||||
mbc.move(mail_id, fdname, parameters.get_user(request.user, "TRASH_FOLDER"))
|
||||
ctx = getctx("ok", next=__get_current_url(request))
|
||||
ctx = getctx("ok", next=get_current_url(request))
|
||||
return HttpResponse(simplejson.dumps(ctx), mimetype="application/json")
|
||||
|
||||
@login_required
|
||||
@@ -264,7 +257,7 @@ def compact(request, name):
|
||||
return folder(request, name, False)
|
||||
|
||||
def render_compose(request, form, posturl, email=None, insert_signature=False):
|
||||
menu = compose_menu("", __get_current_url(request),
|
||||
menu = compose_menu("", get_current_url(request),
|
||||
request.user.get_all_permissions())
|
||||
editor = parameters.get_user(request.user, "EDITOR")
|
||||
if email is None:
|
||||
@@ -276,121 +269,43 @@ def render_compose(request, form, posturl, email=None, insert_signature=False):
|
||||
if insert_signature:
|
||||
signature = EmailSignature(request.user)
|
||||
body += str(signature)
|
||||
randid = None
|
||||
if not request.GET.has_key("id"):
|
||||
if request.session.has_key("compose_mail"):
|
||||
clean_attachments(request.session["compose_mail"]["attachments"])
|
||||
randid = set_compose_session(request)
|
||||
elif not request.session.has_key("compose_mail") \
|
||||
or request.session["compose_mail"]["id"] != request.GET["id"]:
|
||||
randid = set_compose_session(request)
|
||||
|
||||
attachments = request.session["compose_mail"]["attachments"]
|
||||
if len(attachments):
|
||||
short_att_list = "(%s)" \
|
||||
% ", ".join(map(lambda att: att["fname"],
|
||||
attachments[:2] + [{"fname" : "..."}] \
|
||||
if len(attachments) > 2 else attachments))
|
||||
else:
|
||||
short_att_list = ""
|
||||
content = _render_to_string(request, "webmail/compose.html", {
|
||||
"form" : form, "bodyheader" : textheader,
|
||||
"body" : body, "posturl" : posturl
|
||||
"body" : body, "posturl" : posturl,
|
||||
"attachments" : attachments, "short_att_list" : short_att_list
|
||||
})
|
||||
mbc = IMAPconnector(user=request.user.username,
|
||||
password=request.session["password"])
|
||||
ctx = getctx("ok", level=2, editor=editor,
|
||||
**__render_common_components(request, request.session["folder"],
|
||||
menu=menu, content=content))
|
||||
if randid is not None:
|
||||
ctx["id"] = randid
|
||||
return HttpResponse(simplejson.dumps(ctx), mimetype="application/json")
|
||||
|
||||
def __html2plaintext(content):
|
||||
"""HTML to plain text translation
|
||||
|
||||
:param content: some HTML content
|
||||
"""
|
||||
html = lxml.html.fromstring(content)
|
||||
plaintext = ""
|
||||
for ch in html.iter():
|
||||
p = None
|
||||
if ch.text is not None:
|
||||
p = ch.text.strip('\r\t\n')
|
||||
if ch.tag == "img":
|
||||
p = ch.get("alt")
|
||||
if p is None:
|
||||
continue
|
||||
plaintext += p + "\n"
|
||||
|
||||
return plaintext
|
||||
|
||||
def send_mail(request, withctx=False, origmsg=None, posturl=None):
|
||||
form = ComposeMailForm(request.POST)
|
||||
error = None
|
||||
ctx = None
|
||||
editormode = parameters.get_user(request.user, "EDITOR")
|
||||
if form.is_valid():
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import make_msgid, formatdate
|
||||
import smtplib
|
||||
|
||||
body = request.POST["id_body"]
|
||||
charset = "utf-8"
|
||||
if editormode == "html":
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
|
||||
msg = MIMEMultipart(_subtype="related")
|
||||
submsg = MIMEMultipart(_subtype="alternative")
|
||||
textbody = __html2plaintext(body)
|
||||
submsg.attach(MIMEText(textbody.encode(charset),
|
||||
_subtype="plain", _charset=charset))
|
||||
body, images = find_images_in_body(body)
|
||||
submsg.attach(MIMEText(body.encode(charset), _subtype=editormode,
|
||||
_charset=charset))
|
||||
msg.attach(submsg)
|
||||
for img in images:
|
||||
msg.attach(img)
|
||||
else:
|
||||
msg = MIMEText(body.encode(charset), _subtype=editormode)
|
||||
|
||||
msg["Subject"] = request.POST["subject"]
|
||||
address, domain = split_mailbox(request.POST["from_"])
|
||||
try:
|
||||
mb = Mailbox.objects.get(address=address, domain__name=domain)
|
||||
msg["From"] = "%s <%s>" % (mb.name, request.POST["from_"])
|
||||
except Mailbox.DoesNotExist:
|
||||
msg["From"] = request.POST["from_"]
|
||||
msg["To"] = request.POST["to"]
|
||||
msg["Message-ID"] = make_msgid()
|
||||
msg["User-Agent"] = "Modoboa"
|
||||
msg["Date"] = formatdate(time.time(), True)
|
||||
if origmsg and origmsg.has_key("Message-ID"):
|
||||
msg["References"] = msg["In-Reply-To"] = origmsg["Message-ID"]
|
||||
rcpts = msg['To'].split(',')
|
||||
if "cc" in request.POST.keys():
|
||||
msg["Cc"] = request.POST["cc"]
|
||||
rcpts += msg["Cc"].split(",")
|
||||
error = None
|
||||
try:
|
||||
secmode = parameters.get_admin("SMTP_SECURED_MODE")
|
||||
if secmode == "ssl":
|
||||
s = smtplib.SMTP_SSL(parameters.get_admin("SMTP_SERVER"),
|
||||
int(parameters.get_admin("SMTP_PORT")))
|
||||
else:
|
||||
s = smtplib.SMTP(parameters.get_admin("SMTP_SERVER"),
|
||||
int(parameters.get_admin("SMTP_PORT")))
|
||||
if secmode == "starttls":
|
||||
s.starttls()
|
||||
except Exception, text:
|
||||
error = str(text)
|
||||
if error is None:
|
||||
if parameters.get_admin("SMTP_AUTHENTICATION") == "yes":
|
||||
s.login(request.user.username, get_password(request))
|
||||
s.sendmail(msg['From'], rcpts, msg.as_string())
|
||||
s.quit()
|
||||
sentfolder = parameters.get_user(request.user, "SENT_FOLDER")
|
||||
IMAPconnector(user=request.user.username,
|
||||
password=request.session["password"]).push_mail(sentfolder, msg)
|
||||
ctx = getctx("ok", url=__get_current_url(request))
|
||||
|
||||
if ctx is None:
|
||||
listing = render_to_string("webmail/compose.html",
|
||||
{"form" : form,
|
||||
"body" : request.POST["id_body"].strip(),
|
||||
"posturl" : posturl})
|
||||
ctx = getctx("ko", level=2, error=error, listing=listing, editor=editormode)
|
||||
if not withctx:
|
||||
return HttpResponse(simplejson.dumps(ctx), mimetype="application/json")
|
||||
return ctx, HttpResponse(simplejson.dumps(ctx), mimetype="application/json")
|
||||
|
||||
@login_required
|
||||
@is_not_localadmin()
|
||||
def compose(request):
|
||||
if request.method == "POST":
|
||||
return send_mail(request, posturl=reverse(compose))
|
||||
status, resp = send_mail(request, posturl=reverse(compose))
|
||||
return resp
|
||||
|
||||
form = ComposeMailForm()
|
||||
form.fields["from_"].initial = request.user.username
|
||||
@@ -401,13 +316,13 @@ def compose(request):
|
||||
def reply(request, folder, mail_id):
|
||||
msg = fetchmail(request, folder, mail_id, True)
|
||||
if request.method == "POST":
|
||||
ctx, r = send_mail(request, True, origmsg=msg,
|
||||
posturl=reverse(reply, args=[folder, mail_id]))
|
||||
if ctx["status"] == "ok":
|
||||
status, resp = send_mail(request, origmsg=msg,
|
||||
posturl=reverse(reply, args=[folder, mail_id]))
|
||||
if status:
|
||||
IMAPconnector(user=request.user.username,
|
||||
password=request.session["password"]).msg_answered(folder,
|
||||
mail_id)
|
||||
return r
|
||||
return resp
|
||||
|
||||
form = ComposeMailForm()
|
||||
email = ReplyModifier(msg, request.user, form, request.GET.has_key("all"),
|
||||
@@ -419,9 +334,9 @@ def reply(request, folder, mail_id):
|
||||
@is_not_localadmin()
|
||||
def forward(request, folder, mail_id):
|
||||
if request.method == "POST":
|
||||
ctx, response = send_mail(request, True,
|
||||
posturl=reverse(forward, args=[folder, mail_id]))
|
||||
if ctx["status"] == "ok":
|
||||
status, response = send_mail(request,
|
||||
posturl=reverse(forward, args=[folder, mail_id]))
|
||||
if status:
|
||||
IMAPconnector(user=request.user.username,
|
||||
password=request.session["password"]).msgforwarded(folder,
|
||||
mail_id)
|
||||
@@ -523,3 +438,62 @@ def delfolder(request):
|
||||
password=request.session["password"])
|
||||
mbc.delete_folder(request.GET["name"])
|
||||
return ajax_response(request)
|
||||
|
||||
@login_required
|
||||
@is_not_localadmin()
|
||||
def attachments(request, tplname="webmail/attachments.html"):
|
||||
if request.method == "POST":
|
||||
csuploader = AttachmentUploadHandler()
|
||||
request.upload_handlers.insert(0, csuploader)
|
||||
error = None
|
||||
form = AttachmentForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
try:
|
||||
fobj = request.FILES["attachment"]
|
||||
tmpname = save_attachment(fobj)
|
||||
request.session["compose_mail"]["attachments"] \
|
||||
+= [{"fname" : str(fobj),
|
||||
"content-type" : fobj.content_type,
|
||||
"size" : fobj.size,
|
||||
"tmpname" : os.path.basename(tmpname)}]
|
||||
request.session.modified = True
|
||||
return _render(request, "webmail/upload_done.html", {
|
||||
"status" : "ok", "fname" : request.FILES["attachment"],
|
||||
"tmpname" : os.path.basename(tmpname)
|
||||
});
|
||||
except WebmailError, inst:
|
||||
error = _("Failed to save attachment: ") + str(inst)
|
||||
|
||||
if csuploader.toobig:
|
||||
error = _("Attachment is too big (limit: %s)" \
|
||||
% parameters.get_admin("MAX_ATTACHMENT_SIZE"))
|
||||
return _render(request, "webmail/upload_done.html", {
|
||||
"status" : "ko", "error" : error
|
||||
});
|
||||
ctx = {"form" : AttachmentForm(),
|
||||
"attachments" : request.session["compose_mail"]["attachments"]}
|
||||
return _render(request, tplname, ctx)
|
||||
|
||||
@login_required
|
||||
@is_not_localadmin()
|
||||
def delattachment(request):
|
||||
if not request.session.has_key("compose_mail") \
|
||||
or not request.GET.has_key("name") \
|
||||
or not request.GET["name"]:
|
||||
return ajax_response(request, "ko", respmsg=_("Bad query"))
|
||||
|
||||
error = None
|
||||
for att in request.session["compose_mail"]["attachments"]:
|
||||
if att["tmpname"] == request.GET["name"]:
|
||||
request.session["compose_mail"]["attachments"].remove(att)
|
||||
fullpath = os.path.join(settings.MEDIA_ROOT, "tmp", att["tmpname"])
|
||||
try:
|
||||
os.remove(fullpath)
|
||||
except OSError, e:
|
||||
error = _("Failed to remove attachment: ") + str(e)
|
||||
break
|
||||
request.session.modified = True
|
||||
return ajax_response(request)
|
||||
if error is None:
|
||||
error = _("Unknown attachment")
|
||||
return ajax_response(request, "ko", respmsg=error)
|
||||
|
||||
+2
-2
@@ -87,7 +87,7 @@ def save_admin(name, value, app=None):
|
||||
p = Parameter()
|
||||
p.name = fullname
|
||||
if p.value != value:
|
||||
p.value = str(value).encode("string_escape")
|
||||
p.value = str(value).encode("string_escape").strip()
|
||||
p.save()
|
||||
return True
|
||||
|
||||
@@ -105,7 +105,7 @@ def save_user(user, name, value, app=None):
|
||||
p.user = user
|
||||
p.name = fullname
|
||||
if p.value != value:
|
||||
p.value = str(value).encode("string_escape")
|
||||
p.value = str(value).encode("string_escape").strip()
|
||||
p.save()
|
||||
return True
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
This module contains extra functions/shortcuts used to render HTML.
|
||||
"""
|
||||
import sys
|
||||
import re
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import render_to_response
|
||||
from django.template.loader import render_to_string
|
||||
@@ -115,3 +116,28 @@ def static_url(path):
|
||||
if path.startswith("/"):
|
||||
path = path[1:]
|
||||
return "%s%s" % (settings.MEDIA_URL, path)
|
||||
|
||||
def size2integer(value):
|
||||
"""Try to convert a string representing a size to an integer value
|
||||
in bytes.
|
||||
|
||||
Supported formats:
|
||||
* K|k for KB
|
||||
* M|m for MB
|
||||
* G|g for GB
|
||||
|
||||
:param value: the string to convert
|
||||
:return: the corresponding integer value
|
||||
"""
|
||||
m = re.match("(\d+)\s*(\w+)", value)
|
||||
if m is None:
|
||||
if re.match("\d+", value):
|
||||
return int(value)
|
||||
return 0
|
||||
if m.group(2)[0] in ["K", "k"]:
|
||||
return int(m.group(1)) * 2 ** 10
|
||||
if m.group(2)[0] in ["M", "m"]:
|
||||
return int(m.group(1)) * 2 ** 20
|
||||
if m.group(2)[0] in ["G", "g"]:
|
||||
return int(m.group(1)) * 2 ** 30
|
||||
return 0
|
||||
|
||||
+11
-2
@@ -88,7 +88,9 @@ var SqueezeBox = {
|
||||
}
|
||||
}
|
||||
this.content = new Element('div', {id: 'sbox-content'}).inject(this.win);
|
||||
this.closeBtn = new Element('a', {id: 'sbox-btn-close', href: '#'}).inject(this.win);
|
||||
if (this.options.closeBtn) {
|
||||
this.closeBtn = new Element('a', {id: 'sbox-btn-close', href: '#'}).inject(this.win);
|
||||
}
|
||||
this.fx = {
|
||||
overlay: new Fx.Tween(this.overlay, Object.merge({
|
||||
property: 'opacity',
|
||||
@@ -155,6 +157,11 @@ var SqueezeBox = {
|
||||
this.overlay.set('class', this.options.classOverlay);
|
||||
this.win.set('class', this.options.classWindow);
|
||||
if (Browser.ie) this.win.addClass('sbox-window-ie6');
|
||||
if (!this.options.closeBtn) {
|
||||
this.closeBtn.dispose();
|
||||
} else {
|
||||
this.closeBtn.inject(this.win);
|
||||
}
|
||||
},
|
||||
|
||||
close: function(e) {
|
||||
@@ -239,7 +246,9 @@ var SqueezeBox = {
|
||||
|
||||
toggleListeners: function(state) {
|
||||
var fn = (state) ? 'addEvent' : 'removeEvent';
|
||||
this.closeBtn[fn]('click', this.bound.close);
|
||||
if (this.options.closeBtn) {
|
||||
this.closeBtn[fn]('click', this.bound.close);
|
||||
}
|
||||
this.overlay[fn]('click', this.bound.close);
|
||||
this.doc[fn]('keydown', this.bound.key)[fn]('mousewheel', this.bound.scroll);
|
||||
this.doc.getWindow()[fn]('resize', this.bound.window)[fn]('scroll', this.bound.window);
|
||||
|
||||
+16
-3
@@ -20,6 +20,7 @@ var AnchorNavigation = new Class({
|
||||
this.params = $H();
|
||||
this.base = '';
|
||||
this.serialized = null;
|
||||
this.updatenext = true;
|
||||
this.reset_loading_infos();
|
||||
if (this.options.defcallback) {
|
||||
this.register_callback("default", this.options.defcallback);
|
||||
@@ -83,11 +84,19 @@ var AnchorNavigation = new Class({
|
||||
return res;
|
||||
},
|
||||
|
||||
update: function(force) {
|
||||
update: function(force, noupdate) {
|
||||
window.fireEvent("pageRefresh");
|
||||
location.hash = this.serialize();
|
||||
if ($defined(force)) {
|
||||
this.force = force;
|
||||
|
||||
if (!$defined(noupdate)) {
|
||||
noupdate = false;
|
||||
}
|
||||
if (!noupdate) {
|
||||
if ($defined(force)) {
|
||||
this.force = force;
|
||||
}
|
||||
} else {
|
||||
this.updatenext = false;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -183,6 +192,10 @@ var AnchorNavigation = new Class({
|
||||
location.hash = this.deflocation;
|
||||
return;
|
||||
}
|
||||
if (!this.updatenext) {
|
||||
this.updatenext = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var query = this.serialized.substring(1);
|
||||
|
||||
|
||||
@@ -26,6 +26,28 @@ function static_url(value) {
|
||||
return media_url + value;
|
||||
}
|
||||
|
||||
/*
|
||||
* Easy way to deactivate a link (ie. clicking on it does nothing)
|
||||
*/
|
||||
function disable_link(link) {
|
||||
link.removeEvents("click")
|
||||
.addEvent("click", function(evt) {
|
||||
evt.stop();
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Easy way to re-activate a link that was previously disabled. An
|
||||
* extra argument can be passed to specify a new "click" event
|
||||
* callback.
|
||||
*/
|
||||
function enable_link(link, callback) {
|
||||
link.removeEvents("click");
|
||||
if ($defined(callback)) {
|
||||
link.addEvent("click", callback);
|
||||
}
|
||||
}
|
||||
|
||||
function parse_menubar(id) {
|
||||
if ($(id)) {
|
||||
$(id).getElements('li.dropdown').each(function(elem){
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 649 B |
@@ -1,12 +1,12 @@
|
||||
{% load i18n %}
|
||||
{% for field in form %}
|
||||
<div class="row">
|
||||
<div class="row clearfix">
|
||||
{% if field.help_text %}
|
||||
<div id="{{ field.name }}_helptext" class="tip">
|
||||
<div class="tip-title">{% trans "Help" %}</div>
|
||||
<div class="tip-text">{{ field.help_text}}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ field.label_tag }} {{ field }} {% ifequal noerrors False %}{{ field.errors }}{% endifequal %}
|
||||
{{ field.label_tag }}<div class="input_container clearfix"> {{ field }}</div> {% ifequal noerrors False %}{{ field.errors }}{% endifequal %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
@@ -22,7 +22,7 @@ def options_menu(user):
|
||||
"img" : static_url("pics/edit.png"),
|
||||
"label" : _("Change password"),
|
||||
"class" : "boxed",
|
||||
"rel" : "{handler:'iframe',size:{x:360,y:200}}"},
|
||||
"rel" : "{handler:'iframe',size:{x:360,y:200},closeBtn:true}"},
|
||||
{"name" : "preferences",
|
||||
"img" : static_url("pics/user.png"),
|
||||
"label" : _("Preferences"),
|
||||
@@ -38,7 +38,7 @@ def options_menu(user):
|
||||
"img" : static_url("pics/alias.png"),
|
||||
"label" : _("Forward"),
|
||||
"class" : "boxed",
|
||||
"rel" : "{handler:'iframe',size:{x:360,y:320}}"
|
||||
"rel" : "{handler:'iframe',size:{x:360,y:320},closeBtn:true}"
|
||||
}]
|
||||
|
||||
entries[0]["menu"] += events.raiseQueryEvent("UserMenuDisplay",
|
||||
|
||||
Reference in New Issue
Block a user