diff --git a/modoboa/extensions/webmail/__init__.py b/modoboa/extensions/webmail/__init__.py index 8299c6ddc..c25468e2a 100644 --- a/modoboa/extensions/webmail/__init__.py +++ b/modoboa/extensions/webmail/__init__.py @@ -2,7 +2,7 @@ from django.utils.translation import ugettext as _, ugettext_lazy from django.core.urlresolvers import reverse from modoboa.lib import events, parameters -from modoboa.extensions import ModoExtension, exts_pool +from modoboa.core.extensions import ModoExtension, exts_pool class Webmail(ModoExtension): @@ -13,7 +13,7 @@ class Webmail(ModoExtension): needs_media = True def load(self): - from app_settings import ParametersForm, UserSettings + from .app_settings import ParametersForm, UserSettings parameters.register(ParametersForm, "Webmail") parameters.register(UserSettings, "Webmail") @@ -24,22 +24,24 @@ class Webmail(ModoExtension): exts_pool.register_extension(Webmail) + @events.observe("UserMenuDisplay") def menu(target, user): if target != "top_menu": return [] - if not user.has_mailbox: + if not user.mailbox_set.count(): return [] return [ - {"name" : "webmail", - "label" : _("Webmail"), - "url" : reverse("modoboa.extensions.webmail.views.index")}, - ] + {"name": "webmail", + "label": _("Webmail"), + "url": reverse("modoboa.extensions.webmail.views.index")}, + ] + @events.observe("UserLogout") def userlogout(request): - from lib import IMAPconnector - from exceptions import ImapError + from .lib import IMAPconnector + from .exceptions import ImapError if not request.user.has_mailbox: return diff --git a/modoboa/extensions/webmail/exceptions.py b/modoboa/extensions/webmail/exceptions.py index 15465e565..f5e56cf41 100644 --- a/modoboa/extensions/webmail/exceptions.py +++ b/modoboa/extensions/webmail/exceptions.py @@ -8,6 +8,7 @@ import re from django.utils.translation import ugettext as _ from modoboa.lib.exceptions import ModoboaException + class WebmailError(ModoboaException): errorexpr = re.compile("\[([^\]]+)\]\s*([^\.]+)") @@ -22,6 +23,7 @@ class WebmailError(ModoboaException): def __str__(self): return self.reason + class ImapError(ModoboaException): def __init__(self, reason): self.reason = reason diff --git a/modoboa/extensions/webmail/fetch_parser.py b/modoboa/extensions/webmail/fetch_parser.py index 22fd79a4b..b049c4de9 100644 --- a/modoboa/extensions/webmail/fetch_parser.py +++ b/modoboa/extensions/webmail/fetch_parser.py @@ -10,7 +10,10 @@ retrieve multiple attributes of a message. This simple module tries to fix that *problem*. """ -class ParseError(Exception): pass + +class ParseError(Exception): + pass + def parse_next_token(buf): """Look for the next *token* @@ -38,6 +41,7 @@ def parse_next_token(buf): raise ParseError("End of buffer reached while looking for a token end") return end + def parse_bodystructure(buf, depth=0, prefix=""): """Special parser for BODYSTRUCTURE response @@ -49,7 +53,7 @@ def parse_bodystructure(buf, depth=0, prefix=""): character into ``buf`` """ ret = [] - pos = 1 # skip the first ')' + pos = 1 # skip the first ')' nb_bodystruct = 0 pnum = 1 while pos < len(buf): @@ -79,7 +83,7 @@ def parse_bodystructure(buf, depth=0, prefix=""): if type(ret[0]) == list or len(ret) >= 7: partnum = prefix if partnum is not None: - return [{"partnum" : partnum, "struct" : ret}], pos + return [{"partnum": partnum, "struct": ret}], pos return ret, pos if c == ' ': pos += 1 @@ -91,6 +95,7 @@ def parse_bodystructure(buf, depth=0, prefix=""): raise ParseError("End of buffer reached while looking for a BODY/BODYSTRUCTURE end") + def parse_fetch_response(data): """Parse a FETCH response, previously issued by a UID command @@ -166,6 +171,7 @@ def parse_fetch_response(data): return result + def dump_bodystructure(bs, depth=0): if depth: print " " * (depth * 4), diff --git a/modoboa/extensions/webmail/imaputils.py b/modoboa/extensions/webmail/imaputils.py index 189351705..fe68c672c 100644 --- a/modoboa/extensions/webmail/imaputils.py +++ b/modoboa/extensions/webmail/imaputils.py @@ -3,7 +3,9 @@ :mod:`imaputils` --- Extra IMAPv4 utilities ------------------------------------------- """ -import imaplib, ssl, email +import imaplib +import ssl +import email import re import time from functools import wraps @@ -17,6 +19,7 @@ from fetch_parser import parse_fetch_response #imaplib.Debug = 4 + class capability(object): """ Simple decorator to check if the server presents the required @@ -38,6 +41,7 @@ class capability(object): return wrapped_func + class BodyStructure(object): """ BODYSTRUCTURE response parser. @@ -87,7 +91,7 @@ class BodyStructure(object): subtype = definition[1].lower() ftype = "%s/%s" % (definition[0].lower(), subtype) if ftype in ("text/plain", "text/html"): - if not self.contents.has_key(subtype): + if not subtype in self.contents: self.contents[subtype] = [params] else: self.contents[subtype].append(params) @@ -135,6 +139,7 @@ class BodyStructure(object): return att return None + class IMAPconnector(object): __metaclass__ = ConnectionsManager @@ -228,7 +233,7 @@ class IMAPconnector(object): if self.m is not None: try: self._cmd("CHECK") - except ImapError, e: + except ImapError: if hasattr(self, "current_mailbox"): del self.current_mailbox else: @@ -261,7 +266,7 @@ class IMAPconnector(object): data = self._cmd("LOGIN", user, passwd) self.m.state = "AUTH" - if self.m.untagged_responses.has_key("CAPABILITY"): + if "CAPABILITY" in self.m.untagged_responses: self.capabilities = \ self.m.untagged_responses.pop('CAPABILITY')[0].split() else: @@ -271,7 +276,7 @@ class IMAPconnector(object): def logout(self): try: self._cmd("CHECK") - except ImapError, e: + except ImapError: pass self._cmd("LOGOUT") del self.m @@ -353,7 +358,7 @@ class IMAPconnector(object): sdescr = d break if sdescr is None: - sdescr = {"name" : parts[0], "path" : path, "sub" : []} + sdescr = {"name": parts[0], "path": path, "sub": []} descr += [sdescr] if self._parse_mailbox_name(sdescr["sub"], path, delimiter, parts[1:]): sdescr["class"] = "subfolders" @@ -379,7 +384,7 @@ class IMAPconnector(object): if re.search("\%s" % delimiter, name): parts = name.split(".") - if not descr.has_key("path"): + if not "path" in descr: descr["path"] = parts[0] descr["sub"] = [] if self._parse_mailbox_name(descr["sub"], parts[0], delimiter, @@ -443,14 +448,16 @@ class IMAPconnector(object): if topmailbox: md_mailboxes = [] else: - md_mailboxes = [{"name" : "INBOX", "class" : "icon-inbox"}, - {"name" : parameters.get_user(user, "DRAFTS_FOLDER"), - "class" : "icon-file"}, - {"name" : 'Junk', "class" : "icon-fire"}, - {"name" : parameters.get_user(user, "SENT_FOLDER"), - "class" : "icon-envelope"}, - {"name" : parameters.get_user(user, "TRASH_FOLDER"), - "class" : "icon-trash"}] + md_mailboxes = [ + {"name": "INBOX", "class": "icon-inbox"}, + {"name": parameters.get_user(user, "DRAFTS_FOLDER"), + "class": "icon-file"}, + {"name": 'Junk', "class": "icon-fire"}, + {"name": parameters.get_user(user, "SENT_FOLDER"), + "class": "icon-envelope"}, + {"name": parameters.get_user(user, "TRASH_FOLDER"), + "class": "icon-trash"} + ] if until_mailbox: name, parent = separate_mailbox(until_mailbox) if parent: @@ -459,10 +466,10 @@ class IMAPconnector(object): if unseen_messages: for mb in md_mailboxes: - if not mb.has_key("send_status"): + if not "send_status" in mb: continue del mb["send_status"] - key = mb.has_key("path") and "path" or "name" + key = "path" if "path" in mb else "name" count = self.unseen_messages(mb[key]) if count == 0: continue @@ -619,7 +626,7 @@ class IMAPconnector(object): if r'\Answered' in data[int(uid)]['FLAGS']: msg['img_flags'] = [static_url('pics/answered.png')] if r'$Forwarded' in data[int(uid)]['FLAGS']: - if msg.has_key('img_flags'): + if 'img_flags' in msg: msg['img_flags'] += [static_url('pics/forwarded.png')] else: msg['img_flags'] = [static_url('pics/forwarded.png')] @@ -650,9 +657,10 @@ class IMAPconnector(object): data = self._cmd( "FETCH", mailid, "(BODYSTRUCTURE %s[HEADER.FIELDS (%s)])" % (bcmd, " ".join(headers)) - ) + ) return data[int(mailid)] + def separate_mailbox(fullname, sep="."): """Split a mailbox name @@ -666,6 +674,7 @@ def separate_mailbox(fullname, sep="."): return name, parent return fullname, None + def get_imapconnector(request): """Simple shortcut to create a connector @@ -679,4 +688,3 @@ def get_imapconnector(request): if __name__ == "__main__": import doctest doctest.testmod() - diff --git a/modoboa/extensions/webmail/lib.py b/modoboa/extensions/webmail/lib.py index e78463b75..df40d947a 100644 --- a/modoboa/extensions/webmail/lib.py +++ b/modoboa/extensions/webmail/lib.py @@ -1,24 +1,24 @@ # coding: utf-8 +import os +import re import time from datetime import datetime, timedelta import email import lxml import chardet - from django.core.files.uploadhandler import FileUploadHandler, SkipFile from django.utils.translation import ugettext as _, ugettext_lazy from django.conf import settings - -import os -import re from modoboa.lib import u2u_decode, tables, parameters from modoboa.lib.webutils import size2integer from modoboa.lib.email_listing import EmailListing -from modoboa.extensions.webmail.exceptions import WebmailError -from modoboa.extensions.webmail.imaputils import IMAPconnector, get_imapconnector, BodyStructure from modoboa.lib.emailutils import ( EmailAddress, Email, prepare_addresses, set_email_headers ) +from modoboa.extensions.webmail.exceptions import WebmailError +from modoboa.extensions.webmail.imaputils import ( + IMAPconnector, get_imapconnector, BodyStructure +) class WMtable(tables.Table): @@ -32,8 +32,12 @@ class WMtable(tables.Table): ) flags = tables.ImgColumn("flags", width="4%") withatts = tables.ImgColumn("withatts", width="2%") - subject = tables.Column("subject", label=ugettext_lazy("Subject"), width="50%", limit=60) - from_ = tables.Column("from", width="20%", label=ugettext_lazy("From"), limit=30) + subject = tables.Column( + "subject", label=ugettext_lazy("Subject"), width="50%", limit=60 + ) + from_ = tables.Column( + "from", width="20%", label=ugettext_lazy("From"), limit=30 + ) date = tables.Column("date", width="15%", label=ugettext_lazy("Date")) cols_order = ["select", "withatts", "flags", "subject", "from_", "date"] @@ -138,7 +142,7 @@ class ImapListing(EmailListing): def __init__(self, user, password, **kwargs): self.user = user self.mbc = IMAPconnector(user=user.username, password=password) - if kwargs.has_key("pattern"): + if "pattern" in kwargs: self.parse_search_parameters(kwargs["criteria"], kwargs["pattern"]) else: @@ -177,13 +181,14 @@ class ImapListing(EmailListing): def computequota(mbc): try: return int(float(mbc.quota_actual) \ - / float(mbc.quota_limit) * 100) + / float(mbc.quota_limit) * 100) except (AttributeError, TypeError): return -1 def getquota(self): return ImapListing.computequota(self.mbc) + class ImapEmail(Email): headernames = [ ('From', True), @@ -191,7 +196,7 @@ class ImapEmail(Email): ('Cc', True), ('Date', True), ('Subject', True), - ] + ] def __init__(self, mbox, mailid, request, dformat="DISPLAYMODE", addrfull=False, links=0): @@ -251,7 +256,7 @@ class ImapEmail(Email): except AttributeError: value = msg[name] if hdr[1]: - self.headers += [{"name" : label, "value" : value}] + self.headers += [{"name": label, "value": value}] try: label = re.sub("-", "_", label) setattr(self, label, value) @@ -260,11 +265,11 @@ class ImapEmail(Email): @property def headers_as_list(self): - return map(lambda hdr: hdr[0].upper(), self.headernames) + return [hdr[0].upper() for hdr in self.headernames] @property def headers_as_text(self): - return " ".join(map(lambda hdr: hdr[0].upper(), self.headernames)) + return " ".join(self.headers_as_list()) def _find_content_charset(self, part): for pos, elem in enumerate(part["params"]): @@ -277,12 +282,12 @@ class ImapEmail(Email): attname = "part_%s" % att["pnum"] params = None key = None - if att.has_key("params") and att["params"] != "NIL": + if "params" in att and att["params"] != "NIL": params = att["params"] key = "name" if key is None and \ - att.has_key("disposition") and len(att["disposition"]) > 1: + "disposition" in att and len(att["disposition"]) > 1: params = att["disposition"][1] key = "filename" @@ -308,13 +313,10 @@ class ImapEmail(Email): fp.write(decode_payload(params["encoding"], content)) fp.close() - def map_cid(self, url): - import re - m = re.match(".*cid:(.+)", url) if m: - if self.bs.inlines.has_key(m.group(1)): + if m.group(1) in self.bs.inlines: return self.bs.inlines[m.group(1)]["fname"] return url @@ -322,12 +324,13 @@ class ImapEmail(Email): from django.template.loader import render_to_string res = render_to_string("webmail/headers.html", { - "headers" : self.headers, - "folder" : kwargs["folder"], "mail_id" : kwargs["mail_id"], - "attachments" : self.attachments != {} and self.attachments or None - }) + "headers": self.headers, + "folder": kwargs["folder"], "mail_id": kwargs["mail_id"], + "attachments": self.attachments != {} and self.attachments or None + }) return res + class Modifier(ImapEmail): def __init__(self, *args, **kwargs): super(Modifier, self).__init__(*args, **kwargs) @@ -348,7 +351,7 @@ class ReplyModifier(Modifier): def __init__(self, mbox, mailid, request, form, **kwargs): super(ReplyModifier, self).__init__( mbox, mailid, request, dformat="EDITOR", **kwargs - ) + ) self.textheader = "%s %s" % (self.From, _("wrote:")) @@ -358,7 +361,7 @@ class ReplyModifier(Modifier): form.fields["to"].initial = self.From else: form.fields["to"].initial = self.Reply_To - if request.GET.get("all", "0") == "1": # reply-all + if request.GET.get("all", "0") == "1": # reply-all form.fields["cc"].initial = "" toparse = self.To.split(",") if hasattr(self, 'Cc'): @@ -386,11 +389,12 @@ class ReplyModifier(Modifier): body += ">%s" % l self.body = body + class ForwardModifier(Modifier): def __init__(self, mbox, mailid, request, form, **kwargs): super(ForwardModifier, self).__init__( mbox, mailid, request, dformat="EDITOR", **kwargs - ) + ) self._header() form.fields["subject"].initial = "Fwd: %s" % self.Subject @@ -433,6 +437,7 @@ class ForwardModifier(Modifier): def _header_end_html(self): return "" + class EmailSignature(object): """User signature @@ -459,6 +464,7 @@ class EmailSignature(object): def __repr__(self): return self._sig + def decode_payload(encoding, payload): """Decode the payload according to the given encoding @@ -477,6 +483,7 @@ def decode_payload(encoding, payload): return quopri.decodestring(payload) return payload + def find_images_in_body(body): """Looks for images inside a HTML body @@ -510,13 +517,14 @@ def find_images_in_body(body): fp.close() p["Content-ID"] = "<%s>" % cid ct = p["Content-Type"] - p.replace_header("Content-Type", '%s; name="%s"' \ - % (ct, os.path.basename(fname))) + p.replace_header("Content-Type", '%s; name="%s"' + % (ct, os.path.basename(fname))) p["Content-Disposition"] = "inline" parts.append(p) return lxml.html.tostring(html), parts + def set_compose_session(request): """Initialize a new "compose" session. @@ -529,9 +537,10 @@ def set_compose_session(request): """ import uuid randid = str(uuid.uuid4()).replace("-", "") - request.session["compose_mail"] = {"id" : randid, "attachments" : []} + request.session["compose_mail"] = {"id": randid, "attachments": []} return randid + def save_attachment(f): """Save a new attachment to the filesystem. @@ -554,6 +563,7 @@ def save_attachment(f): fp.close() return fp.name + def clean_attachments(attlist): """Remove all attachments from the filesystem @@ -564,9 +574,10 @@ def clean_attachments(attlist): fullpath = os.path.join(settings.MEDIA_ROOT, "webmail", att["tmpname"]) try: os.remove(fullpath) - except OSError, e: + except OSError: pass + def html2plaintext(content): """HTML to plain text translation @@ -586,8 +597,9 @@ def html2plaintext(content): return plaintext + def get_current_url(request): - if not request.session.has_key("folder"): + if not "folder" in request.session: return "" res = "%s?page=%s" % (request.session["folder"], request.session["page"]) @@ -596,6 +608,7 @@ def get_current_url(request): res += "&%s=%s" % (p, request.session[p]) return res + def create_mail_attachment(attdef): """Create the MIME part corresponding to the given attachment. @@ -629,7 +642,7 @@ def send_mail(request, posturl=None): :return: a 2-uple (True|False, HttpResponse) """ from email.mime.multipart import MIMEMultipart - from forms import ComposeMailForm + from .forms import ComposeMailForm from modoboa.lib.webutils import _render_to_string from modoboa.auth.lib import get_password @@ -709,11 +722,12 @@ def send_mail(request, posturl=None): return True, dict(url=get_current_url(request)) listing = _render_to_string(request, "webmail/compose.html", - {"form" : form, "noerrors" : True, - "body" : request.POST["id_body"].strip(), - "posturl" : posturl}) + {"form": form, "noerrors": True, + "body": request.POST["id_body"].strip(), + "posturl": posturl}) return False, dict(status="ko", listing=listing, editor=editormode) + class AttachmentUploadHandler(FileUploadHandler): """ Simple upload handler to limit the size of the attachments users diff --git a/modoboa/extensions/webmail/middleware.py b/modoboa/extensions/webmail/middleware.py index 6e4de692e..e5693f318 100644 --- a/modoboa/extensions/webmail/middleware.py +++ b/modoboa/extensions/webmail/middleware.py @@ -2,6 +2,7 @@ from modoboa.lib.webutils import ajax_response, _render_error from modoboa.extensions.webmail.exceptions import WebmailError, ImapError + class WebmailErrorMiddleware(object): def process_exception(self, request, exception): if not isinstance(exception, (WebmailError, ImapError)): diff --git a/modoboa/extensions/webmail/views.py b/modoboa/extensions/webmail/views.py index 8563bcc6b..09ed1a6af 100644 --- a/modoboa/extensions/webmail/views.py +++ b/modoboa/extensions/webmail/views.py @@ -1,25 +1,30 @@ # coding: utf-8 -from django.conf import settings import os +from django.conf import settings from django.http import HttpResponse +from django.shortcuts import render from django.template import Template, Context from django.utils.translation import ugettext as _, ungettext from django.core.urlresolvers import reverse from django.contrib.auth.decorators import login_required from django.views.decorators.csrf import csrf_exempt from modoboa.lib import parameters -from modoboa.lib.webutils import _render, _render_to_string, \ - ajax_response, ajax_simple_response -from modoboa.lib.decorators import needs_mailbox -from exceptions import WebmailError -from forms import FolderForm, AttachmentForm, ComposeMailForm -from imaputils import get_imapconnector, IMAPconnector, separate_mailbox -from lib import decode_payload, AttachmentUploadHandler, \ - save_attachment, ImapListing, EmailSignature, \ - clean_attachments, set_compose_session, send_mail, \ +from modoboa.lib.webutils import ( + _render_to_string, ajax_response, ajax_simple_response +) +from modoboa.extensions.admin.lib import needs_mailbox +from .exceptions import WebmailError +from .forms import FolderForm, AttachmentForm, ComposeMailForm +from .imaputils import get_imapconnector, IMAPconnector, separate_mailbox +from .lib import ( + decode_payload, AttachmentUploadHandler, + save_attachment, ImapListing, EmailSignature, + clean_attachments, set_compose_session, send_mail, ImapEmail, ReplyModifier, ForwardModifier +) from templatetags import webextras + @login_required @needs_mailbox() def getattachment(request): @@ -36,8 +41,6 @@ def getattachment(request): if not mbox or not mailid or not pnum: raise WebmailError(_("Invalid request")) - headers = {"Content-Type": "text/plain", - "Content-Transfer-Encoding": None} imapc = get_imapconnector(request) partdef, payload = imapc.fetchpart(mailid, mbox, pnum) resp = HttpResponse(decode_payload(partdef["encoding"], payload)) @@ -57,6 +60,7 @@ def getattachment(request): resp["Content-Length"] = partdef["size"] return resp + @login_required @needs_mailbox() def move(request): @@ -69,6 +73,7 @@ def move(request): resp.update(status="ok") return ajax_simple_response(resp) + @login_required @needs_mailbox() def delete(request): @@ -78,7 +83,8 @@ def delete(request): raise WebmailError(_("Invalid request")) selection = [item for item in selection if item.isdigit()] mbc = get_imapconnector(request) - mbc.move(",".join(selection), mbox, parameters.get_user(request.user, "TRASH_FOLDER")) + mbc.move(",".join(selection), mbox, + parameters.get_user(request.user, "TRASH_FOLDER")) count = len(selection) message = ungettext("%(count)d message deleted", "%(count)d messages deleted", @@ -86,6 +92,7 @@ def delete(request): resp = dict(status="ok", respmsg=message) return ajax_simple_response(resp) + @login_required @needs_mailbox() def mark(request, name): @@ -102,7 +109,8 @@ def mark(request, name): return ajax_simple_response( dict(status="ok", action=status, mbox=name, unseen=imapc.unseen_messages(name)) - ) + ) + @login_required @needs_mailbox() @@ -112,8 +120,9 @@ def empty(request, name): get_imapconnector(request).empty(name) content = "