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:
Antoine Nguyen
2011-09-08 09:13:13 +02:00
parent b33e6c0b25
commit 9ddb27573f
21 changed files with 710 additions and 163 deletions
+3
View File
@@ -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
View File
@@ -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
+2
View File
@@ -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;
}
+75 -9
View File
@@ -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;
}
+2 -1
View File
@@ -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");
});
+45 -22
View File
@@ -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>
+9
View File
@@ -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
View File
@@ -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
View File
@@ -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
+26
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+22
View File
@@ -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

+2 -2
View File
@@ -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 %}
+2 -2
View File
@@ -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",