From 312ebe57ec5030577547380a0dbbddddcb70cfc6 Mon Sep 17 00:00:00 2001 From: tonio Date: Fri, 5 Mar 2010 17:56:55 +0100 Subject: [PATCH] Compose, Reply, Reply all. New paramaters to configure connections to IMAP and SMTP servers. --- extensions/amavis_quarantine/sql_listing.py | 2 +- extensions/webmail/forms.py | 2 +- extensions/webmail/imap_listing.py | 63 +++++++++----- extensions/webmail/main.py | 84 ++++++++++++++----- extensions/webmail/templates/compose.html | 7 +- extensions/webmail/templates/index.html | 16 ++-- extensions/webmail/templates/viewmail.html | 3 + extensions/webmail/templatetags/webextras.py | 4 +- lib/email_listing.py | 10 ++- static/pics/settings.png | Bin 611 -> 629 bytes templates/nlayout.html | 2 +- 11 files changed, 138 insertions(+), 55 deletions(-) diff --git a/extensions/amavis_quarantine/sql_listing.py b/extensions/amavis_quarantine/sql_listing.py index 5e29fc3e7..fe24843a8 100644 --- a/extensions/amavis_quarantine/sql_listing.py +++ b/extensions/amavis_quarantine/sql_listing.py @@ -57,7 +57,7 @@ AND quarantine.chunk_ind=1 ORDER BY msgs.time_num DESC """ % self.filter - def messages_count(self, folder=None): + def messages_count(self, **kwargs): return self.count def fetch(self, start=None, stop=None, **kwargs): diff --git a/extensions/webmail/forms.py b/extensions/webmail/forms.py index a57423774..32c5ede87 100644 --- a/extensions/webmail/forms.py +++ b/extensions/webmail/forms.py @@ -4,6 +4,6 @@ from django.utils.translation import ugettext as _ class ComposeMailForm(forms.Form): from_ = forms.CharField(label=_("From")) to = forms.CharField(label=_("To"), widget=forms.Textarea()) - cc = forms.CharField(label=_("Cc"), widget=forms.Textarea()) + cc = forms.CharField(label=_("Cc"), widget=forms.Textarea(), required=False) subject = forms.CharField(label=_("Subject"), max_length=255) diff --git a/extensions/webmail/imap_listing.py b/extensions/webmail/imap_listing.py index 7d5bd9371..2a3fc2dbb 100644 --- a/extensions/webmail/imap_listing.py +++ b/extensions/webmail/imap_listing.py @@ -31,21 +31,36 @@ class WMtable(tables.Table): except AttributeError: return value -class IMAPheader(object): - addr_exp = re.compile("([^<\(]+)<|\(([^>\)]+)>|\)") - name_exp = re.compile("\"(.+)\"") +class EmailAddress(object): + addr_exp = re.compile("([^<\(]+)[<(]([^>)]+)[>)]") + name_exp = re.compile('"([^"]+)"') + def __init__(self, address): + self.fulladdress = address + self.name = self.address = None + m = EmailAddress.addr_exp.match(self.fulladdress) + if m is None: + return + name = m.group(1).strip() + self.address = m.group(2).strip() + m2 = EmailAddress.name_exp.match(name) + if m2: + name = m2.group(1) + self.name = "" + for part in decode_header(name): + if self.name != "": + self.name += " " + if part[1] is None: + self.name += part[0] + else: + self.name += unicode(part[0], part[1]) + self.fulladdress = "%s <%s>" % (self.name, self.address) + +class IMAPheader(object): @staticmethod def parse_address(value): - m = IMAPheader.addr_exp.match(value) - if m: - name = m.group(1) - name = name.strip() - m2 = IMAPheader.name_exp.match(name) - if m2: - return IMAPheader.parse_subject(m2.group(1)) - return IMAPheader.parse_subject(name) - return value + addr = EmailAddress(value) + return addr.name and addr.name or value @staticmethod def parse_address_list(values): @@ -126,16 +141,27 @@ class IMAPconnector(object): return "INBOX" return folder.encode("imap4-utf-7") - def messages_count(self, folder=None, order=None): - if order: - sign = order[:1] - criterion = order[1:].upper() + def messages_count(self, **kwargs): + """An enhanced version of messages_count + + With IMAP, to know how many messages a mailbox contains, we + have to make a request to the server. To avoid requests + multiplications, we sort messages in the same time. This will + be usefull for other methods. + + :param order: sorting order + :param folder: mailbox to scan + """ + if "order" in kwargs.keys() and kwargs["order"]: + sign = kwargs["order"][:1] + criterion = kwargs["order"][1:].upper() if sign == '-': criterion = "REVERSE %s" % criterion else: criterion = "REVERSE DATE" print criterion + folder = kwargs.has_key("folder") and kwargs["folder"] or None (status, data) = self.m.select(self._encodefolder(folder)) (status, data) = self.m.sort("(%s)" % criterion, "UTF-8", "(NOT DELETED)") self.messages = data[0].split() @@ -210,9 +236,8 @@ class ImapListing(EmailListing): tpl = "webmail/index.html" tbltype = WMtable - def __init__(self, user, password, baseurl=None, navparams={}, **kwargs): + def __init__(self, user, password, baseurl=None, **kwargs): self.mbc = IMAPconnector(user=user, password=password) - self.navparams = navparams EmailListing.__init__(self, **kwargs) if baseurl: self.paginator.baseurl = baseurl @@ -232,7 +257,7 @@ class ImapEmail(Email): def __init__(self, msg, **kwargs): Email.__init__(self, msg, **kwargs) - fields = ["Subject", "From", "To", "Cc", "Date"] + fields = ["Subject", "From", "To", "Reply-To", "Cc", "Date"] for f in fields: label = f if not f in msg.keys(): diff --git a/extensions/webmail/main.py b/extensions/webmail/main.py index b87224cd8..d25263f7a 100644 --- a/extensions/webmail/main.py +++ b/extensions/webmail/main.py @@ -22,13 +22,18 @@ def init(): parameters.register("webmail", "IMAP_SERVER", "string", "127.0.0.1", help=_("Address of your IMAP server")) parameters.register("webmail", "IMAP_SECURED", "list_yesno", "no", - help=_("Use a secure connexion to access IMAP server")) + help=_("Use a secured connection to access IMAP server")) parameters.register("webmail", "IMAP_PORT", "int", "143", help=_("Listening port of your IMAP server")) parameters.register("webmail", "SMTP_SERVER", "string", "127.0.0.1", help=_("Address of your SMTP server")) parameters.register("webmail", "SMTP_PORT", "int", "25", help=_("Listening port of your SMTP server")) + parameters.register("webmail", "SMTP_AUTHENTICATION", "list_yesno", "no", + help=_("Server needs authentication")) + parameters.register("webmail", "SMTP_SECURED", "list_yesno", "no", + help=_("Use a secured connection to access SMTP server")) + def urls(): return (r'^mailng/webmail/', @@ -57,7 +62,7 @@ def getctx(status, level=1, **kwargs): def folder(request, name, updatenav=True): if not name: name = "INBOX" - order = request.GET.has_key("order") and request.GET["order"] or None + order = request.GET.has_key("order") and request.GET["order"] or "-date" if updatenav: pageid = request.GET.has_key("page") and int(request.GET["page"]) or 1 if not "navparams" in request.session.keys(): @@ -183,23 +188,40 @@ def render_compose(request, form, bodyheader=None, body=None): ctx = getctx("ok", level=2, menu=menu, listing=content) return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") +def send_mail(request): + form = ComposeMailForm(request.POST) + if form.is_valid(): + from email.mime.text import MIMEText + import smtplib + + msg = MIMEText(request.POST["id_body"].encode("utf-8"), + _charset="utf-8") + msg["Subject"] = request.POST["subject"] + msg["From"] = request.POST["from_"] + msg["To"] = request.POST["to"] + rcpts = msg['To'].split(',') + if "cc" in request.POST.keys(): + msg["Cc"] = request.POST["cc"] + rcpts += msg["Cc"].split(",") + try: + s = smtplib.SMTP(parameters.get("webmail", "SMTP_SERVER")) + if parameters.get("webmail", "SMTP_SECURED") == "yes": + s.starttls() + except (smtplib.SMTPException, ssl.SSLError), error: + print error + + if parameters.get("webmail", "SMTP_AUTHENTICATION") == "yes": + s.login(request.user.username, request.session["password"]) + s.sendmail(msg['From'], rcpts, msg.as_string()) + s.quit() + ctx = getctx("ok", url="%s?page=%s" % (request.session["folder"], + request.session["page"])) + return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") + @login_required def compose(request): if request.method == "POST": - form = ComposeMailForm(request.POST) - if form.is_valid(): - from email.mime.text import MIMEText - import smtplib - - msg = MIMEText(request.POST["id_body"].encode("utf-8"), - _charset="utf-8") - msg["Subject"] = request.POST["subject"] - msg["From"] = request.POST["from_"] - msg["to"] = request.POST["to"] - s = smtplib.SMTP(parameters.get("webmail", "SMTP_SERVER")) - s.sendmail(msg['From'], [msg['To']], msg.as_string()) - s.quit() - return folder(request, request.session["navparams"]["folder"], False) + return send_mail(request) form = ComposeMailForm() form.fields["from_"].initial = request.user.username @@ -208,7 +230,7 @@ def compose(request): @login_required def reply(request, folder, mail_id): if request.method == "POST": - return + return send_mail(request) msg = fetchmail(request, folder, mail_id, True) email = ImapEmail(msg) lines = email.body.split('\n') @@ -219,11 +241,27 @@ def reply(request, folder, mail_id): body += ">%s" % l form = ComposeMailForm() form.fields["from_"].initial = request.user.username - form.fields["to"].initial = msg["From"] - m = re.match("re\s*:\s*.+", msg["Subject"].lower()) - if m: - form.fields["subject"].initial = msg["Subject"] + if not "Reply-To" in msg.keys(): + form.fields["to"].initial = msg["From"] else: - form.fields["subject"].initial = "Re: %s" % msg["Subject"] - textheader = msg["From"] + " " + _("wrote:") + form.fields["to"].initial = msg["Reply-To"] + if request.GET.has_key("all"): + form.fields["cc"].initial = "" + toparse = msg["To"].split(",") + if "Cc" in msg.keys(): + toparse += msg["Cc"].split(",") + for addr in toparse: + tmp = EmailAddress(addr) + if tmp.address and tmp.address == request.user.username: + continue + if form.fields["cc"].initial != "": + form.fields["cc"].initial += ", " + form.fields["cc"].initial += tmp.fulladdress + subject = IMAPheader.parse_subject(msg["Subject"]) + m = re.match("re\s*:\s*.+", subject.lower()) + if m: + form.fields["subject"].initial = subject + else: + form.fields["subject"].initial = "Re: %s" % subject + textheader = EmailAddress(msg["From"]).fulladdress + " " + _("wrote:") return render_compose(request, form, textheader, body) diff --git a/extensions/webmail/templates/compose.html b/extensions/webmail/templates/compose.html index 01bb05acd..369cbbd62 100644 --- a/extensions/webmail/templates/compose.html +++ b/extensions/webmail/templates/compose.html @@ -43,8 +43,13 @@
{% include "common/genericform.html" %}
+ {% if body %} +{{ body }} + + {% else %} + + {% endif %} diff --git a/extensions/webmail/templates/index.html b/extensions/webmail/templates/index.html index 988f20e67..80be116bb 100644 --- a/extensions/webmail/templates/index.html +++ b/extensions/webmail/templates/index.html @@ -160,6 +160,7 @@ tr.selected { updatelisting(resp, "hidden"); $$("a[name=back]").addEvent("click", loadFolder); $$("a[name=reply]").addEvent("click", reply); + $$("a[name=replyall]").addEvent("click", replyall); $$("a[name=delete]").addEvent("click", function(event) { var lnk = event.target; @@ -173,16 +174,16 @@ tr.selected { compose_callback = function(resp) { updatelisting(resp); window.addEvent("resize", function(evt) { - var bodysize = $("listing").getSize().y - $("mailheader").getSize().y; - $("id_body").setStyle("height", bodysize + "px"); + setDivHeight("id_body", $("mailheader").getSize().y, 0); }); $$("a[name=back]").addEvent("click", loadFolder); $$("a[name=sendmail]").addEvent("click", function(evt) { evt.stop(); $("composemail").set("send", { - onSuccess: function(res) { - updatelisting(JSON.decode(res)); - $$("tr").addEvent("dblclick", viewmail); + onSuccess: function(resp) { + resp = JSON.decode(resp); + current_anchor.parse_string(resp.url, true).setparams(navparams); + current_anchor.update(); } }); $("composemail").send(); @@ -201,6 +202,11 @@ tr.selected { location.hash += event.target.get("href"); } register_callback("reply", compose_callback); + replyall = function(event) { + event.stop(); + location.hash += event.target.get("href") + "?all=1"; + } + register_callback("replyall", compose_callback); loadFolder = function(event, obj) { if (!$defined(obj)) { diff --git a/extensions/webmail/templates/viewmail.html b/extensions/webmail/templates/viewmail.html index f5798cb47..1c49fc399 100644 --- a/extensions/webmail/templates/viewmail.html +++ b/extensions/webmail/templates/viewmail.html @@ -40,6 +40,9 @@ #emailheaders a:hover { text-decoration: underline; } + pre { + font-size: medium; + } diff --git a/extensions/webmail/templatetags/webextras.py b/extensions/webmail/templatetags/webextras.py index c393d09c7..1941a9fe5 100644 --- a/extensions/webmail/templatetags/webextras.py +++ b/extensions/webmail/templatetags/webextras.py @@ -34,8 +34,8 @@ def viewm_menu(selection, session, mail_id, perms): "url" : "reply/", "img" : "/static/pics/reply.png", "label" : _("Reply")}, - {"name" : "reply-all", - "url" : "", + {"name" : "replyall", + "url" : "reply/", "img" : "/static/pics/reply-all.png", "label" : _("Reply all")}, {"name" : "forward", diff --git a/lib/email_listing.py b/lib/email_listing.py index 216143ae2..5d77853f6 100644 --- a/lib/email_listing.py +++ b/lib/email_listing.py @@ -17,6 +17,9 @@ class MBconnector(object): self.address = address self.port = port + def messages_count(self, **kwargs): + pass + class Page(object): def __init__(self, pageid, id_start, id_stop, items, items_per_page, has_previous, has_next, @@ -80,11 +83,13 @@ class Paginator(object): return p class EmailListing: - def __init__(self, folder=None, elems_per_page=40, **kwargs): + def __init__(self, folder=None, elems_per_page=40, navparams={}, **kwargs): self.folder = folder self.elems_per_page = elems_per_page + self.navparams = navparams order = "order" in kwargs.keys() and kwargs["order"] or None - self.paginator = Paginator(self.mbc.messages_count(self.folder, order), + self.paginator = Paginator(self.mbc.messages_count(folder=self.folder, + order=order), elems_per_page) def render_navbar(self, page): @@ -212,3 +217,4 @@ class Email(object): body = re.sub("<(/?)body", lambda m: "<%sdiv" % m.group(1), body) body = Template(decode(body)).render({}) return (False, body) + diff --git a/static/pics/settings.png b/static/pics/settings.png index 9460dfc746be14e4b2a6a94f91d002fa4a56318f..259ed26dd000fcfb81b2c312980765a181a65486 100644 GIT binary patch delta 558 zcmV+}0@3~B1oZ@vQhy5s3pXIqsqSY00013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2H zRA^-&M@dak?_?!z0005mNkl zB{f-z2o(e&Wd1=BTy-IcD-ky>{R5>f2t%?_3|K*ty!n`-f`8j2l!-JYS+s~Babagj z+jP~1cXfHs<#6vg=Y?~YbQbXR*^|E0KU#Wx=G>Um+`V@vqw8@KK!{2fAfGc>U0XSZ zog{M!bUkk7a|V`W0hpnnO3Q8#3aX%ErjltiO#|o;l8gE|O=HjAr(hPTmg@ji%XJE7kv)6g$-!_9j#TZt wNz^ej(q2lLRHUf2EfP`!6$3*;pld;NWk8DJ!ikO0VpoFFHi=0W6@pc1 z)0c)IL_|6>Zf1rt9qYog`?=?wd+)hF{3itGBb`yAfC|X*Sbrk-7fm#sQEZObQdIk| z_5-MoY;Vl*0cBb`wU_gcfMMFGTgMu}QvtE<=B=4KoPc<}H(@mN9z zasVQr4&EFxo<5r}0QL5*5jNM?kUAym^{@9@vx(gq6)Z7;U=?o+3>uoYCFr_t=( zeoiR7F9OIr1%E5Fbg&WWyGXfGVY&7NAQ_J(?*8+PPQ1#$G)k2c-6wk}mJ65@Oai*A zK`@d!4AiXmm5@%?35laej?rv3c{leKw>3OY@%V68aOMzyJ_~kyIB@do(69TV7 - +