From 965b88adbd522ce3d77ddf1b8423d9ba33c88b58 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 11 Feb 2015 10:34:25 +0100 Subject: [PATCH] Remove webmail extension. --- modoboa/extensions/webmail/__init__.py | 0 modoboa/extensions/webmail/app_settings.py | 162 --- modoboa/extensions/webmail/exceptions.py | 44 - modoboa/extensions/webmail/forms.py | 211 --- .../extensions/webmail/general_callbacks.py | 37 - modoboa/extensions/webmail/lib/__init__.py | 33 - modoboa/extensions/webmail/lib/attachments.py | 115 -- .../extensions/webmail/lib/fetch_parser.py | 233 --- modoboa/extensions/webmail/lib/imapemail.py | 327 ----- modoboa/extensions/webmail/lib/imapheader.py | 113 -- modoboa/extensions/webmail/lib/imaputils.py | 788 ---------- modoboa/extensions/webmail/lib/sendmail.py | 65 - modoboa/extensions/webmail/lib/signature.py | 31 - modoboa/extensions/webmail/lib/utils.py | 46 - modoboa/extensions/webmail/modo_extension.py | 25 - .../static/webmail/css/attachments.css | 40 - .../webmail/static/webmail/css/webmail.css | 303 ---- .../webmail/static/webmail/js/webmail.js | 1285 ----------------- .../templates/webmail/attachments.html | 29 - .../webmail/templates/webmail/compose.html | 43 - .../templates/webmail/compose_menubar.html | 13 - .../webmail/templates/webmail/email_list.html | 25 - .../webmail/templates/webmail/folder.html | 10 - .../webmail/templates/webmail/folders.html | 4 - .../webmail/templates/webmail/headers.html | 11 - .../webmail/templates/webmail/index.html | 75 - .../templates/webmail/main_action_bar.html | 9 - .../templates/webmail/upload_done.html | 15 - .../webmail/templatetags/__init__.py | 0 .../webmail/templatetags/webmail_tags.py | 221 --- modoboa/extensions/webmail/urls.py | 25 - modoboa/extensions/webmail/views.py | 618 -------- 32 files changed, 4956 deletions(-) delete mode 100644 modoboa/extensions/webmail/__init__.py delete mode 100644 modoboa/extensions/webmail/app_settings.py delete mode 100644 modoboa/extensions/webmail/exceptions.py delete mode 100644 modoboa/extensions/webmail/forms.py delete mode 100644 modoboa/extensions/webmail/general_callbacks.py delete mode 100644 modoboa/extensions/webmail/lib/__init__.py delete mode 100644 modoboa/extensions/webmail/lib/attachments.py delete mode 100644 modoboa/extensions/webmail/lib/fetch_parser.py delete mode 100644 modoboa/extensions/webmail/lib/imapemail.py delete mode 100644 modoboa/extensions/webmail/lib/imapheader.py delete mode 100644 modoboa/extensions/webmail/lib/imaputils.py delete mode 100644 modoboa/extensions/webmail/lib/sendmail.py delete mode 100644 modoboa/extensions/webmail/lib/signature.py delete mode 100644 modoboa/extensions/webmail/lib/utils.py delete mode 100644 modoboa/extensions/webmail/modo_extension.py delete mode 100644 modoboa/extensions/webmail/static/webmail/css/attachments.css delete mode 100644 modoboa/extensions/webmail/static/webmail/css/webmail.css delete mode 100644 modoboa/extensions/webmail/static/webmail/js/webmail.js delete mode 100644 modoboa/extensions/webmail/templates/webmail/attachments.html delete mode 100644 modoboa/extensions/webmail/templates/webmail/compose.html delete mode 100644 modoboa/extensions/webmail/templates/webmail/compose_menubar.html delete mode 100644 modoboa/extensions/webmail/templates/webmail/email_list.html delete mode 100644 modoboa/extensions/webmail/templates/webmail/folder.html delete mode 100644 modoboa/extensions/webmail/templates/webmail/folders.html delete mode 100644 modoboa/extensions/webmail/templates/webmail/headers.html delete mode 100644 modoboa/extensions/webmail/templates/webmail/index.html delete mode 100644 modoboa/extensions/webmail/templates/webmail/main_action_bar.html delete mode 100644 modoboa/extensions/webmail/templates/webmail/upload_done.html delete mode 100644 modoboa/extensions/webmail/templatetags/__init__.py delete mode 100644 modoboa/extensions/webmail/templatetags/webmail_tags.py delete mode 100644 modoboa/extensions/webmail/urls.py delete mode 100644 modoboa/extensions/webmail/views.py diff --git a/modoboa/extensions/webmail/__init__.py b/modoboa/extensions/webmail/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/modoboa/extensions/webmail/app_settings.py b/modoboa/extensions/webmail/app_settings.py deleted file mode 100644 index 70e371332..000000000 --- a/modoboa/extensions/webmail/app_settings.py +++ /dev/null @@ -1,162 +0,0 @@ -# coding: utf-8 -from django import forms -from django.utils.translation import ugettext_lazy as _ -from modoboa.lib.parameters import AdminParametersForm, UserParametersForm -from modoboa.lib.form_utils import SeparatorField, YesNoField, InlineRadioSelect - - -class ParametersForm(AdminParametersForm): - app = "webmail" - - sep3 = SeparatorField(label=_("General")) - - max_attachment_size = forms.CharField( - label=_("Maximum attachment size"), - initial="2048", - help_text=_( - "Maximum attachment size in bytes (or KB, MB, GB if specified)") - ) - - sep1 = SeparatorField(label=_("IMAP settings")) - - imap_server = forms.CharField( - label=_("Server address"), - initial="127.0.0.1", - help_text=_("Address of your IMAP server") - ) - - imap_secured = YesNoField( - label=_("Use a secured connection"), - initial="no", - help_text=_("Use a secured connection to access IMAP server") - ) - - imap_port = forms.IntegerField( - label=_("Server port"), - initial=143, - help_text=_("Listening port of your IMAP server") - ) - - sep2 = SeparatorField(label=_("SMTP settings")) - - smtp_server = forms.CharField( - label=_("Server address"), - initial="127.0.0.1", - help_text=_("Address of your SMTP server") - ) - - smtp_secured_mode = forms.ChoiceField( - label=_("Secured connection mode"), - choices=[("none", _("None")), - ("starttls", "STARTTLS"), - ("ssl", "SSL/TLS")], - initial="none", - help_text=_("Use a secured connection to access SMTP server"), - widget=InlineRadioSelect - ) - - smtp_port = forms.IntegerField( - label=_("Server port"), - initial=25, - help_text=_("Listening port of your SMTP server") - ) - - smtp_authentication = YesNoField( - label=_("Authentication required"), - initial="no", - help_text=_("Server needs authentication") - ) - - -class UserSettings(UserParametersForm): - app = "webmail" - - sep1 = SeparatorField(label=_("Display")) - - displaymode = forms.ChoiceField( - initial="plain", - label=_("Default message display mode"), - choices=[("html", "html"), ("plain", "text")], - help_text=_("The default mode used when displaying a message"), - widget=InlineRadioSelect() - ) - - enable_links = YesNoField( - initial="no", - label=_("Enable HTML links display"), - help_text=_("Enable/Disable HTML links display") - ) - - messages_per_page = forms.IntegerField( - initial=40, - label=_("Number of displayed emails per page"), - help_text=_("Sets the maximum number of messages displayed in a page") - ) - - refresh_interval = forms.IntegerField( - initial=300, - label=_("Listing refresh rate"), - help_text=_("Automatic folder refresh rate (in seconds)") - ) - - mboxes_col_width = forms.IntegerField( - initial=200, - label=_("Mailboxes container's width"), - help_text=_("The width of the mailbox list container") - ) - - sep2 = SeparatorField(label=_("Mailboxes")) - - trash_folder = forms.CharField( - initial="Trash", - label=_("Trash folder"), - help_text=_("Folder where deleted messages go") - ) - - sent_folder = forms.CharField( - initial="Sent", - label=_("Sent folder"), - help_text=_("Folder where copies of sent messages go") - ) - - drafts_folder = forms.CharField( - initial="Drafts", - label=_("Drafts folder"), - help_text=_("Folder where drafts go") - ) - - sep3 = SeparatorField(label=_("Composing messages")) - - editor = forms.ChoiceField( - initial="plain", - label=_("Default editor"), - choices=[("html", "html"), ("plain", "text")], - help_text=_("The default editor to use when composing a message"), - widget=InlineRadioSelect() - ) - - signature = forms.CharField( - initial="", - label=_("Signature text"), - help_text=_("User defined email signature"), - required=False - ) - - visibility_rules = { - "enable_links": "displaymode=html" - } - - @staticmethod - def has_access(user): - return user.mailbox_set.count() != 0 - - def clean_mboxes_col_width(self): - """Check if the entered value is a positive integer. - - It must also be different from 0. - """ - if self.cleaned_data['mboxes_col_width'] <= 0: - raise forms.ValidationError( - _('Value must be a positive integer (> 0)') - ) - return self.cleaned_data['mboxes_col_width'] diff --git a/modoboa/extensions/webmail/exceptions.py b/modoboa/extensions/webmail/exceptions.py deleted file mode 100644 index e5012ab8a..000000000 --- a/modoboa/extensions/webmail/exceptions.py +++ /dev/null @@ -1,44 +0,0 @@ -# coding: utf-8 -""" -:mod:`exceptions` --- Webmail custom exceptions ------------------------------------------------ - -""" -import re -from django.utils.translation import ugettext as _ -from modoboa.lib.exceptions import ModoboaException, InternalError - - -class WebmailInternalError(InternalError): - errorexpr = re.compile(r'\[([^\]]+)\]\s*([^\.]+)') - - def __init__(self, reason, ajax=False): - match = WebmailInternalError.errorexpr.match(reason) - if not match: - self.reason = reason - else: - self.reason = "%s: %s" % (_("Server response"), match.group(2)) - self.ajax = ajax - - def __str__(self): - return self.reason - - -class UnknownAction(ModoboaException): - - """ - Use this exception when the webmail encounter an unknown action. - """ - http_code = 404 - - def __init__(self): - super(UnknownAction, self).__init__(_("Unknown action")) - - -class ImapError(ModoboaException): - - def __init__(self, reason): - self.reason = reason - - def __str__(self): - return str(self.reason) diff --git a/modoboa/extensions/webmail/forms.py b/modoboa/extensions/webmail/forms.py deleted file mode 100644 index c377fc91b..000000000 --- a/modoboa/extensions/webmail/forms.py +++ /dev/null @@ -1,211 +0,0 @@ -# coding: utf-8 -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText - -import lxml.html - -from django import forms -from django.conf import settings -from django.utils.translation import ugettext_lazy as _ - -from modoboa.lib import parameters -from modoboa.lib.email_utils import set_email_headers -from .lib import ( - ImapEmail, create_mail_attachment -) - - -def html2plaintext(content): - """HTML to plain text translation. - - :param content: some HTML content - """ - if not content: - return "" - 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 make_body_images_inline(body): - """Looks for images inside the body and make them inline. - - Before sending a message in HTML format, it is necessary to find - all img tags contained in the body in order to rewrite them. For - example, icons provided by CKeditor are stored on the server - filesystem and not accessible from the outside. We must embark - them as parts of the MIME message if we want recipients to - display them correctly. - - :param body: the HTML body to parse - """ - import os - from email.mime.image import MIMEImage - from urlparse import urlparse - - html = lxml.html.fromstring(body) - parts = [] - for tag in html.iter("img"): - src = tag.get("src") - if src is None: - continue - o = urlparse(src) - path = os.path.join(settings.MODOBOA_DIR, o.path[1:]) - if not os.path.exists(path): - continue - fname = os.path.basename(path) - cid = "%s@modoboa" % os.path.splitext(fname)[0] - tag.set("src", "cid:%s" % cid) - with open(path, "rb") as fp: - part = MIMEImage(fp.read()) - part["Content-ID"] = "<%s>" % cid - part.replace_header( - "Content-Type", '%s; name="%s"' % (part["Content-Type"], fname) - ) - part["Content-Disposition"] = "inline" - parts.append(part) - return lxml.html.tostring(html), parts - - -class ComposeMailForm(forms.Form): - - """Compose mail form.""" - - to = forms.CharField(label=_("To")) - cc = forms.CharField(label=_("Cc"), required=False) - cci = forms.CharField(label=_("Cci"), required=False) - subject = forms.CharField( - label=_("Subject"), - max_length=255, - required=False - ) - origmsgid = forms.CharField(label="", required=False) - body = forms.CharField(required=False) - - def __init__(self, *args, **kwargs): - """Custom constructor.""" - super(ComposeMailForm, self).__init__(*args, **kwargs) - self.field_widths = { - "cc": "11", - "cci": "11", - "subject": "11" - } - - def _html_msg(self): - """Create a multipart message. - - We attach two alternatives: - * text/html - * text/plain - """ - msg = MIMEMultipart(_subtype="related") - submsg = MIMEMultipart(_subtype="alternative") - body = self.cleaned_data["body"] - charset = "utf-8" - if body: - tbody = html2plaintext(body) - body, images = make_body_images_inline(body) - else: - tbody = "" - images = [] - submsg.attach( - MIMEText(tbody.encode(charset), _subtype="plain", _charset=charset) - ) - submsg.attach( - MIMEText(body.encode(charset), _subtype="html", _charset=charset) - ) - msg.attach(submsg) - for img in images: - msg.attach(img) - return msg - - def _plain_msg(self): - """Create a simple text message. - """ - charset = "utf-8" - text = MIMEText(self.cleaned_data["body"].encode(charset), - _subtype="plain", _charset=charset) - return text - - def _build_msg(self, request): - """Convert form's content to a MIME message. - """ - editormode = parameters.get_user(request.user, "EDITOR") - msg = getattr(self, "_%s_msg" % editormode)() - - if request.session["compose_mail"]["attachments"]: - wrapper = MIMEMultipart(_subtype="mixed") - wrapper.attach(msg) - for attdef in request.session["compose_mail"]["attachments"]: - wrapper.attach(create_mail_attachment(attdef)) - msg = wrapper - return msg - - def to_msg(self, request): - """Convert form's content to an object ready to send. - - We set headers at the end to be sure no one will override - them. - - """ - msg = self._build_msg(request) - set_email_headers( - msg, self.cleaned_data["subject"], request.user.encoded_address, - self.cleaned_data['to'] - ) - origmsgid = self.cleaned_data.get("origmsgid", None) - if origmsgid: - msg["References"] = msg["In-Reply-To"] = origmsgid - return msg - - -class ForwardMailForm(ComposeMailForm): - - """Forward mail form.""" - - def _build_msg(self, request): - """Convert form's content to a MIME message. - - We also add original attachments (if any) to the new message. - """ - from modoboa.extensions.webmail.lib import decode_payload - - mbox = request.GET.get("mbox", None) - mailid = request.GET.get("mailid", None) - msg = super(ForwardMailForm, self)._build_msg(request) - origmsg = ImapEmail(request, False, "%s:%s" % (mbox, mailid)) - if origmsg.attachments: - if not msg.is_multipart or not msg.get_content_subtype() == "mixed": - wrapper = MIMEMultipart(_subtype="mixed") - wrapper.attach(msg) - msg = wrapper - for attpart, fname in origmsg.attachments.items(): - attdef, payload = origmsg.fetch_attachment(attpart) - attdef["fname"] = fname - msg.attach(create_mail_attachment( - attdef, decode_payload(attdef["encoding"], payload) - )) - return msg - - -class FolderForm(forms.Form): - oldname = forms.CharField( - label="", - widget=forms.HiddenInput(attrs={"class": "form-control"}), - required=False - ) - name = forms.CharField( - widget=forms.TextInput(attrs={"class": "form-control"})) - - -class AttachmentForm(forms.Form): - attachment = forms.FileField(label=_("Select a file")) diff --git a/modoboa/extensions/webmail/general_callbacks.py b/modoboa/extensions/webmail/general_callbacks.py deleted file mode 100644 index bfc496b92..000000000 --- a/modoboa/extensions/webmail/general_callbacks.py +++ /dev/null @@ -1,37 +0,0 @@ -from django.utils.translation import ugettext as _ -from django.core.urlresolvers import reverse -from modoboa.lib import events - - -@events.observe("UserMenuDisplay") -def menu(target, user): - if target != "top_menu": - return [] - if not user.mailbox_set.count(): - return [] - return [ - {"name": "webmail", - "label": _("Webmail"), - "url": reverse("webmail:index")}, - ] - - -@events.observe("UserLogout") -def userlogout(request): - from .lib import IMAPconnector - from .exceptions import ImapError - - if not request.user.mailbox_set.count(): - return - try: - m = IMAPconnector(user=request.user.username, - password=request.session["password"]) - except Exception: - # TODO silent exception are bad : we should at least log it - return - - # The following statement may fail under Python 2.6... - try: - m.logout() - except ImapError: - pass diff --git a/modoboa/extensions/webmail/lib/__init__.py b/modoboa/extensions/webmail/lib/__init__.py deleted file mode 100644 index 0f0b166b3..000000000 --- a/modoboa/extensions/webmail/lib/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -from .attachments import ( - create_mail_attachment, save_attachment, clean_attachments, - set_compose_session, AttachmentUploadHandler -) -from .imapemail import ImapEmail, ReplyModifier, ForwardModifier -from .imaputils import ( - BodyStructure, IMAPconnector, get_imapconnector, separate_mailbox -) -from .signature import EmailSignature -from .utils import decode_payload, WebmailNavigationParameters -from .sendmail import send_mail -from .fetch_parser import parse_fetch_response - - -__all__ = [ - 'AttachmentUploadHandler', - 'BodyStructure', - 'EmailSignature', - 'ForwardModifier', - 'IMAPconnector', - 'ImapEmail', - 'ReplyModifier', - 'WebmailNavigationParameters', - 'clean_attachments', - 'create_mail_attachment', - 'decode_payload', - 'get_imapconnector', - 'parse_fetch_response', - 'save_attachment', - 'send_mail', - 'separate_mailbox', - 'set_compose_session', -] diff --git a/modoboa/extensions/webmail/lib/attachments.py b/modoboa/extensions/webmail/lib/attachments.py deleted file mode 100644 index 7da2ed898..000000000 --- a/modoboa/extensions/webmail/lib/attachments.py +++ /dev/null @@ -1,115 +0,0 @@ -import os -from rfc6266 import build_header -from django.conf import settings -from django.core.files.uploadhandler import FileUploadHandler, SkipFile -from modoboa.lib import parameters -from modoboa.lib.exceptions import InternalError -from modoboa.lib.web_utils import size2integer - - -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, "webmail") - try: - fp = NamedTemporaryFile(dir=dstdir, delete=False) - except Exception as e: - raise InternalError(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, "webmail", att["tmpname"]) - try: - os.remove(fullpath) - except OSError: - pass - - -def create_mail_attachment(attdef, payload=None): - """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 - - if "content-type" in attdef: - maintype, subtype = attdef["content-type"].split("/") - elif "Content-Type" in attdef: - maintype, subtype = attdef["Content-Type"].split("/") - else: - return None - res = MIMEBase(maintype, subtype) - if payload is None: - with open(os.path.join( - settings.MEDIA_ROOT, "webmail", attdef["tmpname"]), "rb") as fp: - res.set_payload(fp.read()) - else: - res.set_payload(payload) - Encoders.encode_base64(res) - if isinstance(attdef['fname'], str): - attdef['fname'] = attdef['fname'].decode('utf-8') - res['Content-Disposition'] = build_header(attdef['fname']) - return res - - -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/modoboa/extensions/webmail/lib/fetch_parser.py b/modoboa/extensions/webmail/lib/fetch_parser.py deleted file mode 100644 index 8d83cdf2d..000000000 --- a/modoboa/extensions/webmail/lib/fetch_parser.py +++ /dev/null @@ -1,233 +0,0 @@ -# coding: utf-8 -""" -:mod:`fetch_parser` --- Simple parser for FETCH responses ---------------------------------------------------------- - -The ``imaplib`` module doesn't parse IMAP responses, it returns raw -values. This is pretty annoying when one FETCH command is issued to -retrieve multiple attributes of a message. - -This simple module tries to fix that *problem*. -""" - - -class ParseError(Exception): - pass - - -class Token(object): - - def __init__(self, value): - self.value = value - - -class Literal(Token): - - def __init__(self, value): - super(Literal, self).__init__(value) - self.next_token_len = int(self.value[1:-1]) - - -def parse_next_token(buf): - """Look for the next *token* - - By *token*, I mean: *literal*, *quoted* or anything else until the - next ' ' or ')' character (number, NIL and others should fall into - this last category). - - :param buf: the buffer to parse - :return: the position of the token's last caracter into ``buf`` - """ - end = -1 - klass = Token - if buf[0] == '{': - # Literal - end = buf.find('}') - klass = Literal - elif buf[0] == '"': - # quoted - end = buf.find('"', 1) - else: - for pos, c in enumerate(buf): - if c in [' ', ')']: - end = pos - 1 - break - if end == -1: - raise ParseError( - "End of buffer reached while looking for a token end") - end += 1 - token = klass(buf[:end]) - return token, end - - -def parse_bodystructure(buf, depth=0, prefix=""): - """Special parser for BODYSTRUCTURE response - - This function tries to transform a BODYSTRUCTURE response sent by - the server into the corresponding list structure. - - :param buf: the buffer to parse - :return: a list object and the position of the last scanned - character into ``buf`` - """ - ret = [] - pos = 1 # skip the first ')' - nb_bodystruct = 0 - pnum = 1 - while pos < len(buf): - c = buf[pos] - if c == '(': - nprefix = "%s.%s" % (prefix, pnum) if prefix != "" else "%s" % pnum - subret, end = parse_bodystructure(buf[pos:], depth + 1, nprefix) - pnum += 1 - if nb_bodystruct == 0: - ret.append(subret) - elif nb_bodystruct == 1: - newret = [ret.pop(-1), subret] - ret.append(newret) - else: - ret[-1].append(subret) - pos += end + 1 - nb_bodystruct += 1 - continue - - if c == ')': - partnum = None - if depth: - # FIXME : the following is buggy because it doesn't - # make a difference between a content-disposition with - # multiple args beeing parsed and a mime part! (see - # the last example at the end of the file) - if isinstance(ret[0], list) or len(ret) >= 7: - partnum = prefix - if partnum is not None: - return [{"partnum": partnum, "struct": ret}], pos - return ret, pos - if c == ' ': - pos += 1 - continue - nb_bodystruct = 0 - token, end = parse_next_token(buf[pos:]) - pos += end - if isinstance(token, Literal): - ret.append(buf[pos:pos + token.next_token_len].strip('"')) - pos += token.next_token_len - else: - ret.append(token.value.strip('"')) - - 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 - - We extract the message number, the UID and its value and consider - what remains as the response data. - - :param data: the data returned by the ``imaplib`` command - :return: a dictionnary - """ - result = {} - cpt = 0 - while cpt < len(data): - content = () - while cpt < len(data) and data[cpt] != ')': - if isinstance(data[cpt], str): - # FIXME : probably an unsolicited response - cpt += 1 - continue - content += data[cpt] - cpt += 1 - cpt += 1 - - buf = "".join(content) - parts = buf.split(' ', 3) - msgdef = result[int(parts[2])] = {} - response = parts[3] - - while len(response): - if response.startswith('BODY') and response[4] == '[': - end = response.find(']', 5) - if response[end + 1] == '<': - end = response.find('>', end + 1) - end += 1 - else: - end = response.find(' ') - cmdname = response[:end] - response = response[end + 1:] - - end = 0 - if cmdname in ['BODY', 'BODYSTRUCTURE', 'FLAGS']: - parendepth = 0 - instring = False - for pos, c in enumerate(response): - if not instring and c == '"': - instring = True - continue - if instring and c == '"': - if pos and response[pos - 1] != '\\': - instring = False - continue - if not instring and c == '(': - parendepth += 1 - continue - if not instring and c == ')': - parendepth -= 1 - if parendepth == 0: - end = pos + 1 - break - else: - token, end = parse_next_token(response) - if isinstance(token, Literal): - response = response[end:] - end = token.next_token_len - - msgdef[cmdname] = response[:end] - response = response[end + 1:] - try: - func = globals()["parse_%s" % cmdname.lower()] - msgdef[cmdname] = func(msgdef[cmdname])[0] - except KeyError: - pass - - return result - - -def dump_bodystructure(bs, depth=0): - if depth: - print " " * (depth * 4), - if isinstance(bs[0], dict): - print "%s :" % bs[0]["partnum"], - struct = bs[0]["struct"] - else: - struct = bs - - if isinstance(struct[0], list): - print "multipart/%s" % struct[1] - for part in struct[0]: - dump_bodystructure(part, depth + 1) - else: - print "%s/%s" % (struct[0], struct[1]) - - -if __name__ == "__main__": -# resp = [('855 (UID 46931 BODYSTRUCTURE ((("text" "plain" ("charset" "iso-8859-1") NIL NIL "quoted-printable" 886 32 NIL NIL NIL NIL)("text" "html" ("charset" "us-ascii") NIL NIL "quoted-printable" 1208 16 NIL NIL NIL NIL) "alternative" ("boundary" "----=_NextPart_001_0003_01CCC564.B2F64FF0") NIL NIL NIL)("application" "octet-stream" ("name" "Carte Verte_2.pdf") NIL NIL "base64" 285610 NIL ("attachment" ("filename" "Carte Verte_2.pdf")) NIL NIL) "mixed" ("boundary" "----=_NextPart_000_0002_01CCC564.B2F64FF0") NIL NIL NIL) BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)] {153}', 'From: \r\nTo: \r\nCc: \r\nSubject: Notre contact du 28/12/2011 - 192175092\r\nDate: Wed, 28 Dec 2011 13:29:17 +0100\r\n\r\n'), ')'] - -# resp = [('856 (UID 46936 BODYSTRUCTURE (("text" "plain" ("charset" "ISO-8859-1") NIL NIL "quoted-printable" 724 22 NIL NIL NIL NIL)("text" "html" ("charset" "ISO-8859-1") NIL NIL "quoted-printable" 2662 48 NIL NIL NIL NIL) "alternative" ("boundary" "----=_Part_1326887_254624357.1325083973970") NIL NIL NIL) BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)] {258}', 'Date: Wed, 28 Dec 2011 15:52:53 +0100 (CET)\r\nFrom: =?ISO-8859-1?Q?Malakoff_M=E9d=E9ric?= \r\nTo: Antoine Nguyen \r\nSubject: =?ISO-8859-1?Q?Votre_inscription_au_grand_Jeu_Malakoff_M=E9d=E9ric?=\r\n\r\n'), ')'] - - resp = [ - ('856 (UID 11111 BODYSTRUCTURE ((("text" "plain" ("charset" "UTF-8") NIL NIL "7bit" 0 0 NIL NIL NIL NIL) "mixed" ("boundary" "----=_Part_407172_3159001.1321948277321") NIL NIL NIL)("application" "octet-stream" ("name" "26274308.pdf") NIL NIL "base64" 14906 NIL ("attachment" ("filename" "26274308.pdf")) NIL NIL) "mixed" ("boundary" "----=_Part_407171_9686991.1321948277321") NIL NIL NIL)', - ), - ')'] - -# resp = [('19 (UID 19 FLAGS (\\Seen) BODYSTRUCTURE (("text" "plain" ("charset" "ISO-8859-1" "format" "flowed") NIL NIL "7bit" 2 1 NIL NIL NIL NIL)("message" "rfc822" ("name*" "ISO-8859-1\'\'%5B%49%4E%53%43%52%49%50%54%49%4F%4E%5D%20%52%E9%63%E9%70%74%69%6F%6E%20%64%65%20%76%6F%74%72%65%20%64%6F%73%73%69%65%72%20%64%27%69%6E%73%63%72%69%70%74%69%6F%6E%20%46%72%65%65%20%48%61%75%74%20%44%E9%62%69%74") NIL NIL "8bit" 3632 ("Wed, 13 Dec 2006 20:30:02 +0100" {70}', -# "[INSCRIPTION] R\xe9c\xe9ption de votre dossier d'inscription Free Haut D\xe9bit"), -# (' (("Free Haut Debit" NIL "inscription" "freetelecom.fr")) (("Free Haut Debit" NIL "inscription" "freetelecom.fr")) ((NIL NIL "hautdebit" "freetelecom.fr")) ((NIL NIL "nguyen.antoine" "wanadoo.fr")) NIL NIL NIL "<20061213193125.9DA0919AC@dgroup2-2.proxad.net>") ("text" "plain" ("charset" "iso-8859-1") NIL NIL "8bit" 1428 38 NIL ("inline" NIL) NIL NIL) 76 NIL ("inline" ("filename*" "ISO-8859-1\'\'%5B%49%4E%53%43%52%49%50%54%49%4F%4E%5D%20%52%E9%63%E9%70%74%69%6F%6E%20%64%65%20%76%6F%74%72%65%20%64%6F%73%73%69%65%72%20%64%27%69%6E%73%63%72%69%70%74%69%6F%6E%20%46%72%65%65%20%48%61%75%74%20%44%E9%62%69%74")) NIL NIL) "mixed" ("boundary" "------------040706080908000209030901") NIL NIL NIL) BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)] {266}', -# 'Date: Tue, 19 Dec 2006 19:50:13 +0100\r\nFrom: Antoine Nguyen \r\nTo: Antoine Nguyen \r\nSubject: [Fwd: [INSCRIPTION] =?ISO-8859-1?Q?R=E9c=E9ption_de_votre_?=\r\n =?ISO-8859-1?Q?dossier_d=27inscription_Free_Haut_D=E9bit=5D?=\r\n\r\n'), ')'] - - # resp = [('123 (UID 3 BODYSTRUCTURE (((("text" "plain" ("charset" "iso-8859-1") NIL NIL "quoted-printable" 1266 30 NIL NIL NIL NIL)("text" "html" ("charset" "iso-8859-1") NIL NIL "quoted-printable" 8830 227 NIL NIL NIL NIL) "alternative" ("boundary" "_000_152AC7ECD1F8AB43A9AD95DBDDCA3118082C09GKIMA24cmcicfr_") NIL NIL NIL)("image" "png" ("name" "image005.png") "" "image005.png" "base64" 7464 NIL ("inline" ("filename" "image005.png" "size" "5453" "creation-date" "Tue, 06 Sep 2011 13:33:49 GMT" "modification-date" "Tue, 06 Sep 2011 13:33:49 GMT")) NIL NIL)("image" "jpeg" ("name" "image006.jpg") "" "image006.jpg" "base64" 2492 NIL ("inline" ("filename" "image006.jpg" "size" "1819" "creation-date" "Tue, 06 Sep 2011 13:33:49 GMT" "modification-date" "Tue, 06 Sep 2011 13:33:49 GMT")) NIL NIL) "related" ("boundary" "_006_152AC7ECD1F8AB43A9AD95DBDDCA3118082C09GKIMA24cmcicfr_" "type" "multipart/alternative") NIL NIL NIL)("application" "pdf" ("name" "bilan assurance CIC.PDF") NIL "bilan assurance CIC.PDF" "base64" 459532 NIL ("attachment" ("filename" "bilan assurance CIC.PDF" "size" "335811" "creation-date" "Fri, 16 Sep 2011 12:45:23 GMT" "modification-date" "Fri, 16 Sep 2011 12:45:23 GMT")) NIL NIL)(("text" "plain" ("charset" "utf-8") NIL NIL "quoted-printable" 1389 29 NIL NIL NIL NIL)("text" "html" ("charset" "utf-8") NIL NIL "quoted-printable" 1457 27 NIL NIL NIL NIL) "alternative" ("boundary" "===============0775904800==") ("inline" NIL) NIL NIL) "mixed" ("boundary" "_007_152AC7ECD1F8AB43A9AD95DBDDCA3118082C09GKIMA24cmcicfr_") NIL ("fr-FR") NIL)',), ')'] - - # resp = [('856 (UID 11111 BODYSTRUCTURE ((("text" "plain" ("charset" "UTF-8") NIL NIL "7bit" 0 0 NIL NIL NIL NIL) "mixed" ("boundary" "----=_Part_407172_3159001.1321948277321") NIL NIL NIL)("application" "octet-stream" ("name" "26274308.pdf") NIL NIL "base64" 14906 NIL ("attachment" ("filename" "(26274308.pdf")) NIL NIL) "mixed" ("boundary" "----=_Part_407171_9686991.1321948277321") NIL NIL NIL)',), ')'] - - print parse_fetch_response(resp) diff --git a/modoboa/extensions/webmail/lib/imapemail.py b/modoboa/extensions/webmail/lib/imapemail.py deleted file mode 100644 index 0b68b31e2..000000000 --- a/modoboa/extensions/webmail/lib/imapemail.py +++ /dev/null @@ -1,327 +0,0 @@ -""" -Set of classes to manipulate/display emails inside the webmail. -""" -import os -import re -import email - -import chardet -from rfc6266 import parse_headers - -from django.conf import settings -from django.utils.translation import ugettext as _ - -from modoboa.lib import parameters, u2u_decode -from modoboa.lib.email_utils import Email, EmailAddress -from .imaputils import ( - get_imapconnector, BodyStructure -) -from .utils import decode_payload - - -class ImapEmail(Email): - - """ - A class to represent an email fetched from an IMAP server. - """ - - headernames = [ - ('From', True), - ('To', True), - ('Cc', True), - ('Date', True), - ('Subject', True), - ] - - def __init__(self, request, addrfull, *args, **kwargs): - super(ImapEmail, self).__init__(*args, **kwargs) - self.request = request - self.addrfull = addrfull - self.imapc = get_imapconnector(request) - self.mbox, self.mailid = self.mailid.split(":") - - headers = self.msg['BODY[HEADER.FIELDS (%s)]' % self.headers_as_text] - msg = email.message_from_string(headers) - for hdr in self.headernames: - label = hdr[0] - hdrvalue = self.get_header(msg, label) - if not hdrvalue: - continue - if hdr[1]: - self.headers += [{"name": label, "value": hdrvalue}] - label = re.sub("-", "_", label) - setattr(self, label, hdrvalue) - - def get_header(self, msg, hdrname): - """Look for a particular header. - - We also try to decode the default value. - """ - from . import imapheader - - hdrvalue = super(ImapEmail, self).get_header(msg, hdrname) - if not hdrvalue: - return "" - try: - key = re.sub("-", "_", hdrname).lower() - hdrvalue = getattr(imapheader, "parse_%s" % key)( - hdrvalue, full=self.addrfull - ) - except AttributeError: - pass - return hdrvalue - - @property - def msg(self): - """ - """ - if self._msg is None: - self._msg = self.imapc.fetchmail( - self.mbox, self.mailid, readonly=False, - headers=self.headers_as_list - ) - self.bs = BodyStructure(self._msg['BODYSTRUCTURE']) - self._find_attachments() - if self.dformat not in ["plain", "html"]: - self.dformat = parameters.get_user( - self.request.user, self.dformat - ) - fallback_fmt = "html" if self.dformat == "plain" else "plain" - self.mformat = self.dformat \ - if self.dformat in self.bs.contents else fallback_fmt - return self._msg - - @property - def headers_as_list(self): - return [hdr[0].upper() for hdr in self.headernames] - - @property - def headers_as_text(self): - return " ".join(self.headers_as_list) - - @property - def body(self): - """Load email's body. - - This operation has to be made "on demand" because it requires - a communication with the IMAP server. - - """ - if self._body is None and self.bs.contents: - bodyc = u'' - for part in self.bs.contents[self.mformat]: - pnum = part['pnum'] - data = self.imapc._cmd( - "FETCH", self.mailid, "(BODY.PEEK[%s])" % pnum - ) - content = decode_payload( - part['encoding'], data[int(self.mailid)]['BODY[%s]' % pnum] - ) - charset = self._find_content_charset(part) - if charset is not None: - try: - content = content.decode(charset) - except (UnicodeDecodeError, LookupError): - result = chardet.detect(content) - content = content.decode(result['encoding']) - bodyc += content - self._fetch_inlines() - self._body = getattr(self, "viewmail_%s" % self.mformat)( - bodyc, links=self.links - ) - return self._body - - @body.setter - def body(self, value): - self._body = value - - def _find_content_charset(self, part): - for pos, elem in enumerate(part["params"]): - if elem == "charset": - return part["params"][pos + 1] - return None - - def _find_attachments(self): - """Retrieve attachments from the parsed body structure. - - We try to find and decode a file name for each attachment. If - we failed, a generic name will be used (ie. part_1, part_2, ...). - """ - for att in self.bs.attachments: - attname = "part_%s" % att["pnum"] - if "params" in att and att["params"] != "NIL": - attname = u2u_decode.u2u_decode(att["params"][1]) \ - .strip("\r\t\n") - elif "disposition" in att and len(att["disposition"]) > 1: - for pos, value in enumerate(att["disposition"][1]): - if not value.startswith("filename"): - continue - header = "%s; %s=%s" \ - % (att['disposition'][0], - value, - att["disposition"][1][pos + 1].strip("\r\t\n")) - attname = parse_headers(header).filename_unsafe - if attname is None: - attname = u2u_decode.u2u_decode( - att["disposition"][1][pos + 1] - ).strip("\r\t\n") - break - self.attachments[att["pnum"]] = attname - - def _fetch_inlines(self): - for cid, params in self.bs.inlines.iteritems(): - if re.search(r"\.\.", cid): - continue - fname = "webmail/%s_%s" % (self.mailid, cid) - path = os.path.join(settings.MEDIA_ROOT, fname) - params["fname"] = os.path.join(settings.MEDIA_URL, fname) - if os.path.exists(path): - continue - - pdef, content = self.imapc.fetchpart( - self.mailid, self.mbox, params["pnum"] - ) - with open(path, "wb") as fpo: - fpo.write(decode_payload(params["encoding"], content)) - - def map_cid(self, url): - m = re.match(".*cid:(.+)", url) - if m: - if m.group(1) in self.bs.inlines: - return self.bs.inlines[m.group(1)]["fname"] - return url - - def render_headers(self, **kwargs): - 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 - }) - return res - - def fetch_attachment(self, pnum): - """Fetch an attachment from the IMAP server. - """ - return self.imapc.fetchpart(self.mailid, self.mbox, pnum) - - -class Modifier(ImapEmail): - - """Message modifier.""" - - def __init__(self, form, *args, **kwargs): - kwargs["dformat"] = "EDITOR" - super(Modifier, self).__init__(*args, **kwargs) - self.form = form - getattr(self, "_modify_%s" % self.dformat)() - - def _modify_plain(self): - self.body = re.sub("", "", self.body) - - def _modify_html(self): - if self.dformat == "html" and self.mformat != self.dformat: - self.body = re.sub("", "", self.body) - self.body = re.sub("\n", "
", self.body) - - @property - def subject(self): - """Just a shortcut to return a subject in any case.""" - return self.Subject if hasattr(self, "Subject") else "" - - -class ReplyModifier(Modifier): - - """Modify a message to reply to it.""" - - headernames = ImapEmail.headernames + \ - [("Reply-To", True), - ("Message-ID", False)] - - def __init__(self, *args, **kwargs): - super(ReplyModifier, self).__init__(*args, **kwargs) - - self.textheader = "%s %s" % (self.From, _("wrote:")) - if hasattr(self, "Message_ID"): - self.form.fields["origmsgid"].initial = self.Message_ID - if not hasattr(self, "Reply_To"): - self.form.fields["to"].initial = self.From - else: - self.form.fields["to"].initial = self.Reply_To - if self.request.GET.get("all", "0") == "1": # reply-all - self.form.fields["cc"].initial = "" - toparse = self.To.split(",") - if hasattr(self, 'Cc'): - toparse += self.Cc.split(",") - for addr in toparse: - tmp = EmailAddress(addr) - if tmp.address and tmp.address == self.request.user.username: - continue - if self.form.fields["cc"].initial != "": - self.form.fields["cc"].initial += ", " - self.form.fields["cc"].initial += tmp.fulladdress - m = re.match(r"re\s*:\s*.+", self.subject.lower()) - if m: - self.form.fields["subject"].initial = self.subject - else: - self.form.fields["subject"].initial = "Re: %s" % self.subject - - def _modify_plain(self): - super(ReplyModifier, self)._modify_plain() - lines = self.body.split('\n') - body = "" - for l in lines: - if body != "": - body += "\n" - body += ">%s" % l - self.body = body - - -class ForwardModifier(Modifier): - - """ - Modify a message so it can be forwarded. - """ - - def __init__(self, *args, **kwargs): - super(ForwardModifier, self).__init__(*args, **kwargs) - self._header() - self.form.fields["subject"].initial = "Fwd: %s" % self.subject - - def __getfunc(self, name): - return getattr(self, "%s_%s" % (name, self.dformat)) - - def _header(self): - self.textheader = self.__getfunc("_header_begin")() + "\n" - self.textheader += \ - self.__getfunc("_header_line")(_("Subject"), self.subject) - self.textheader += \ - self.__getfunc("_header_line")(_("Date"), self.Date) - for hdr in ["From", "To", "Reply-To"]: - try: - key = re.sub("-", "_", hdr) - value = getattr(self, key) - self.textheader += \ - self.__getfunc("_header_line")(_(hdr), value) - except AttributeError: - pass - self.textheader += self.__getfunc("_header_end")() - - def _header_begin_plain(self): - return "----- %s -----" % _("Original message") - - def _header_begin_html(self): - return "----- %s -----" % _("Original message") - - def _header_line_plain(self, key, value): - return "%s: %s\n" % (key, value) - - def _header_line_html(self, key, value): - return "

%s: %s

" % (key, value) - - def _header_end_plain(self): - return "\n" - - def _header_end_html(self): - return "" diff --git a/modoboa/extensions/webmail/lib/imapheader.py b/modoboa/extensions/webmail/lib/imapheader.py deleted file mode 100644 index 4d6d9628d..000000000 --- a/modoboa/extensions/webmail/lib/imapheader.py +++ /dev/null @@ -1,113 +0,0 @@ -""" -Set of functions used to parse and transform email headers. -""" -import chardet -from modoboa.lib.email_utils import EmailAddress - -__all__ = [ - 'parse_from', 'parse_to', 'parse_message_id', 'parse_date', - 'parse_reply_to', 'parse_cc', 'parse_subject' -] - - -def to_unicode(value): - """Try to convert a string to unicode. - """ - if value is None or isinstance(value, unicode): - return value - try: - value = value.decode('utf-8') - except UnicodeDecodeError: - pass - else: - return value - try: - res = chardet.detect(value) - except UnicodeDecodeError: - return value - if res["encoding"] == "ascii": - return value - return value.decode(res["encoding"]) - - -def parse_address(value, **kwargs): - """Parse an email address. - """ - addr = EmailAddress(value) - if "full" in kwargs.keys() and kwargs["full"]: - return to_unicode(addr.fulladdress) - result = addr.name and addr.name or addr.fulladdress - return to_unicode(result) - - -def parse_address_list(values, **kwargs): - """Parse a list of email addresses. - """ - lst = values.split(",") - result = "" - for addr in lst: - if result != "": - result += ", " - result += parse_address(addr, **kwargs) - return result - - -def parse_from(value, **kwargs): - """Parse a From: header. - """ - return parse_address(value, **kwargs) - - -def parse_to(value, **kwargs): - """Parse a To: header. - """ - return parse_address_list(value, **kwargs) - - -def parse_cc(value, **kwargs): - """Parse a Cc: header. - """ - return parse_address_list(value, **kwargs) - - -def parse_reply_to(value, **kwargs): - """Parse a Reply-To: header. - """ - return parse_address_list(value, **kwargs) - - -def parse_date(value, **kwargs): - """Parse a Date: header. - """ - import datetime - import email - - tmp = email.utils.parsedate_tz(value) - if not tmp: - return value - try: - ndate = datetime.datetime(*(tmp)[:7]) - now = datetime.datetime.now() - if now - ndate > datetime.timedelta(7): - return ndate.strftime("%d/%m/%Y %H:%M") - return ndate.strftime("%a %H:%M") - except ValueError: - return value - - -def parse_message_id(value, **kwargs): - """Parse a Message-ID: header. - """ - return value.strip('\n') - - -def parse_subject(value, **kwargs): - """Parse a Subject: header. - """ - from modoboa.lib import u2u_decode - - try: - subject = u2u_decode.u2u_decode(value) - except UnicodeDecodeError: - subject = value - return to_unicode(subject) diff --git a/modoboa/extensions/webmail/lib/imaputils.py b/modoboa/extensions/webmail/lib/imaputils.py deleted file mode 100644 index 7d34a75c8..000000000 --- a/modoboa/extensions/webmail/lib/imaputils.py +++ /dev/null @@ -1,788 +0,0 @@ -# coding: utf-8 -""" -:mod:`imaputils` --- Extra IMAPv4 utilities -------------------------------------------- -""" -import email -import imaplib -import re -import ssl -import time -from functools import wraps - -from django.utils.translation import ugettext as _ - -from ..exceptions import ImapError, WebmailInternalError -from .fetch_parser import parse_fetch_response -from modoboa.lib import parameters -from modoboa.lib.connections import ConnectionsManager -from modoboa.lib.exceptions import InternalError - - -# imaplib.Debug = 4 - - -class capability(object): - - """ - Simple decorator to check if the server presents the required - capability. If not, a fallback method is called instead. - - :param name: the capability name (upper case) - :param fallback_method: a method's name - """ - - def __init__(self, name, fallback_method): - self.name = name - self.fallback_method = fallback_method - - def __call__(self, method): - @wraps(method) - def wrapped_func(cls, *args, **kwargs): - if self.name in cls.capabilities: - return method(cls, *args, **kwargs) - return getattr(cls, self.fallback_method)(cls, **kwargs) - - return wrapped_func - - -class BodyStructure(object): - - """ - BODYSTRUCTURE response parser. - - Just a simple class that tries to distinguish content parts from - attachments. - """ - - def __init__(self, definition=None): - self.is_multipart = False - self.contents = {} - self.attachments = [] - self.inlines = {} - - if definition is not None: - self.load_from_definition(definition) - - def __store_part(self, definition, pnum, multisubtype): - """Store the given message part in the appropriate category. - - This method sort parts in two categories: - - * contents (what is going to be displayed) - * attachments - - As there is no official definition about what is a content and - what is an attachment, the following rules are applied: - - * If the MIME type is text/plain or text/html: - - * If no previous part of this type has already been seen, - it's a content - * Otherwise it's an attachment - - * Else, if the multipart subtype is related, we consider this - part as content because it is certainly an embedded image - - * Any other MIME type is considered as an attachment (for now) - - :param definition: a part definition (list) - :param pnum: the part's number - :param multisubtype: the multipart subtype - - """ - pnum = "1" if pnum is None else pnum - params = dict(pnum=pnum, params=definition[2], cid=definition[3], - description=definition[4], encoding=definition[5], - size=definition[6]) - mtype = definition[0].lower() - subtype = definition[1].lower() - ftype = "%s/%s" % (definition[0].lower(), subtype) - if ftype in ("text/plain", "text/html"): - if subtype not in self.contents: - self.contents[subtype] = [params] - else: - self.contents[subtype].append(params) - return - elif multisubtype in ["related"]: - self.inlines[params["cid"].strip("<>")] = params - return - - params["Content-Type"] = ftype - if len(definition) > 7: - extensions = ["md5", "disposition", "language", "location"] - if mtype == "text": - extensions = ["textlines"] + extensions - elif ftype == "message/rfc822": - extensions = [ - "envelopestruct", - "bodystruct", - "textlines"] + extensions - for idx, value in enumerate(definition[7:]): - params[extensions[idx]] = value - - self.attachments += [params] - - def load_from_definition(self, definition, multisubtype=None): - if isinstance(definition, dict): - struct = definition["struct"] - pnum = definition["partnum"] - elif isinstance(definition[0], dict): - struct = definition[0]["struct"] - pnum = definition[0]["partnum"] - else: - struct = definition - pnum = None - - if isinstance(struct[0], list): - for part in struct[0]: - self.load_from_definition(part, struct[1]) - return - - self.__store_part(struct, pnum, multisubtype) - - def has_attachments(self): - return len(self.attachments) - - def find_attachment(self, pnum): - for att in self.attachments: - if pnum == att["pnum"]: - return att - return None - - -class IMAPconnector(object): - - """The IMAPv4 connector.""" - - __metaclass__ = ConnectionsManager - - list_base_pattern = ( - r'\((?P.*?)\) "(?P.*)" "?(?P[^"]*)"?' - ) - list_response_pattern_literal = re.compile( - r'\((?P.*?)\) "(?P.*)" \{(?P\d+)\}') - list_response_pattern = re.compile(list_base_pattern) - listextended_response_pattern = \ - re.compile(list_base_pattern + r'\s*(?P.*)') - unseen_pattern = re.compile(r'[^\(]+\(UNSEEN (\d+)\)') - - def __init__(self, user=None, password=None): - self.__hdelimiter = None - self.quota_usage = -1 - self.criterions = [] - self.address = parameters.get_admin("IMAP_SERVER") - self.port = int(parameters.get_admin("IMAP_PORT")) - self.login(user, password) - - def _cmd(self, name, *args, **kwargs): - """IMAP command wrapper - - To simplify errors handling, this wrapper calls the - appropriate method (``uid`` or FIXME) and then check the - return code. If an error has occured, an ``ImapError`` - exception is raised. - - For specific commands commands (FETCH, ...), the result is - parsed using the IMAPclient module before being returned. - - :param name: the command's name - :return: the command's result - """ - if name in ['FETCH', 'SORT', 'STORE', 'COPY', 'SEARCH']: - try: - typ, data = self.m.uid(name, *args) - except imaplib.IMAP4.error as e: - raise ImapError(e) - if typ == "NO": - raise ImapError(data) - if name == 'FETCH': - return parse_fetch_response(data) - return data - - try: - typ, data = self.m._simple_command(name, *args) - except imaplib.IMAP4.error as e: - raise ImapError(e) - if typ == "NO": - raise ImapError(data) - if 'responses' not in kwargs: - if name not in self.m.untagged_responses: - return None - return self.m.untagged_responses.pop(name) - res = [] - for r in kwargs['responses']: - if r not in self.m.untagged_responses: - return None - res.append(self.m.untagged_responses.pop(r)) - return res - - def __find_content_in_bodystruct(self, bodystruct, mtype, stype, prefix=""): - """Retrieve the number (index) of a specific part - - This part number will generally be used inside a FETCH request - to specify a ``BODY.PEEK`` section. - - :param bodystruct: a BODYSTRUCTURE list - :param mtype: the MIME main type (like text) - :param stype: the MIME sub type (like plain) - :param prefix: the prefix that will be added to the current part number - :return: a tuple (index (None on error), encoding (string), size (int)) - """ - if type(bodystruct[0]) in [list, tuple]: - cpt = 1 - for part in bodystruct[0]: - nprefix = "%s" % cpt if prefix == "" else "%s.%d" % ( - prefix, cpt) - index, encoding, size = \ - self.__find_content_in_bodystruct( - part, - mtype, - stype, - nprefix) - if index is not None: - return (index, encoding, size) - cpt += 1 - else: - if bodystruct[0].lower() == mtype and \ - bodystruct[1].lower() == stype: - return ("1" if not len(prefix) else prefix, - bodystruct[5], int(bodystruct[6])) - return (None, None, 0) - - @property - def hdelimiter(self): - """Return the default hierachy delimiter. - - This is a simple way to retrieve the default delimiter (see - http://www.imapwiki.org/ClientImplementation/MailboxList). - - :return: a string - """ - if self.__hdelimiter is None: - data = self._cmd("LIST", "", "") - m = self.list_response_pattern.match(data[0]) - if m is None: - raise InternalError(_("Failed to retrieve hierarchy delimiter")) - self.__hdelimiter = m.group('delimiter') - return self.__hdelimiter - - def refresh(self, user, password): - """Check if current connection needs a refresh - - Is it really secure? - """ - if self.m is not None: - try: - self._cmd("NOOP") - except ImapError: - if hasattr(self, "current_mailbox"): - del self.current_mailbox - else: - return - - self.login(user, password) - - def login(self, user, passwd): - """Custom login method - - We connect to the server, issue a LOGIN command. If - successfull, we try to record a eventuel CAPABILITY untagged - response. Otherwise, we issue the command. - - :param user: username - :param passwd: password - """ - import socket - - if isinstance(user, unicode): - user = user.encode("utf-8") - if isinstance(passwd, unicode): - passwd = passwd.encode("utf-8") - try: - secured = parameters.get_admin("IMAP_SECURED") - if secured == "yes": - self.m = imaplib.IMAP4_SSL(self.address, self.port) - else: - self.m = imaplib.IMAP4(self.address, self.port) - except (socket.error, imaplib.IMAP4.error, ssl.SSLError) as error: - raise ImapError(_("Connection to IMAP server failed: %s" % error)) - - data = self._cmd("LOGIN", user, passwd) - self.m.state = "AUTH" - if "CAPABILITY" in self.m.untagged_responses: - self.capabilities = \ - self.m.untagged_responses.pop('CAPABILITY')[0].split() - else: - data = self._cmd("CAPABILITY") - self.capabilities = data[0].split() - - def logout(self): - """Logout from server.""" - try: - self._cmd("CHECK") - except ImapError: - pass - self._cmd("LOGOUT") - del self.m - self.m = None - if hasattr(self, "current_mailbox"): - del self.current_mailbox - - def parse_search_parameters(self, criterion, pattern): - """Parse search information and apply them.""" - - def or_criterion(old, c): - if old == "": - return c - return "OR (%s) (%s)" % (old, c) - - if criterion == u"both": - criterion = u"from_addr, subject" - criterions = "" - if isinstance(pattern, unicode): - pattern = pattern.encode("utf-8") - if isinstance(criterion, unicode): - criterion = criterion.encode("utf-8") - for c in criterion.split(','): - if c == "from_addr": - key = "FROM" - elif c == "subject": - key = "SUBJECT" - else: - continue - criterions = \ - or_criterion(criterions, '(%s "%s")' % (key, pattern)) - self.criterions = [criterions] - - 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 and kwargs["order"]: - sign = kwargs["order"][:1] - criterion = kwargs["order"][1:].upper() - if sign == '-': - criterion = "REVERSE %s" % criterion - else: - criterion = "REVERSE DATE" - folder = kwargs["folder"] if "folder" in kwargs else None - - # FIXME: pourquoi suis je obligé de faire un SELECT ici? un - # EXAMINE plante mais je pense que c'est du à une mauvaise - # lecture des réponses de ma part... - self.select_mailbox(folder, readonly=False) - data = self._cmd("SORT", "(%s)" % criterion, "UTF-8", "(NOT DELETED)", - *self.criterions) - self.messages = data[0].split() - self.getquota(folder) - return len(self.messages) - - def select_mailbox(self, name, readonly=True, force=False): - """Issue a SELECT/EXAMINE command to the server - - The given name is first 'imap-utf7' encoded. - - :param name: mailbox's name - :param readonly: - """ - if hasattr(self, "current_mailbox"): - if self.current_mailbox == name and not force: - return - self.current_mailbox = name - name = name.encode("imap4-utf-7") - if readonly: - self._cmd("EXAMINE", name) - else: - self._cmd("SELECT", name) - self.m.state = "SELECTED" - - def unseen_messages(self, mailbox): - """Return the number of unseen messages - - :param mailbox: the mailbox's name - :return: an integer - """ - data = self._cmd("STATUS", mailbox.encode("imap4-utf-7"), "(UNSEEN)") - m = self.unseen_pattern.match(data[-1]) - if m is None: - return 0 - return int(m.group(1)) - - def _encode_mbox_name(self, folder): - if not folder: - return "INBOX" - return folder.encode("imap4-utf-7") - - def _parse_mailbox_name(self, descr, prefix, delimiter, parts): - if not len(parts): - return False - path = "%s%s%s" % (prefix, delimiter, parts[0]) - sdescr = None - for d in descr: - if d["path"] == path: - sdescr = d - break - if sdescr is None: - sdescr = {"name": parts[0], "path": path, "sub": []} - descr += [sdescr] - if self._parse_mailbox_name(sdescr["sub"], path, delimiter, parts[1:]): - sdescr["class"] = "subfolders" - return True - - def _listmboxes_simple(self, topmailbox='INBOX', mailboxes=None, **kwargs): - # data = self._cmd("LIST", "", "*") - if not mailboxes: - mailboxes = [] - (status, data) = self.m.list() - newmboxes = [] - for mb in data: - flags, delimiter, name = self.list_response_pattern.match( - mb).groups() - name = name.strip('"').decode("imap4-utf-7") - mdm_found = False - for idx, mdm in enumerate(mailboxes): - if mdm["name"] == name: - mdm_found = True - descr = mailboxes[idx] - break - if not mdm_found: - descr = dict(name=name) - newmboxes += [descr] - - if re.search("\%s" % delimiter, name): - parts = name.split(delimiter) - if "path" not in descr: - descr["path"] = parts[0] - descr["sub"] = [] - if self._parse_mailbox_name(descr["sub"], parts[0], delimiter, - parts[1:]): - descr["class"] = "subfolders" - continue - - from operator import itemgetter - mailboxes += sorted(newmboxes, key=itemgetter("name")) - - @capability('LIST-EXTENDED', '_listmboxes_simple') - def _listmboxes(self, topmailbox, mailboxes, until_mailbox=None): - """Retrieve mailboxes list.""" - pattern = ( - "{0}{1}%".format(topmailbox.encode("imap4-utf-7"), self.hdelimiter) - if topmailbox else "%" - ) - resp = self._cmd("LIST", "", pattern, "RETURN", "(CHILDREN)") - newmboxes = [] - for mb in resp: - if not mb: - continue - if type(mb) in [str, unicode]: - flags, delimiter, name, childinfo = \ - self.listextended_response_pattern.match(mb).groups() - else: - flags, delimiter, namelen = ( - self.list_response_pattern_literal.match(mb[0]).groups() - ) - name = mb[1][0:int(namelen)] - flags = flags.split(' ') - name = name.decode("imap4-utf-7") - mdm_found = False - for idx, mdm in enumerate(mailboxes): - if mdm["name"] == name: - mdm_found = True - descr = mailboxes[idx] - break - if not mdm_found: - descr = dict(name=name) - newmboxes += [descr] - - if r'\Marked' in flags or r'\UnMarked' not in flags: - descr["send_status"] = True - if r'\HasChildren' in flags: - if r'\NonExistent' in flags: - descr["removed"] = True - descr["path"] = name - descr["sub"] = [] - if until_mailbox and until_mailbox.startswith(name): - self._listmboxes(name, descr["sub"], until_mailbox) - - from operator import itemgetter - mailboxes += sorted(newmboxes, key=itemgetter("name")) - - def getmboxes( - self, user, topmailbox='', until_mailbox=None, - unseen_messages=True): - """Returns a list of mailboxes for a particular user - - By default, only the first level of mailboxes under - ``topmailbox`` is returned. If ``until_mailbox`` is specified, - all levels needed to access this mailbox will be returned. - - :param user: a ``User`` instance - :param topmailbox: the mailbox where to start in the tree - :param until_mailbox: the deepest needed mailbox - :param unseen_messages: include unseen messages counters or not - :return: a list - """ - if topmailbox: - md_mailboxes = [] - else: - md_mailboxes = [ - {"name": "INBOX", "class": "fa fa-inbox"}, - {"name": parameters.get_user(user, "DRAFTS_FOLDER"), - "class": "fa fa-file"}, - {"name": 'Junk', "class": "fa fa-fire"}, - {"name": parameters.get_user(user, "SENT_FOLDER"), - "class": "fa fa-envelope"}, - {"name": parameters.get_user(user, "TRASH_FOLDER"), - "class": "fa fa-trash"} - ] - if until_mailbox: - name, parent = separate_mailbox(until_mailbox, self.hdelimiter) - if parent: - until_mailbox = parent - self._listmboxes(topmailbox, md_mailboxes, until_mailbox) - - if unseen_messages: - for mb in md_mailboxes: - if "send_status" not in mb: - continue - del mb["send_status"] - key = "path" if "path" in mb else "name" - if not mb.get("removed", False): - count = self.unseen_messages(mb[key]) - if count == 0: - continue - mb["unseen"] = count - return md_mailboxes - - def _add_flag(self, mbox, msgset, flag): - """Add flag(s) to a messages set - - :param mbox: the mailbox containing the messages - :param msgset: messages set (uid) - :param flag: the flag to add - """ - self.select_mailbox(mbox, False) - self._cmd("STORE", msgset, "+FLAGS", flag) - - def mark_messages_unread(self, mbox, msgset): - """Mark a set of messages as unread - - :param mbox: the mailbox containing the messages - :param msgset: messages set (uid) - """ - self.select_mailbox(mbox, False) - self._cmd("STORE", msgset, "-FLAGS", r'(\Seen)') - - def mark_messages_read(self, mbox, msgset): - """Mark a set of messages as unread - - :param mbox: the mailbox containing the messages - :param msgset: messages set (uid) - """ - self._add_flag(mbox, msgset, r'(\Seen)') - - def msg_forwarded(self, mailbox, mailid): - self._add_flag(mailbox, mailid, '($Forwarded)') - - def msg_answered(self, mailbox, mailid): - """Add the \Answered flag to this email""" - self._add_flag(mailbox, mailid, r'(\Answered)') - - def move(self, msgset, oldmailbox, newmailbox): - """Move messages between mailboxes - - """ - self.select_mailbox(oldmailbox, False) - self._cmd("COPY", msgset, newmailbox.encode("imap4-utf-7")) - self._cmd("STORE", msgset, "+FLAGS", r'(\Deleted \Seen)') - - def push_mail(self, folder, msg): - now = imaplib.Time2Internaldate(time.time()) - self.m.append(self._encode_mbox_name(folder), r'(\Seen)', now, str(msg)) - - def empty(self, mbox): - self.select_mailbox(mbox, False) - resp = self._cmd("SEARCH", "ALL") - seq = ",".join(resp[0].split()) - if seq == "": - return - self._cmd("STORE", seq, "+FLAGS", r'(\Deleted)') - self._cmd("EXPUNGE") - - def compact(self, mbox): - """Compact a specific mailbox - - Issue an EXPUNGE command for the specified mailbox. - - :param mbox: the mailbox's name - """ - self.select_mailbox(mbox, False) - self._cmd("EXPUNGE") - - def create_folder(self, name, parent=None): - if parent is not None: - name = "%s%s%s" % (parent, self.hdelimiter, name) - typ, data = self.m.create(self._encode_mbox_name(name)) - if typ == "NO": - raise WebmailInternalError(data[0]) - return True - - def rename_folder(self, oldname, newname): - typ, data = self.m.rename(self._encode_mbox_name(oldname), - self._encode_mbox_name(newname)) - if typ == "NO": - raise WebmailInternalError(data[0], ajax=True) - return True - - def delete_folder(self, name): - typ, data = self.m.delete(self._encode_mbox_name(name)) - if typ == "NO": - raise WebmailInternalError(data[0]) - return True - - def getquota(self, mailbox): - """Retrieve quota information from the server. - - We also compute the current usage. - """ - if "QUOTA" not in self.capabilities: - self.quota_limit = self.quota_current = None - return - - data = self._cmd("GETQUOTAROOT", self._encode_mbox_name(mailbox), - responses=["QUOTAROOT", "QUOTA"]) - if data is None: - self.quota_limit = self.quota_current = None - return - - quotadef = data[1][0] - m = re.search(r"\(STORAGE (\d+) (\d+)\)", quotadef) - if not m: - print "Problem while parsing quota def" - return - self.quota_limit = int(m.group(2)) - self.quota_current = int(m.group(1)) - try: - self.quota_usage = ( - int(float(self.quota_current) / float(self.quota_limit) * 100) - ) - except TypeError: - self.quota_usage = -1 - - def fetchpart(self, uid, mbox, partnum): - """Retrieve a specific message part - - Useful to fetch attachments from the server. Part headers and - the payload are returned separatly. - - :param uid: a message UID - :param mbox: the mailbox containing the message - :param partnum: the part number - :return: a 2uple (dict, string) - """ - self.select_mailbox(mbox, False) - data = self._cmd("FETCH", uid, "(BODYSTRUCTURE BODY[%s])" % partnum) - bs = BodyStructure(data[int(uid)]["BODYSTRUCTURE"]) - attdef = bs.find_attachment(partnum) - return attdef, data[int(uid)]["BODY[%s]" % partnum] - - def fetch(self, start, stop=None, mbox=None): - """Retrieve information about messages from the server - - Issue a FETCH command to retrieve information about one or - more messages (such as headers) from the server. - - :param start: index of the first message - :param stop: index of the last message (optionnal) - :param mbox: the mailbox that contains the messages - """ - self.select_mailbox(mbox, False) - if start and stop: - submessages = self.messages[start - 1:stop] - mrange = ",".join(submessages) - else: - submessages = [start] - mrange = start - query = ( - "(FLAGS BODYSTRUCTURE BODY.PEEK[HEADER.FIELDS (DATE FROM TO CC " - "SUBJECT)])" - ) - data = self._cmd("FETCH", mrange, query) - result = [] - for uid in submessages: - msg = email.message_from_string( - data[int(uid)]['BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)]'] - ) - msg['imapid'] = uid - if r'\Seen' not in data[int(uid)]['FLAGS']: - msg['style'] = 'unseen' - if r'\Answered' in data[int(uid)]['FLAGS']: - msg['answered'] = True - if r'$Forwarded' in data[int(uid)]['FLAGS']: - msg['forwarded'] = True - bstruct = BodyStructure(data[int(uid)]['BODYSTRUCTURE']) - if bstruct.has_attachments(): - msg['attachments'] = True - result += [msg] - return result - - def fetchmail(self, mbox, mailid, readonly=True, headers=None): - """Retrieve information about a specific message - - Issue a FETCH command to retrieve a message's content from the - server. In order to not overload the server, we first retrieve - the BODYSTRUCTURE of the message. Then, according to the - result and to the user's preferences, we retrieve the - appropriate content (plain, html, etc.). - - :param mbox: the mailbox containing the message - :param mailid: the message's unique id - :param readonly: - :param headers: - """ - self.select_mailbox(mbox, readonly) - if headers is None: - headers = ['DATE', 'FROM', 'TO', 'CC', 'SUBJECT'] - bcmd = "BODY.PEEK" if readonly else "BODY" - 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 - - If a separator is found in ``fullname``, this function returns the - corresponding name and parent mailbox name. - """ - if sep in fullname: - parts = fullname.split(sep) - name = parts[-1] - parent = sep.join(parts[0:len(parts) - 1]) - return name, parent - return fullname, None - - -def get_imapconnector(request): - """Simple shortcut to create a connector - - :param request: a ``Request`` object - """ - imapc = IMAPconnector(user=request.user.username, - password=request.session["password"]) - return imapc - - -if __name__ == "__main__": - import doctest - doctest.testmod() diff --git a/modoboa/extensions/webmail/lib/sendmail.py b/modoboa/extensions/webmail/lib/sendmail.py deleted file mode 100644 index 14b4eaa19..000000000 --- a/modoboa/extensions/webmail/lib/sendmail.py +++ /dev/null @@ -1,65 +0,0 @@ -import smtplib -from modoboa.lib import parameters -from modoboa.lib.email_utils import prepare_addresses -from modoboa.lib.web_utils import _render_to_string -from modoboa.lib.cryptutils import get_password -from modoboa.extensions.webmail.exceptions import WebmailInternalError -from . import get_imapconnector, clean_attachments - - -def send_mail(request, form, 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 posturl: the url to post the message form to - :return: a 2-uple (True|False, HttpResponse) - """ - if not form.is_valid(): - editormode = parameters.get_user(request.user, "EDITOR") - listing = _render_to_string( - request, "webmail/compose.html", - {"form": form, "noerrors": True, - "body": form.cleaned_data.get("body", "").strip(), - "posturl": posturl} - ) - return False, dict(status="ko", listing=listing, editor=editormode) - - msg = form.to_msg(request) - rcpts = prepare_addresses(form.cleaned_data["to"], "envelope") - for hdr in ["cc", "cci"]: - if form.cleaned_data[hdr]: - msg[hdr.capitalize()] = prepare_addresses(form.cleaned_data[hdr]) - rcpts += prepare_addresses(form.cleaned_data[hdr], "envelope") - 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 as text: - raise WebmailInternalError(str(text)) - - if parameters.get_admin("SMTP_AUTHENTICATION") == "yes": - try: - s.login(request.user.username, get_password(request)) - except smtplib.SMTPException as err: - raise WebmailInternalError(str(err)) - try: - s.sendmail(request.user.email, rcpts, msg.as_string()) - s.quit() - except smtplib.SMTPException as err: - raise WebmailInternalError(str(err)) - - sentfolder = parameters.get_user(request.user, "SENT_FOLDER") - get_imapconnector(request).push_mail(sentfolder, msg) - clean_attachments(request.session["compose_mail"]["attachments"]) - del request.session["compose_mail"] - return True, {} diff --git a/modoboa/extensions/webmail/lib/signature.py b/modoboa/extensions/webmail/lib/signature.py deleted file mode 100644 index 2b24507fa..000000000 --- a/modoboa/extensions/webmail/lib/signature.py +++ /dev/null @@ -1,31 +0,0 @@ -import re -from modoboa.lib import parameters - - -class EmailSignature(object): - - """User signature - - :param user: User object - """ - - def __init__(self, user): - self._sig = u"" - dformat = parameters.get_user(user, "EDITOR") - content = parameters.get_user(user, "SIGNATURE") - if len(content): - getattr(self, "_format_sig_%s" % dformat)(content) - - def _format_sig_plain(self, content): - self._sig = u""" ---- -%s""" % content - - def _format_sig_html(self, content): - content = re.sub("\n", "
", content) - self._sig = u"""
----
-%s""" % content - - def __unicode__(self): - return self._sig diff --git a/modoboa/extensions/webmail/lib/utils.py b/modoboa/extensions/webmail/lib/utils.py deleted file mode 100644 index 366384de0..000000000 --- a/modoboa/extensions/webmail/lib/utils.py +++ /dev/null @@ -1,46 +0,0 @@ -""" -Misc. utilities. -""" -from modoboa.lib.web_utils import NavigationParameters - - -def decode_payload(encoding, payload): - """Decode the payload according to the given encoding - - Supported encodings: base64, quoted-printable. - - :param encoding: the encoding's name - :param payload: the value to decode - :return: a string - """ - encoding = encoding.lower() - if encoding == "base64": - import base64 - return base64.b64decode(payload) - elif encoding == "quoted-printable": - import quopri - return quopri.decodestring(payload) - return payload - - -class WebmailNavigationParameters(NavigationParameters): - - """ - Specific NavigationParameters subclass for the webmail. - """ - - def __init__(self, request, defmailbox=None): - super(WebmailNavigationParameters, self).__init__( - request, 'webmail_navparams' - ) - if defmailbox is not None: - self.parameters += [('mbox', defmailbox, False)] - - def _store_page(self): - """Specific method to store the current page.""" - if self.request.GET.get("reset_page", None) or "page" not in self: - self["page"] = 1 - else: - page = self.request.GET.get("page", None) - if page is not None: - self["page"] = int(page) diff --git a/modoboa/extensions/webmail/modo_extension.py b/modoboa/extensions/webmail/modo_extension.py deleted file mode 100644 index 7ee635a15..000000000 --- a/modoboa/extensions/webmail/modo_extension.py +++ /dev/null @@ -1,25 +0,0 @@ -# coding: utf-8 -"""Declare and register the webmail extension.""" - -from django.utils.translation import ugettext_lazy - -from modoboa.core.extensions import ModoExtension, exts_pool -from modoboa.lib import parameters - - -class Webmail(ModoExtension): - name = "webmail" - label = "Webmail" - version = "1.0" - description = ugettext_lazy("Simple IMAP webmail") - needs_media = True - available_for_topredirection = True - - def load(self): - from .app_settings import ParametersForm, UserSettings - - parameters.register(ParametersForm, "Webmail") - parameters.register(UserSettings, "Webmail") - from modoboa.extensions.webmail import general_callbacks - -exts_pool.register_extension(Webmail) diff --git a/modoboa/extensions/webmail/static/webmail/css/attachments.css b/modoboa/extensions/webmail/static/webmail/css/attachments.css deleted file mode 100644 index b4c0de430..000000000 --- a/modoboa/extensions/webmail/static/webmail/css/attachments.css +++ /dev/null @@ -1,40 +0,0 @@ -#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; -} - -div.row label { - width: 150px; -} - -#id_attachment { - width: 290px; -} diff --git a/modoboa/extensions/webmail/static/webmail/css/webmail.css b/modoboa/extensions/webmail/static/webmail/css/webmail.css deleted file mode 100644 index 4e73a4381..000000000 --- a/modoboa/extensions/webmail/static/webmail/css/webmail.css +++ /dev/null @@ -1,303 +0,0 @@ -/* MEDIA */ - -@media (min-width: 768px) { - .main { - padding-left: 0; - padding-right: 0; - } -} - -@media (max-width: 1198px) { - .container-fluid { - padding: 0; - } - - .display-up-1198-webmail { - display: none; - } - .display-less-1198{ - display: inherit; - } - -} - -/* GENERAL */ - -.main { - position: absolute; - top: 51px; - left: 0; - right: 0; - bottom: 0; - overflow-x: hidden; -} - -#listing { - position: absolute; - top: 75px; - bottom: 0; - left: 0; - right: 0; - overflow-y: auto; - overflow-x: hidden; - padding-left: 15px; - padding-right: 15px; -} - -#folders { - top: 73px; - left: 15px; - right: 15px; - overflow-x: auto; - overflow-y: auto; -} - -/* .sidebar .ui-resizable-handle { */ -/* background-position: center center; */ -/* background-repeat: no-repeat; */ -/* } */ - -/* .sidebar .ui-resizable-n { */ -/* top: 0px; */ -/* } */ - -/* .sidebar .ui-resizable-s { */ -/* bottom: 0px; */ -/* } */ - -/* .sidebar .ui-resizable-e { */ -/* right: 0px; */ -/* background-image: url("../../pics/grippy.png"); */ -/* width: 8px; */ -/* } */ - -/* .sidebar .ui-resizable-w { */ -/* left: 0px; */ -/* background-image: url("../../pics/grippy.png"); */ -/* width: 8px; */ -/* } */ - -.nav-sidebar li > a { - padding-top: 7px; - padding-bottom: 7px; -} - -.nav-sidebar li li { - padding-left: 10px; -} - -#mboxes_container a[name=loadfolder] { - white-space: nowrap; - overflow: auto; - text-overflow: ellipsis; - -o-text-overflow: ellipsis; -} - -#mailcontent { - border: 0; - margin: 0; - padding: 0; - width: 100%; - height: 100%; -} - -.unseen { - font-weight: bold; -} - -#left-toolbar { - position: absolute; - margin-left: 0px; - width: 100%; - left: 0; - bottom: 0; -} - -#quotabox { - padding-top: 15px; -} - -#quotabar { - height: 5px; - margin-bottom: 0; -} - -#quotaraw { - margin-left: 10px; -} - - -/* - * Mailboxes list - */ -.email { - border-top: 1px solid #ddd; - padding-top: 10px; -} - -div.email:hover { - background-color: #f5f5f5; -} - -.openable { - cursor: pointer; -} - -.leftcol-heading { - margin-bottom: 15px; -} - -#mboxactions { - margin: 5px 0; -} - -div.clickbox { - position: absolute; - left: 10px; - top: 5px; - width: 14px; - height: 14px; - z-index: 100; -} - -div.collapsed { - background-image: url("../../pics/collapsed.png"); - background-repeat: no-repeat; -} - -div.expanded { - background-image: url("../../pics/expanded.png"); - background-repeat: no-repeat; -} - -#mboxes_container .nav .droppable div.clickbox { - z-index: 800; -} - -/* - * Compose form - */ -#mailheader { - position: relative; - padding-bottom: 5px; -} - -#mailheader label { - text-align: left; -} - -#mailheader .options { - margin-bottom: 8px; -} - -#attachments { - position: relative; - margin: 1px 0 5px 0; - padding: 5px 0; - cursor: pointer; - width: 100%; -} - -#attachments img { - float: left; -} - -list { - position: absolute; - top: 5px; - left: 180px; - font-weight: bold; -} - -#body_container { - position: static; - /*height: 600px;*/ -} - -#id_body { - width: 100%; - height: 100%; -} - -/* - * Attachments form - */ -#submit { - margin: 0 auto; -} - -#attachment_list { - margin-top: 20px; - height: 150px; - overflow-x: hidden; - overflow-y: auto; - padding: 2px; -} - -#attachment_list i, -#attachment_list label { - float: left; -} - -/* - * Drap & Drop - */ -.dragbox { - padding : 5px; - text-align : center; - z-index : 1001; -} - -/* - * Folders form - */ - -.modal-body { - max-height: 300px; - overflow-y: auto; -} - -#folders2 li { - position: relative; -} - -#folders2 div.clickbox { - left: 0; - top: 0px; -} - -#folders2 li li { - margin-left: 10px; -} - -#folders2 .active > a, -#folders2 .active > a:hover, -#folders2 .active > a:focus { - color: #fff; - background-color: #428bca; -} - -/* - * Table - */ - - - -/* - * Action buttons - */ - -#menubar { - padding-left: 15px; - padding-bottom: 20px; - z-index: 100; -} - -#menubar .btn-toolbar ul.navbar-right{ - margin-right: 3%; -} - -#menubar .btn-toolbar .navbar-form{ - margin: 0px; -} - diff --git a/modoboa/extensions/webmail/static/webmail/js/webmail.js b/modoboa/extensions/webmail/static/webmail/js/webmail.js deleted file mode 100644 index f34a39365..000000000 --- a/modoboa/extensions/webmail/static/webmail/js/webmail.js +++ /dev/null @@ -1,1285 +0,0 @@ -/** - * Creates an instance of Webmail. - * - * @constructor - * @param {Object} options - instance options - * @classdesc The javascript code that brings the webmail to life! - */ -var Webmail = function(options) { - this.initialize(options); -}; - -Webmail.prototype = { - constructor: Webmail, - - defaults: { - poller_interval: 300, /* in seconds */ - poller_url: "", - move_url: "", - submboxes_url: "", - delattachment_url: "", - ro_mboxes: ["INBOX"], - trash: "", - hdelimiter: '.', - mboxes_col_width: 200 - }, - - initialize: function(options) { - this.options = $.extend({}, this.defaults, options); - this.rtimer = null; - this.editorid = "id_body"; - - this.navobject = new History({ - deflocation: "?action=listmailbox&reset_page=true", - defcallback: $.proxy(this.listmailbox_callback, this) - }); - this.poller = new Poller(this.options.poller_url, { - interval: this.options.poller_interval * 1000, - success_cb: $.proxy(this.poller_cb, this), - args: this.get_visible_mailboxes - }); - this.record_unseen_messages(); - - $("#mboxactions").on("shown.bs.dropdown", function() { - var $menu = $("ul", this); - var offset = $menu.offset(); - - $("body").append($menu); - $menu.show(); - $menu.css("position", "absolute"); - $menu.css("top", offset.top + "px"); - $menu.css("left", offset.left + "px"); - $(this).data("original_menu", $menu); - }).on("hide.bs.dropdown", function() { - var $this = $(this); - $this.append($this.data("original_menu")); - $this.data("original_menu").removeAttr("style"); - }); - - $(".sidebar").resizable({ - start: function(event, ui) { - $('#listing iframe').css('pointer-events', 'none'); - }, - handles: "e", - minWidth: $(".sidebar").width(), - maxWidth: 400, - stop: function(event, ui) { - $('#listing iframe').css('pointer-events', 'auto'); - }, - resize: function(event, ui) { - $(".main").css({ - marginLeft: ui.size.width + "px" - }); - } - }); - - this.resize(); - - this.init_droppables(); - this.register_navcallbacks(); - this.listen(); - }, - - /** - * Register navigation callbacks. - * - * @this Webmail - */ - register_navcallbacks: function() { - this.navobject.register_callback( - "listmailbox", $.proxy(this.listmailbox_callback, this)); - this.navobject.register_callback( - "compose", $.proxy(this.compose_callback, this)); - this.navobject.register_callback( - "viewmail", $.proxy(this.viewmail_callback, this)); - - this.navobject.register_callback( - "reply", $.proxy(this.compose_callback, this)); - this.navobject.register_callback( - "replyall", $.proxy(this.compose_callback, this)); - this.navobject.register_callback( - "forward", $.proxy(this.compose_callback, this)); - }, - - listen: function() { - $(window).resize($.proxy(this.resize, this)); - - $(document).on( - "click", "a[name=compose]", $.proxy(this.compose_loader, this)); - $(document).on( - "click", "a[name=totrash]", $.proxy(this.delete_messages, this)); - $(document).on( - "click", "a[name*=mark-]", $.proxy(this.send_mark_request, this)); - $(document).on("click", "a[name=compress]", $.proxy(this.compress, this)); - $(document).on("click", "a[name=empty]", $.proxy(this.empty, this)); - $(document).on("click", "#bottom-bar a", $.proxy(this.getpage_loader, this)); - - $(document).on( - "click", "a[name=loadfolder]", $.proxy(this.listmailbox_loader, this)); - $(document).on("click", "a[name=selectfolder]", this.select_parent_mailbox); - $(document).on( - "click", "div[class*=clickbox]", $.proxy(this.mbox_state_callback, this)); - $(document).on("click", "a[name=newmbox]", $.proxy(this.new_mailbox, this)); - $(document).on("click", "a[name=editmbox]", $.proxy(this.edit_mbox, this)); - $(document).on("click", "a[name=removembox]", $.proxy(this.remove_mbox, this)); - - $(document).on("click", "div.openable", $.proxy(this.viewmail_loader, this)); - - $(document).on("click", "a[name=reply]", $.proxy(this.reply_loader, this)); - $(document).on("click", "a[name=replyall]", $.proxy(this.reply_loader, this)); - $(document).on("click", "a[name=forward]", $.proxy(this.reply_loader, this)); - $(document).on("click", "a[name=delete]", $.proxy(this.delete_message, this)); - $(document).on( - "click", "a[name=activate_links]", $.proxy(function(e) { this.display_mode(e, "1"); }, this)); - $(document).on("click", "a[name=disable_links]", $.proxy(function(e) { this.display_mode(e, "0"); }, this)); - - $(document).on("click", "a[name=sendmail]", $.proxy(this.sendmail, this)); - - $(document).on("click", "a[name=attachments]", $.proxy(function(e) { - modalbox(e, undefined, $("a[name=attachments]").attr("href"), - $.proxy(this.attachments_init, this)); - }, this)); - }, - - /** - * Setup an "infinite scroll" behaviour for the email list. - */ - setup_infinite_scroll: function(data) { - var $container = $("#listing"); - var that = this; - - if ($container.data("infinite-scroll") !== undefined) { - if (data.pages && data.pages[0] != 1) { - $container.scrollTop(10); - } - $container.infinite_scroll("reset_loaded_pages", data.pages); - $container.infinite_scroll("resume"); - return; - } - - $container.infinite_scroll({ - initial_pages: data.pages, - url: this.options.listing_url, - calculate_bottom: function($element) { - return $("#emails").height() - $element.height(); - }, - get_args: function() { - var args = $.extend({}, that.navparams); - args.scroll = true; - return args; - }, - process_results: function(data, direction) { - var $emails = $("#emails"); - - if (direction === "down") { - $emails.html($emails.html() + data.listing); - } else { - var row_id = $emails.children(".email").first().attr("id"); - - $emails.html(data.listing + $emails.html()); - - var $row = $("#" + row_id); - $("#listing").scrollTop($row.offset().top); - } - }, - end_of_list_reached: function($element) { - $element.append( - $("
").html( - gettext("No more message in this mailbox.") - ) - ); - } - }); - }, - - /** - * Disable the infinite scroll mode. - */ - disable_infinite_scroll: function() { - var $container = $("#listing"); - - if ($container.data("infinite-scroll") !== undefined) { - $container.infinite_scroll("pause"); - } - }, - - record_unseen_messages: function() { - var unseen_counters = {}; - - $("#folders").find("a.unseen").each(function() { - var $this = $(this); - unseen_counters[$this.attr("href")] = parseInt($this.attr("data-toggle")); - }); - this.unseen_counters = unseen_counters; - }, - - /** - * Global resize event callback. - */ - resize: function() { - var $window = $(window); - var current_action = this.navobject.getparam("action"); - - $("#folders").height( - $("#left-toolbar").offset().top - $("#folders").offset().top - ); - - // if ($window.width() <= 768) { - // $(".main").css("margin-left", "0"); - // } else { - // $(".main").css("margin-left", $(".sidebar").width() + "px"); - // } - if (current_action == "compose") { - this.resize_compose_body(); - } - }, - - /** - * Resize the textarea used to write plain/text messages. - */ - resize_compose_body: function() { - var $container = $("#body_container"); - - if ($container.length) { - var top = $container.offset().top; - - $("#body_container").innerHeight($(window).height() - top - 10); - } - }, - - /** - * Callback of the 'compose' action. - * - * It is also shared with other similar actions : reply, forward. - */ - resize_editor: function() { - CKEDITOR.instances[this.editorid].resize("100%", - $("#body_container").outerHeight(true)); - }, - - /** - * Simple helper to retrieve the currently selected mailbox. - * - * @this Webmail - */ - get_current_mailbox: function() { - return this.navobject.getparam("mbox", "INBOX"); - }, - - /** - * Keep track of interesting navigation parameters in order to - * restore them later. - * - * @this Webmail - */ - store_nav_params: function() { - var params = new Array("order", "pattern", "criteria"); - - this.navparams = {}; - for (var idx in params) { - this.navparams[params[idx]] = this.navobject.getparam(params[idx]); - } - }, - - /** - * Restore navigation parameters previously stored via - * store_nav_params. - * - * @this Webmail - */ - restore_nav_params: function() { - if (this.navparams === undefined) { - return; - } - var navobject = this.navobject; - $.each(this.navparams, function(key, value) { - if (value === undefined) { - return; - } - navobject.setparam(key, value); - }); - }, - - go_back_to_listing: function() { - var curmb = this.get_current_mailbox(); - this.navobject.reset() - .setparams({action: "listmailbox", mbox: curmb}); - this.restore_nav_params(); - this.navobject.update(true); - }, - - /* - * Enable or disable edit and remove actions for the currently - * selected mailbox. - */ - enable_mb_actions: function(state) { - if (state === undefined) { - state = true; - } - if (state) { - $("a[name=editmbox], a[name=removembox]").removeClass("disabled"); - } else { - $("a[name=editmbox], a[name=removembox]").addClass("disabled"); - } - }, - - /** - * Update the current displayed content. - * - * @this Webmail - * @param {Object} response - object containing the new content - */ - page_update: function(response) { - var curmb = this.get_current_mailbox(); - - if (!$('li[name="' + curmb + '"]').hasClass("active")) { - this.load_and_select_mailbox(curmb); - } - this.enable_mb_actions(); - for (var idx in this.options.ro_mboxes) { - if (curmb == this.options.ro_mboxes[idx]) { - this.enable_mb_actions(false); - break; - } - } - if (response.menu !== undefined) { - $("#menubar").html(response.menu); - $("#searchfield").searchbar({ - navobj: this.navobject, - pattern_changed: function(navobj) { - navobj.setparam("reset_page", "true"); - } - }); - } - - $("#listing").html(response.listing); - - if (this.navobject.getparam("action") == "listmailbox") { - this.setup_infinite_scroll(response); - } else { - this.disable_infinite_scroll(); - } - }, - - /** - * Set the *unseen messages* counter for a particular mailbox in - * the list. If the mailbox is currently selected, we force a - * listing update. - * - */ - set_unseen_messages: function(mailbox, value) { - if (this.poller.running_request) { - return; - } - if (this.unseen_counters[mailbox] !== undefined && - value == this.unseen_counters[mailbox]) { - return; - } - - if (this.navobject.params.action == "listmailbox") { - var curmb = this.get_current_mailbox(); - if (curmb == mailbox) { - this.navobject.setparam("reset_page", "true").update(true); - } - } - - var $link = $('a[href="' + mailbox + '"]'); - var parts = mailbox.split(this.options.hdelimiter); - var dname = " " + parts[parts.length - 1]; - var $span = $link.children("span:visible"); - - this.unseen_counters[mailbox] = value; - if (value) { - $link.html(dname + " (" + value + ")"); - $link.addClass("unseen"); - } else { - $link.html(dname); - $link.removeClass("unseen"); - } - $link.prepend($span); - }, - - /* - * Increment or decrement the *unseen messages* counter of the - * given mailbox. - */ - change_unseen_messages: function(mailbox, offset) { - if (this.unseen_counters[mailbox] === undefined) { - this.unseen_counters[mailbox] = 0; - } - this.set_unseen_messages(mailbox, this.unseen_counters[mailbox] + offset); - }, - - /* - * Returns a list containing the currently visible mailboxes (in - * the left tree). - */ - get_visible_mailboxes: function() { - var res = []; - - $("#folders").find("ul:visible").children("li.droppable").each(function() { - res.push(encodeURIComponent($(this).attr("name"))); - }); - return "mboxes=" + res.join(","); - }, - - /* - * Poller callback. - */ - poller_cb: function(data) { - for (var mb in data) { - this.set_unseen_messages(mb, parseInt(data[mb])); - } - }, - - /** - * Inject a new *clickbox* somewhere in the tree. - * - * @this Webmail - * @param {Object} $container - box container - */ - inject_clickbox: function($container) { - $container.prepend($("
", {'class' : 'clickbox collapsed'})); - }, - - /* - * Injects a single mailbox somewhere in the tree - */ - inject_mailbox: function($parent, mailbox, linkname, unseen) { - var $li = $("
  • ", { - name: mailbox, - 'class': "droppable" - }); - var $link = $("", { - name: linkname, - href: mailbox - }); - var parts = mailbox.split(this.options.hdelimiter); - var linkcontent = " "; - var displayname = linkcontent + parts[parts.length - 1]; - - $li.append($link); - $parent.append($li); - - if (unseen !== undefined) { - $link.addClass("unseen"); - $link.html(displayname + " (" + unseen + ")"); - this.unseen_counters[$link.attr("href")] = unseen; - } else { - $link.html(displayname); - } - return $li; - }, - - /** - * Inject new mailboxes under a given parent in the tree. - * - * @this Webmail - * @param {Object} $parent - an existing
  • node. - * @param {Array} mboxes - list of mailboxes to inject - */ - inject_mailboxes: function($parent, mboxes) { - var $ul = $("