diff --git a/extensions/webmail/forms.py b/extensions/webmail/forms.py
index 1de6598e1..32ffe97ea 100644
--- a/extensions/webmail/forms.py
+++ b/extensions/webmail/forms.py
@@ -9,3 +9,6 @@ class ComposeMailForm(forms.Form):
class FolderForm(forms.Form):
name = forms.CharField()
+
+class AttachmentForm(forms.Form):
+ attachment = forms.FileField(label="")
diff --git a/extensions/webmail/lib.py b/extensions/webmail/lib.py
index f1c5c50b5..8600762ac 100644
--- a/extensions/webmail/lib.py
+++ b/extensions/webmail/lib.py
@@ -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
diff --git a/extensions/webmail/main.py b/extensions/webmail/main.py
index 77a86ec60..be689f7f4 100644
--- a/extensions/webmail/main.py
+++ b/extensions/webmail/main.py
@@ -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,
diff --git a/extensions/webmail/static/css/attachments.css b/extensions/webmail/static/css/attachments.css
new file mode 100644
index 000000000..ce9eedd5a
--- /dev/null
+++ b/extensions/webmail/static/css/attachments.css
@@ -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;
+}
\ No newline at end of file
diff --git a/extensions/webmail/static/css/webmail.css b/extensions/webmail/static/css/webmail.css
index deb211ebc..b27847859 100644
--- a/extensions/webmail/static/css/webmail.css
+++ b/extensions/webmail/static/css/webmail.css
@@ -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;
}
diff --git a/extensions/webmail/static/js/fdmenu.js b/extensions/webmail/static/js/fdmenu.js
index e966b75ed..721d69ecd 100644
--- a/extensions/webmail/static/js/fdmenu.js
+++ b/extensions/webmail/static/js/fdmenu.js
@@ -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");
});
diff --git a/extensions/webmail/static/js/webmail.js b/extensions/webmail/static/js/webmail.js
index 9f61e42c8..8f2c11e03 100644
--- a/extensions/webmail/static/js/webmail.js
+++ b/extensions/webmail/static/js/webmail.js
@@ -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);
}
/*
diff --git a/extensions/webmail/templates/webmail/attachments.html b/extensions/webmail/templates/webmail/attachments.html
new file mode 100644
index 000000000..d1660e605
--- /dev/null
+++ b/extensions/webmail/templates/webmail/attachments.html
@@ -0,0 +1,115 @@
+{% extends "layout_simple.html" %}
+{% load i18n %}
+{% block html_head %}
+
+
+
+{% endblock %}
+{% block content %}
+
{% trans "Attachments" %}
+{{ error }}
+
+
+

{% trans "Uploading.." %}
+
+
+
+{% for att in attachments %}
+
+
+
+
+
+
+{% endfor %}
+
+
+
+
+{% endblock %}
diff --git a/extensions/webmail/templates/webmail/compose.html b/extensions/webmail/templates/webmail/compose.html
index b2988713d..424e41b97 100644
--- a/extensions/webmail/templates/webmail/compose.html
+++ b/extensions/webmail/templates/webmail/compose.html
@@ -3,6 +3,20 @@
{% if body %}
diff --git a/extensions/webmail/templates/webmail/folders.html b/extensions/webmail/templates/webmail/folders.html
index 78a838579..cf0b5610b 100644
--- a/extensions/webmail/templates/webmail/folders.html
+++ b/extensions/webmail/templates/webmail/folders.html
@@ -3,7 +3,7 @@
{% if titlebar %}
+ rel="{handler: 'iframe', size: {x: 350, y: 400}, closeBtn: true}">
diff --git a/extensions/webmail/templates/webmail/upload_done.html b/extensions/webmail/templates/webmail/upload_done.html
new file mode 100644
index 000000000..761c5d9ca
--- /dev/null
+++ b/extensions/webmail/templates/webmail/upload_done.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
diff --git a/extensions/webmail/urls.py b/extensions/webmail/urls.py
index 201341160..e950fd222 100644
--- a/extensions/webmail/urls.py
+++ b/extensions/webmail/urls.py
@@ -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
.+)/(?P[\w\-\+]+)/part/$', 'getattachment'),
(r'^(?P.+)/(?P[\w\-\+]+)/delete/$', 'delete'),
(r'^(?P.+)/(?P[\w\-\+]+)/content/$', 'getmailcontent'),
diff --git a/extensions/webmail/views.py b/extensions/webmail/views.py
index e916375a3..1a76a3a9e 100644
--- a/extensions/webmail/views.py
+++ b/extensions/webmail/views.py
@@ -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("""
""").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)
diff --git a/lib/parameters.py b/lib/parameters.py
index 8134f6e85..a29986423 100644
--- a/lib/parameters.py
+++ b/lib/parameters.py
@@ -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
diff --git a/lib/webutils.py b/lib/webutils.py
index b91f8c9c2..c41b8d593 100644
--- a/lib/webutils.py
+++ b/lib/webutils.py
@@ -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
diff --git a/static/js/SqueezeBox.js b/static/js/SqueezeBox.js
index b386bca6c..77cb7ac2c 100644
--- a/static/js/SqueezeBox.js
+++ b/static/js/SqueezeBox.js
@@ -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);
diff --git a/static/js/anchor_nav.js b/static/js/anchor_nav.js
index c98624711..055896f50 100644
--- a/static/js/anchor_nav.js
+++ b/static/js/anchor_nav.js
@@ -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);
diff --git a/static/js/common.js b/static/js/common.js
index b5c8214b3..7a5d462e9 100644
--- a/static/js/common.js
+++ b/static/js/common.js
@@ -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){
diff --git a/static/pics/attachment.png b/static/pics/attachment.png
new file mode 100644
index 000000000..529bb7f55
Binary files /dev/null and b/static/pics/attachment.png differ
diff --git a/templates/common/genericform.html b/templates/common/genericform.html
index 0d349f9ad..3ebfbb087 100644
--- a/templates/common/genericform.html
+++ b/templates/common/genericform.html
@@ -1,12 +1,12 @@
{% load i18n %}
{% for field in form %}
-
+
{% if field.help_text %}
{% trans "Help" %}
{{ field.help_text}}
{% endif %}
- {{ field.label_tag }} {{ field }} {% ifequal noerrors False %}{{ field.errors }}{% endifequal %}
+ {{ field.label_tag }}
{{ field }}
{% ifequal noerrors False %}{{ field.errors }}{% endifequal %}
{% endfor %}
diff --git a/userprefs/templatetags/uprefs_extras.py b/userprefs/templatetags/uprefs_extras.py
index 3c34858a3..cc79d34c0 100644
--- a/userprefs/templatetags/uprefs_extras.py
+++ b/userprefs/templatetags/uprefs_extras.py
@@ -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",