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 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"), allow_empty_file=True) class ComposeMailForm(forms.Form):
    """Compose mail form."""

    from_ = forms.ChoiceField(
        label=_("From"), choices=[],
        widget=forms.Select(attrs={"class": "selectize"})
    )
    to = forms.CharField(
        label=_("To"), validators=[validate_email_list])
    cc = forms.CharField(
        label=_("Cc"), required=False, validators=[validate_email_list],
        widget=forms.TextInput(
            attrs={"placeholder": _("Enter one or more addresses."),
                   "class": "selectize-contact"})
    )
    bcc = forms.CharField(
        label=_("Bcc"), required=False, validators=[validate_email_list],
        widget=forms.TextInput(
            attrs={"placeholder": _("Enter one or more addresses."),
                   "class": "selectize-contact"})
    )

    subject = forms.CharField(
        label=_("Subject"),
        max_length=255,
        required=False
    )
    origmsgid = forms.CharField(
        label="", required=False, widget=forms.HiddenInput())
    body = forms.CharField(
        required=False, widget=CKEditorUploadingWidget(
            attrs={"class": "editor form-control"})
    )

    def __init__(self, user, *args, **kwargs):
        """Custom constructor."""
        super(ComposeMailForm, self).__init__(*args, **kwargs)
        from_addresses = [(user.email, user.email)]
        for address in user.mailbox.alias_addresses:
            try:
                validate_email(address)
                from_addresses += [(address, address)]
            except forms.ValidationError:
                pass
        additional_sender_addresses = (
            user.mailbox.senderaddress_set.values_list("address", flat=True))
        from_addresses += [
            (address, address) for address in additional_sender_addresses]
        self.fields["from_"].choices = from_addresses
        self.fields["from_"].initial = user.email
        self.field_widths = {
            "cc": "11",
            "bcc": "11",
            "subject": "11"
        }

    def clean_to(self):
        """Convert to a list."""
        to = self.cleaned_data["to"]
        return email_utils.prepare_addresses(to, "envelope")

    def clean_cc(self):
        """Convert to a list."""
        cc = self.cleaned_data["cc"]
        return email_utils.prepare_addresses(cc, "envelope")

    def clean_bcc(self):
        """Convert to a list."""
        bcc = self.cleaned_data["bcc"]
        return email_utils.prepare_addresses(bcc, "envelope")

    def _html_msg(self, sender, headers):
        """Create a multipart message.

        We attach two alternatives:
        * text/html
        * text/plain
        """
        body = self.cleaned_data["body"]
        if body:
            tbody = html2plaintext(body)
            body, images = make_body_images_inline(body)
        else:
            tbody = ""
            images = []
        msg = EmailMultiAlternatives(
            self.cleaned_data["subject"],
            tbody,
            sender, self.cleaned_data["to"],
            cc=self.cleaned_data["cc"],
            bcc=self.cleaned_data["bcc"],
            headers=headers
        )
        msg.attach_alternative(body, "text/html")
        for img in images:
            msg.attach(img)
        return msg

    def _plain_msg(self, sender, headers):
        """Create a simple text message."""
        msg = EmailMessage(
            self.cleaned_data["subject"],
            self.cleaned_data["body"],
            sender,
            self.cleaned_data["to"],
            cc=self.cleaned_data["cc"],
            bcc=self.cleaned_data["bcc"],
            headers=headers
        )
        return msg

    def _format_sender_address(self, user, address):
        """Format address before message is sent."""
        if user.first_name != "" or user.last_name != "":
            return '"{}" <{}>'.format(
                Header(user.fullname, "utf8"), address)
        return address

    def _build_msg(self, request):
        """Build message to send.

        Can be overidden by children.
        """
        headers = {
            "User-Agent": "Modoboa {}".format(
                pkg_resources.get_distribution("modoboa").version)
        }
        origmsgid = self.cleaned_data.get("origmsgid")
        if origmsgid:
            headers.update({
                "References": origmsgid,
                "In-Reply-To": origmsgid
            })
        mode = request.user.parameters.get_value("editor")
        sender = self._format_sender_address(
            request.user, self.cleaned_data["from_"])
        return getattr(self, "_{}_msg".format(mode))(sender, headers)

    def to_msg(self, request):
        """Convert form's content to an object ready to send."""
        msg = self._build_msg(request)
        if request.session["compose_mail"]["attachments"]:
            for attdef in request.session["compose_mail"]["attachments"]:
                msg.attach(create_mail_attachment(attdef))
        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.
        """
        mbox = request.GET.get("mbox", None)
        mailid = request.GET.get("mailid", None)
        msg = super(ForwardMailForm, self)._build_msg(request)
        origmsg = ImapEmail(request, "%s:%s" % (mbox, mailid))
        if origmsg.attachments:
            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 def percent_encode(string, safe, encoding):
def is_token_char(ch):
def is_token(candidate):
def is_ascii(text):
def fits_inside_codec(text, codec):
def is_lws_safe(text):
def normalize_ws(text):
def qd_quote(text):
def build_header(
    filename, disposition='attachment', filename_compat=None
): 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) or bytes
    :return: the new random name
    """
    from tempfile import NamedTemporaryFile

    try:
        fp = NamedTemporaryFile(dir=get_storage_path(""), delete=False)
    except Exception as e:
        raise InternalError(str(e))
    if isinstance(f, (six.binary_type, six.text_type)):
        fp.write(smart_bytes(f))
    else:
        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 = get_storage_path(att["tmpname"])
        try:
            os.remove(fullpath)
        except OSError:
            pass 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.
    """
    randid = str(uuid.uuid4()).replace("-", "")
    request.session["compose_mail"] = {"id": randid, "attachments": []}
    return randid 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(
            param_tools.get_global_parameter("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 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, *args, **kwargs):
        super(ImapEmail, self).__init__(*args, **kwargs)
        self.request = request
        self.imapc = get_imapconnector(request)
        self.mbox, self.mailid = self.mailid.split(":")

    def _insert_contact_links(self, addresses):
        """Insert 'add to address book' links."""
        result = []
        title = _("Add to contacts")
        url = reverse("api:contact-list")
        link_tpl = (
            u" <a class='addcontact' href='{}' title='{}'>"
            u"<span class='fa fa-vcard'></span></a>"
        )
        for address in addresses:
            address += link_tpl.format(url, title)
            result.append(address)
        return result

    def fetch_headers(self, raw_addresses=False):
        """Fetch message headers from server."""
        msg = self.imapc.fetchmail(
            self.mbox, self.mailid, readonly=False,
            what=" ".join(self.headers_as_list)
        )
        headers = msg["BODY[HEADER.FIELDS ({})]".format(self.headers_as_text)]
        self.fetch_body_structure(msg)
        msg = email.message_from_string(headers)
        contacts_plugin_installed = exts_pool.get_extension("modoboa_contacts")
        headers_with_address = ("From", "To", "Cc", "Reply-To")
        for hdr in self.headernames:
            label = hdr[0]
            hdrvalue = self.get_header(msg, label, raw=raw_addresses)
            if not hdrvalue:
                continue
            safe = False
            if hdr[1]:
                if label in headers_with_address:
                    if contacts_plugin_installed and not raw_addresses:
                        hdrvalue = self._insert_contact_links(hdrvalue)
                    hdrvalue = ", ".join(hdrvalue)
                    safe = True
                self.headers += [
                    {"name": label, "value": hdrvalue, "safe": safe}]
            label = re.sub("-", "_", label)
            setattr(self, label, hdrvalue)

    def get_header(self, msg, hdrname, **kwargs):
        """Look for a particular header.

        We also try to decode the default value.
        """
        hdrvalue = super().get_header(msg, hdrname)
        if hdrname in ["From", "Reply-To"]:
            # Store a raw copy for further use
            setattr(self, "original_{}".format(hdrname.replace("-", "")),
                    hdrvalue)
        if not hdrvalue:
            return ""
        try:
            key = re.sub("-", "_", hdrname).lower()
            hdrvalue = getattr(imapheader, "parse_%s" % key)(
                hdrvalue, **kwargs)
        except AttributeError:
            pass
        return hdrvalue

    def fetch_body_structure(self, msg=None):
        """Fetch BODYSTRUCTURE for email."""
        if msg is None:
            msg = self.imapc.fetchmail(
                self.mbox, self.mailid, readonly=False
            )
        self.bs = BodyStructure(msg["BODYSTRUCTURE"])
        self._find_attachments()
        if self.dformat not in ["plain", "html"]:
            self.dformat = self.request.user.parameters.get_value(
                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)

    @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:
            self.fetch_body_structure()
            bodyc = u""
            parts = self.bs.contents.get(self.mformat, [])
            for part in parts:
                pnum = part["pnum"]
                data = self.imapc._cmd(
                    "FETCH", self.mailid, "(BODY.PEEK[%s])" % pnum
                )
                if not data or not int(self.mailid) in data:
                    continue
                content = decode_payload(
                    part["encoding"], data[int(self.mailid)]["BODY[%s]" % pnum]
                )
                if not isinstance(content, six.text_type):
                    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()
            bodyc = getattr(self, "_post_process_%s" % self.mformat)(bodyc)
            self._body = getattr(self, "viewmail_%s" % self.mformat)(
                bodyc, links=self.links
            )
        return self._body

    @body.setter
    def body(self, value):
        self._body = value

    @property
    def source(self):
        """Retrieve email source."""
        return self.imapc.fetchmail(
            self.mbox, self.mailid, what="source")['BODY[]']

    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_{}".format(att["pnum"])
            if "params" in att and att["params"] != "NIL":
                for pos, value in enumerate(att["params"]):
                    if not value.startswith("name"):
                        continue
                    attname = (
                        u2u_decode.u2u_decode(att["params"][pos + 1])
                        .strip("\r\t\n")
                    )
                    break
            elif "disposition" in att and len(att["disposition"]) > 1:
                for pos, value in enumerate(att["disposition"][1]):
                    if not value.startswith("filename"):
                        continue
                    attname = u2u_decode.u2u_decode(
                        att["disposition"][1][pos + 1]
                    ).strip("\r\t\n")
                    break
            self.attachments[att["pnum"]] = smart_text(attname)

    def _fetch_inlines(self):
        """Store inline images on filesystem to display them."""
        for cid, params in list(self.bs.inlines.items()):
            if re.search(r"\.\.", cid):
                continue
            fname = "{}_{}".format(self.mailid, cid)
            path = get_storage_path(fname)
            params["fname"] = os.path.join(
                settings.MEDIA_URL,
                os.path.basename(get_storage_path("")),
                fname
            )
            if default_storage.exists(path):
                continue
            pdef, content = self.imapc.fetchpart(
                self.mailid, self.mbox, params["pnum"]
            )
            default_storage.save(
                path, ContentFile(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 fetch_attachment(self, pnum):
        """Fetch an attachment from the IMAP server."""
        return self.imapc.fetchpart(self.mailid, self.mbox, pnum) 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().__init__(*args, **kwargs)

        self.textheader = u"%s %s" % (self.From, _("wrote:"))
        if self.dformat == "html":
            self.textheader = u"<p>{}</p>".format(self.textheader)
        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.original_From
        else:
            self.form.fields["to"].initial = self.original_ReplyTo
        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 = u"Fwd: %s" % self.subject

    def __getfunc(self, name):
        return getattr(self, "%s_%s" % (name, self.dformat))

    def _header(self):
        self.textheader = u"{}\n".format(self.__getfunc("_header_begin")())
        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 u"----- %s -----" % _("Original message")

    def _header_begin_html(self):
        return u"----- %s -----" % _("Original message")

    def _header_line_plain(self, key, value):
        return u"%s: %s\n" % (key, value)

    def _header_line_html(self, key, value):
        return u"<p>%s: %s</p>" % (key, conditional_escape(value))

    def _header_end_plain(self):
        return u"\n"

    def _header_end_html(self):
        return u"" class IMAPconnector(object):

    """The IMAPv4 connector."""

    namespaces_pattern = re.compile(r'(\(\(.+?\)\)|NIL)')
    namespace_pattern = re.compile(
        r'\("(?P<prefix>.*?)" "(?P<delimiter>.+?)"\)')
    list_base_pattern = (
        r'\((?P<flags>.*?)\) "(?P<delimiter>.*)" "?(?P<name>[^"]*)"?'
    )
    list_response_pattern_literal = re.compile(
        r'\((?P<flags>.*?)\) "(?P<delimiter>.*)" \{(?P<namelen>\d+)\}')
    list_response_pattern = re.compile(list_base_pattern)
    listextended_response_pattern = \
        re.compile(list_base_pattern + r'\s*(?P<childinfo>.*)')
    unseen_pattern = re.compile(r'[^\(]+\(UNSEEN (\d+)\)')

    def __init__(self, user=None, password=None):
        self.__hdelimiter = None
        self.__ns_prefixes = {}
        self.quota_usage = -1
        self.criterions = []
        self.conf = dict(param_tools.get_global_parameters("modoboa_webmail"))
        self.address = self.conf["imap_server"]
        self.port = self.conf["imap_port"]
        self.login(user, password)
        self.load_namespaces()

    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 FetchResponseParser().parse(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

    @property
    def hdelimiter(self):
        """Return the default hierachy delimiter.

        :return: a string
        """
        if self.__hdelimiter is None:
            raise InternalError(
                _("Failed to retrieve hierarchy 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
        """
        try:
            if self.conf["imap_secured"]:
                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))

        passwd = self.m._quote(passwd)
        data = self._cmd("LOGIN", smart_bytes(user), smart_bytes(passwd))
        self.m.state = "AUTH"
        if "CAPABILITY" in self.m.untagged_responses:
            self.capabilities = (
                self.m.untagged_responses.pop('CAPABILITY')[0]
                .decode().split())
        else:
            data = self._cmd("CAPABILITY")
            self.capabilities = data[0].decode().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 load_namespaces(self):
        """Load available namespaces."""
        data = self._cmd("NAMESPACE")
        nslist = self.namespaces_pattern.findall(data[0].decode())
        for pos, item in enumerate(["personal", "others", "public"]):
            if nslist[pos] == "NIL":
                continue
            ns = nslist[pos][1:-1]
            for m in self.namespace_pattern.finditer(ns):
                if self.__hdelimiter is None:
                    self.__hdelimiter = m.group("delimiter")
                if item not in self.__ns_prefixes:
                    self.__ns_prefixes[item] = []
                self.__ns_prefixes[item].append(m.group("prefix"))

    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 = ""
        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))
        if six.PY3:
            criterions = bytearray(criterions, "utf-8")
        elif isinstance(criterions, six.text_type):
            criterions = criterions.encode("utf-8")
        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)
        cmdname = "SORT" if six.PY3 else b"SORT"
        data = self._cmd(
            cmdname,
            bytearray("(%s)" % criterion, "utf-8"),
            b"UTF-8", b"(NOT DELETED)", *self.criterions)
        self.messages = data[0].decode().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 = self._encode_mbox_name(name)
        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", self._encode_mbox_name(mailbox), "(UNSEEN)")
        m = self.unseen_pattern.match(data[-1].decode())
        if m is None:
            return 0
        return int(m.group(1))

    def _encode_mbox_name(self, folder):
        """Encode folder name (str) to imap4-utf-7 and quote it."""
        if not folder:
            return "INBOX"
        return b'"' + folder.encode("imap4-utf-7") + b'"'

    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.decode()).groups()
            name = bytearray(name.strip('"'), "utf-8").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 = {"name": name}
                newmboxes += [descr]

            if re.search(r"\%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").decode(), self.hdelimiter)
            if topmailbox else "%"
        )
        resp = self._cmd(
            "LIST", '""', pattern, "RETURN", "(CHILDREN STATUS (MESSAGES))")
        newmboxes = []
        for mb in resp:
            if not mb:
                continue
            if type(mb) in [list, tuple]:
                flags, delimiter, namelen = (
                    self.list_response_pattern_literal.match(
                        mb[0].decode()).groups()
                )
                name = mb[1][0:int(namelen)]
            else:
                flags, delimiter, name, childinfo = (
                    self.listextended_response_pattern.match(
                        mb.decode()).groups())
            flags = flags.split(" ")
            name = bytearray(name, "utf-8")
            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 = {"name": name}
                newmboxes += [descr]

            if '\\Marked' in flags or '\\UnMarked' not in flags:
                descr["send_status"] = True
            if r'\NonExistent' in flags:
                descr["removed"] = True
            if '\\HasChildren' in flags:
                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",
                 "label": _("Inbox")},
                {"name": user.parameters.get_value("drafts_folder"),
                 "class": "fa fa-file", "label": _("Drafts")},
                {"name": user.parameters.get_value("junk_folder"),
                 "class": "fa fa-fire", "label": _("Junk")},
                {"name": user.parameters.get_value("sent_folder"),
                 "class": "fa fa-envelope", "label": _("Sent")},
                {"name": user.parameters.get_value("trash_folder"),
                 "class": "fa fa-trash", "label": _("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 mb.get("removed", False):
                    continue
                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 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 _remove_flag(self, mbox, msgset, flag):
        """Remove flag from a message set.

        :param mbox: the mailbox containing the messages
        :param msgset: messages set (uid)
        :param flag: the flag to remove
        """
        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._remove_flag(mbox, msgset, 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 mark_messages_flagged(self, mbox, msgset):
        """Mark a set of messages as flagged.

        :param mbox: the mailbox containing the messages
        :param msgset: messages set (uid)
        """
        self._add_flag(mbox, msgset, r'(\Flagged)')

    def mark_messages_unflagged(self, mbox, msgset):
        """Mark a set of messages as unflagged.

        :param mbox: the mailbox containing the messages
        :param msgset: messages set (uid)
        """
        self._remove_flag(mbox, msgset, r'(\Flagged)')

    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, self._encode_mbox_name(newmailbox))
        self._cmd("STORE", msgset, "+FLAGS", r'(\Deleted \Seen)')

    def push_mail(self, folder, msg):
        now = imaplib.Time2Internaldate(time.time())
        msg = bytes(msg) if six.PY3 else str(msg)
        return self.m.append(
            self._encode_mbox_name(folder), r'(\Seen)', now, msg)

    def empty(self, mbox):
        self.select_mailbox(mbox, False)
        resp = self._cmd("SEARCH", "ALL")
        seq = b",".join(resp[0].split())
        if seq == b"":
            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
        try:
            data = self._cmd("GETQUOTAROOT", self._encode_mbox_name(mailbox),
                             responses=["QUOTAROOT", "QUOTA"])
        except ImapError:
            data = None
        finally:
            if data is None:
                self.quota_limit = self.quota_current = None
                return

        quotadef = data[1][0].decode()
        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
        headers = "DATE FROM TO CC SUBJECT"
        query = (
            "(FLAGS BODYSTRUCTURE RFC822.SIZE BODY.PEEK[HEADER.FIELDS ({})])"
            .format(headers)
        )
        data = self._cmd("FETCH", mrange, query)
        result = []
        for uid in submessages:
            msg_data = data[int(uid)]
            msg = email.message_from_string(
                msg_data["BODY[HEADER.FIELDS ({})]".format(headers)]
            )
            msg["imapid"] = uid
            msg["size"] = msg_data["RFC822.SIZE"]
            if r"\Seen" not in msg_data["FLAGS"]:
                msg["style"] = "unseen"
            if r"\Answered" in msg_data["FLAGS"]:
                msg["answered"] = True
            if r"$Forwarded" in msg_data["FLAGS"]:
                msg["forwarded"] = True
            if r"\Flagged" in msg_data["FLAGS"]:
                msg["flagged"] = True
            bstruct = BodyStructure(msg_data["BODYSTRUCTURE"])
            if bstruct.has_attachments():
                msg["attachments"] = True
            result += [msg]
        return result

    def fetchmail(self, mbox, mailid, readonly=True, what="bodystructure"):
        """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 what == "bodystructure":
            to_fetch = "(BODYSTRUCTURE)"
        elif what == "source":
            to_fetch = "(BODY[])"
        else:
            bcmd = "BODY.PEEK" if readonly else "BODY"
            to_fetch = "(BODYSTRUCTURE {}[HEADER.FIELDS ({})])".format(
                bcmd, what)
        data = self._cmd("FETCH", mailid, to_fetch)
        return data[int(mailid)] 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 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 class EmailSignature(object):
    """User signature.

    :param user: User object
    """

    def __init__(self, user):
        self._sig = u""
        dformat = user.parameters.get_value("editor")
        content = user.parameters.get_value("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 = "---<br>{}".format(content)
        self._sig = content
        return

    def __str__(self):
        return self._sig 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) 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 = request.user.parameters.get_value("editor")
        listing = render_to_string(
            "modoboa_webmail/compose.html",
            {"form": form, "noerrors": True,
             "body": form.cleaned_data.get("body", "").strip(),
             "posturl": posturl},
            request
        )
        return False, {"status": "ko", "listing": listing, "editor": editormode}

    msg = form.to_msg(request)
    conf = dict(param_tools.get_global_parameters("modoboa_webmail"))
    options = {
        "host": conf["smtp_server"], "port": conf["smtp_port"]
    }
    if conf["smtp_secured_mode"] == "ssl":
        options.update({"use_ssl": True})
    elif conf["smtp_secured_mode"] == "starttls":
        options.update({"use_tls": True})
    if conf["smtp_authentication"]:
        options.update({
            "username": request.user.username,
            "password": get_password(request)
        })
    try:
        with mail.get_connection(**options) as connection:
            msg.connection = connection
            msg.send()
    except smtplib.SMTPResponseException as inst:
        return False, {"status": "ko", "error": inst.smtp_error}
    except smtplib.SMTPRecipientsRefused as inst:
        error = ", ".join(["{}: {}".format(rcpt, error)
                           for rcpt, error in inst.recipients.items()])
        return False, {"status": "ko", "error": error}

    # Copy message to sent folder
    sentfolder = request.user.parameters.get_value("sent_folder")
    get_imapconnector(request).push_mail(sentfolder, msg.message())
    clean_attachments(request.session["compose_mail"]["attachments"])
    del request.session["compose_mail"]

    return True, {} def viewmail_menu(selection, folder, user, mail_id=None):
def compose_menu(selection, backurl, user, **kwargs):
def listmailbox_menu(selection, folder, user, **kwargs):
def print_mailboxes(
        tree, selected=None, withunseen=False, selectonly=False,
        hdelimiter='.'):
def mboxes_menu():
def parse_imap_header(value, header):
def attachment_url(mbox, mail_id, fname, key):
import base64
import os
from django.conf import settings
from django.urls import reverse
from django.http import HttpResponse
from django.shortcuts import render
from django.template.loader import render_to_string
from django.utils.translation import ugettext as _, ungettext
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.gzip import gzip_page
from django.contrib.auth.decorators import login_required
from modoboa.admin.lib import needs_mailbox
from modoboa.core.extensions import exts_pool
from modoboa.lib.exceptions import ModoboaException, BadRequest
from modoboa.lib.paginator import Paginator
from modoboa.lib.web_utils import (
    ajax_response, render_to_json_response
)
from modoboa.parameters import tools as param_tools
from .exceptions import UnknownAction
from .forms import (
    FolderForm, AttachmentForm, ComposeMailForm, ForwardMailForm
)
from .lib import (
    decode_payload, AttachmentUploadHandler,
    save_attachment, EmailSignature,
    clean_attachments, set_compose_session, send_mail,
    ImapEmail, WebmailNavigationParameters, ReplyModifier, ForwardModifier,
    get_imapconnector, IMAPconnector, separate_mailbox, rfc6266
)
from .templatetags import webmail_tags

    :param request: a ``Request`` object
    :param imapc: an ``IMAPconnector` object
    :return: a string
    """
    curmbox = WebmailNavigationParameters(request).get("mbox", "INBOX")
    return render_to_string("modoboa_webmail/folders.html", {
        "selected": curmbox,
        "mboxes": imapc.getmboxes(request.user),
        "withunseen": True
    }, request)


def listmailbox(request, defmailbox="INBOX", update_session=True):
    """Mailbox content listing.

    Return a list of messages contained in the specified mailbox. The
    number of elements returned depends on the ``MESSAGES_PER_PAGE``
    parameter. (user preferences)

    :param request: a ``Request`` object
    :param defmailbox: the default mailbox (when not present inside
                       request arguments)
    :return: a dictionnary
    """
    navparams = WebmailNavigationParameters(request, defmailbox)
    previous_page_id = int(navparams["page"]) if "page" in navparams else None
    if update_session:
        navparams.store()
    mbox = navparams.get('mbox')
    page_id = int(navparams["page"])
    mbc = get_imapconnector(request)
    mbc.parse_search_parameters(
        navparams.get("criteria"), navparams.get("pattern"))
    sort_order = navparams.get("order")
    paginator = Paginator(
        mbc.messages_count(folder=mbox, order=sort_order),
        request.user.parameters.get_value("messages_per_page")
    )
    page = paginator.getpage(page_id)
    content = ""
    if page is not None:
        email_list = mbc.fetch(page.id_start, page.id_stop, mbox)
        content = render_to_string(
            "modoboa_webmail/email_list.html", {
                "email_list": email_list,
                "page": page_id,
                "with_top_div": request.GET.get("scroll", "false") == "false"
            }, request
        )
        length = len(content)
    else:
        if page_id == 1:
            content = u"<div class='alert alert-info'>{0}</div>".format(
                _("Empty mailbox")
            )
        length = 0
        if previous_page_id is not None:
            navparams["page"] = previous_page_id
    return {
        "listing": content, "length": length, "pages": [page_id],
        "menuargs": {"sort_order": sort_order}
    }


def render_compose(request, form, posturl, email=None, **kwargs):
    """Render the compose form."""
    resp = {}
    if email is None:
        body = ""
        textheader = ""
    else:
        body = email.body
        textheader = email.textheader
    if kwargs.get("insert_signature"):
        signature = EmailSignature(request.user)
        body = "{}{}".format(body, signature)
    randid = None
    condition = (
        "id" not in request.GET or
        "compose_mail" not in request.session or
        request.session["compose_mail"]["id"] != request.GET["id"]
    )
    if condition:
        randid = set_compose_session(request)
        if kwargs.get("load_email_attachments"):
            for pnum, fname in email.attachments.items():
                partdef, payload = email.fetch_attachment(pnum)
                request.session["compose_mail"]["attachments"].append({
                    "fname": fname,
                    "content-type": partdef["Content-Type"],
                    "size": partdef["size"],
                    "tmpname": save_attachment(base64.b64decode(payload))
                })
            request.session.modified = True

    attachment_list = request.session["compose_mail"]["attachments"]
    if attachment_list:
        resp["menuargs"] = {"attachment_counter": len(attachment_list)}

    if textheader:
        body = "{}\n{}".format(textheader, body)
    form.fields["body"].initial = body
    content = render_to_string("modoboa_webmail/compose.html", {
        "form": form, "posturl": posturl
    }, request)

    resp.update({
        "listing": content,
        "editor": request.user.parameters.get_value("editor")
    })
    if randid is not None:
        resp["id"] = randid
    return resp


def compose(request):
    """Compose email."""
    url = "?action=compose"
    if request.method == "POST":

        form = ComposeMailForm(request.user, request.POST)