mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-18 05:51:26 +02:00
Remove webmail extension.
This commit is contained in:
@@ -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']
|
||||
@@ -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)
|
||||
@@ -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"))
|
||||
@@ -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
|
||||
@@ -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',
|
||||
]
|
||||
@@ -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
|
||||
@@ -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: <Service.client10@maaf.fr>\r\nTo: <TONIO@NGYN.ORG>\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?= <communication@communication.malakoffmederic.com>\r\nTo: Antoine Nguyen <tonio@ngyn.org>\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 <nguyen.antoine@wanadoo.fr>\r\nTo: Antoine Nguyen <tonio@koalabs.org>\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@01CC6CAA.4FADC490>" "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@01CC6CAA.4FADC490>" "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)
|
||||
@@ -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("</?pre>", "", self.body)
|
||||
|
||||
def _modify_html(self):
|
||||
if self.dformat == "html" and self.mformat != self.dformat:
|
||||
self.body = re.sub("</?pre>", "", self.body)
|
||||
self.body = re.sub("\n", "<br>", 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 "<p>%s: %s</p>" % (key, value)
|
||||
|
||||
def _header_end_plain(self):
|
||||
return "\n"
|
||||
|
||||
def _header_end_html(self):
|
||||
return ""
|
||||
@@ -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)
|
||||
@@ -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<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.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()
|
||||
@@ -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, {}
|
||||
@@ -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", "<br/>", content)
|
||||
self._sig = u"""<br/>
|
||||
---<br/>
|
||||
%s""" % content
|
||||
|
||||
def __unicode__(self):
|
||||
return self._sig
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,29 +0,0 @@
|
||||
{% extends "common/generic_modal_form.html" %}{% load url from future %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% block modalbody %}
|
||||
{{ block.super }}
|
||||
|
||||
<div class="col-sm-12 text-center">
|
||||
<input type="button" class="btn btn-primary" id="submit" name="submit" value="{% trans 'Attach' %}" />
|
||||
</div>
|
||||
|
||||
<div id="upload_status" style="display:none">
|
||||
<img src="{% static 'css/spinner.gif' %}" /> {% trans "Uploading.." %}
|
||||
</div>
|
||||
<iframe id="upload_target" name="upload_target" src="#"
|
||||
style="width:0;height:0;border:0px solid #fff;"></iframe>
|
||||
<div class="well" id="attachment_list">
|
||||
{% for att in attachments %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<a href="{% url 'webmail:attachment_delete' %}?name={{ att.tmpname }}"
|
||||
name="delattachment">
|
||||
<span class="fa fa-remove"></span>
|
||||
</a>
|
||||
<label>{{ att.fname }}</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,43 +0,0 @@
|
||||
{% load i18n form_tags %}{% load url from future %}
|
||||
|
||||
<form class="form-horizontal" method="POST" action="{{ posturl }}" id="composemail">
|
||||
{% csrf_token %}
|
||||
|
||||
<div id="compose-container">
|
||||
<div id="mailheader">
|
||||
<div class="form-group{% if form.errors.to %} has-error{% endif %}">
|
||||
<label class="col-sm-1 control-label" for="id_to">{% trans "To" %}</label>
|
||||
<div class="col-sm-8">
|
||||
<input id="id_to" name="to" class="form-control" value="{% if form.to.value %}{{ form.to.value }}{% endif %}"{% if form.to.errors %} placeholder="{% for error in form.to.errors %}{{ error }}{% endfor %}"{% endif %}>
|
||||
</div>
|
||||
{% if not form.cc.value %}
|
||||
<div class="col-sm-2">
|
||||
<button id="add_cc" class="btn btn-default btn-xs"><span class="fa fa-plus"></span> {% trans "Cc" %}</button>
|
||||
<button id="add_cci" class="btn btn-default btn-xs"><span class="fa fa-plus"> {% trans "Cci" %}</span></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if form.cc.value %}
|
||||
{% render_field form.cc label_width="col-sm-1" %}
|
||||
{% render_field form.cci label_width="col-sm-1" %}
|
||||
{% else %}
|
||||
{% render_field form.cc label_width="col-sm-1" hidden=True %}
|
||||
{% render_field form.cci label_width="col-sm-1" hidden=True %}
|
||||
{% endif %}
|
||||
{% render_field form.subject label_width="col-sm-1" %}
|
||||
|
||||
</div>
|
||||
|
||||
<div id="body_container">
|
||||
{% if body %}
|
||||
<textarea id="id_body" name="body" class="form-control editor">{% if bodyheader %}{{ bodyheader }}{% endif %}
|
||||
{{ body }}
|
||||
</textarea>
|
||||
{% else %}
|
||||
<textarea id="id_body" name="body" class="form-control editor"></textarea>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
{% extends "common/buttons_list.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block extra_buttons %}
|
||||
<div class="nav navbar-nav action-buttons btn-group">
|
||||
<a name="attachments" href="{% url 'webmail:attachment_list' %}" class="btn btn-default">
|
||||
<span class="fa fa-paperclip"></span>
|
||||
{% trans "Attachments" %}
|
||||
<span id="attachment_counter">{% if attachment_counter %}({{ attachment_counter }}){% endif %}</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,25 +0,0 @@
|
||||
{% load i18n %}
|
||||
{% load webmail_tags %}
|
||||
{% load static %}
|
||||
|
||||
{% if with_top_div %}<div id="emails">{% endif %}
|
||||
{% for email in email_list %}
|
||||
<div id="{{ email.imapid }}" class="row email{% if email.style %} {{ email.style }}{% endif %}" data-page="{{ page }}">
|
||||
<div class="hidden-xs col-sm-1">
|
||||
<img name="drag" src="{% static 'pics/grippy.png' %}" class="draggable" />
|
||||
|
||||
<input type="checkbox" />
|
||||
</div>
|
||||
<div class="col-xs-9 col-sm-9 openable">
|
||||
{{ email.subject|parse_imap_header:"subject" }}
|
||||
<p class="text-muted">
|
||||
{{ email.from|parse_imap_header:"from" }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-xs-3 col-sm-2 text-right">
|
||||
{{ email.date|parse_imap_header:"date" }}
|
||||
<p>{% if email.answered %}<span class="fa fa-mail-reply"></span>{% endif %}{% if email.forwarded %} <span class="fa fa-mail-forward"></span>{% endif %}{% if email.attachments %} <span class="fa fa-paperclip"></span>{% endif %}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if with_top_div %}</div>{% endif %}
|
||||
@@ -1,10 +0,0 @@
|
||||
{% extends "common/generic_modal_form.html" %}
|
||||
{% load i18n webmail_tags %}
|
||||
{% block extrafields %}
|
||||
<div id="folders2" class="well">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li class="nav-header">{% trans "Parent mailbox" %}</li>
|
||||
{% print_mailboxes mboxes selected withunseen selectonly hdelimiter %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,4 +0,0 @@
|
||||
{% load i18n webmail_tags %}
|
||||
<ul class="nav nav-sidebar">
|
||||
{% print_mailboxes mboxes selected withunseen %}
|
||||
</ul>
|
||||
@@ -1,11 +0,0 @@
|
||||
{% extends "common/mailheaders.html" %}{% load i18n %}{% load url from future %}
|
||||
{% block extraheaders %}{% if attachments %}
|
||||
<div class="row">
|
||||
<div class="header">{% trans "Attachments" %}</div>
|
||||
<div class="value">{% url "webmail:attachment_get" as url %}
|
||||
{% for key, fname in attachments.items %}
|
||||
<a href="{{ url }}?mbox={{ folder }}&mailid={{ mail_id }}&fname={{ fname }}&partnumber={{ key }}">{{ fname }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}{% endblock %}
|
||||
@@ -1,75 +0,0 @@
|
||||
{% extends "twocols.html" %}{% load i18n lib_tags webmail_tags %}{% load url from future %}
|
||||
{% load static %}
|
||||
{% block pagetitle %}Webmail{% endblock %}
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'jquery-ui/themes/base/all.css' %}" />
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'webmail/css/webmail.css' %}" />
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script type="text/javascript" src="{% static 'jquery-ui/jquery-ui.min.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/ckeditor/ckeditor.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/history.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/jquery.htmltable.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/searchbar.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/jquery.infinitescroll.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'webmail/js/webmail.js' %}"></script>
|
||||
<script type="text/javascript">
|
||||
var webmail = undefined;
|
||||
|
||||
$(document).ready(function() {
|
||||
webmail = new Webmail({
|
||||
poller_interval: {{ refreshrate }},
|
||||
poller_url: "{% url 'webmail:unseen_messages_check' %}",
|
||||
listing_url: "{% url 'webmail:index' %}?action=listmailbox",
|
||||
move_url: "{% url 'webmail:mail_move' %}",
|
||||
submboxes_url: "{% url 'webmail:submailboxes_get' %}",
|
||||
delattachment_url: "{% url 'webmail:attachment_delete' %}",
|
||||
deflocation: "{{ deflocation }}",
|
||||
defcallback: "{{ defcallback }}",
|
||||
ro_mboxes: {% tolist ro_mboxes %},
|
||||
trash: "{{ trash }}",
|
||||
hdelimiter: '{{ hdelimiter }}',
|
||||
mboxes_col_width: {{ mboxes_col_width }}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block leftcol %}
|
||||
<div class="leftcol-heading text-center">
|
||||
<a name="compose" class="btn btn-success btn-lg" href="compose">
|
||||
<span class="fa fa-edit"></span>
|
||||
{% trans "Compose" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="folders">
|
||||
{{ mboxes }}
|
||||
</div>
|
||||
|
||||
<div id="left-toolbar" class="row">
|
||||
<div class="col-sm-2">
|
||||
<div id="mboxactions" class="btn-group dropup">
|
||||
<button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown">
|
||||
<span class="fa fa-cog"></span>
|
||||
</button>
|
||||
{% mboxes_menu %}
|
||||
</div>
|
||||
</div>
|
||||
{% if quota != -1 %}
|
||||
<div id="quotabox" class="col-sm-10">
|
||||
<div id="quotabar" class="progress">
|
||||
<div class="progress-bar {% progress_color quota %}" role="progressbar" style="width: {{ quota }}%" aria-valuenow="{{ quota }}" aria-valuemin="0" aria-valuemax="100">{{ quota }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block apparea %}
|
||||
<div id="menubar" role="navbar"></div>
|
||||
<div id="listing"></div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{% extends "common/buttons_list.html" %}{% load i18n %}
|
||||
|
||||
{% block before_menubar %}
|
||||
<div class="col-xs-5">
|
||||
{% include "common/email_searchbar.html" with extraopts=extraopts %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block menubar_classes %}col-xs-7{% endblock %}
|
||||
@@ -1,15 +0,0 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head profile="http://www.w3.org/2005/10/profile">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<script type="text/javascript">
|
||||
{% if status == "ok" %}
|
||||
parent.webmail.upload_success("{{ fname }}", "{{ tmpname }}");
|
||||
{% else %}
|
||||
parent.webmail.upload_error("{{ error }}");
|
||||
{% endif %}
|
||||
</script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
@@ -1,221 +0,0 @@
|
||||
# coding: utf-8
|
||||
from django import template
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.conf import settings
|
||||
|
||||
from modoboa.extensions.webmail.lib import (
|
||||
imapheader, separate_mailbox
|
||||
)
|
||||
from modoboa.lib import parameters
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def viewmail_menu(selection, folder, user, mail_id=None):
|
||||
"""Menu of the viewmail location."""
|
||||
entries = [
|
||||
{"name": "back",
|
||||
"url": "javascript:history.go(-1)",
|
||||
"img": "fa fa-arrow-left",
|
||||
"class": "btn-default",
|
||||
"label": _("Back")},
|
||||
{"name": "reply",
|
||||
"url": "action=reply&mbox=%s&mailid=%s" % (folder, mail_id),
|
||||
"img": "fa fa-mail-reply",
|
||||
"class": "btn-primary",
|
||||
"label": _("Reply"),
|
||||
"menu": [
|
||||
{"name": "replyall",
|
||||
"url": "action=reply&mbox=%s&mailid=%s&all=1" % (folder, mail_id),
|
||||
"img": "fa fa-mail-reply-all",
|
||||
"label": _("Reply all")},
|
||||
{"name": "forward",
|
||||
"url": "action=forward&mbox=%s&mailid=%s" % (folder, mail_id),
|
||||
"img": "fa fa-mail-forward",
|
||||
"label": _("Forward")},
|
||||
]},
|
||||
{"name": "delete",
|
||||
"img": "fa fa-trash",
|
||||
"class": "btn-danger",
|
||||
"url": "{0}?mbox={1}&selection[]={2}".format(
|
||||
reverse("webmail:mail_delete"), folder, mail_id),
|
||||
"title": _("Delete")
|
||||
},
|
||||
{"name": "display_options",
|
||||
"title": _("Display options"),
|
||||
"img": "fa fa-cog",
|
||||
"menu": [
|
||||
{"name": "activate_links",
|
||||
"label": _("Activate links")},
|
||||
{"name": "disable_links",
|
||||
"label": _("Disable links")}
|
||||
]
|
||||
}
|
||||
]
|
||||
menu = render_to_string('common/buttons_list.html',
|
||||
{"selection": selection, "entries": entries,
|
||||
"user": user, "extraclasses": "pull-left"})
|
||||
|
||||
return menu
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def compose_menu(selection, backurl, user, **kwargs):
|
||||
"""The menu of the compose action."""
|
||||
entries = [
|
||||
{"name": "back",
|
||||
"url": "javascript:history.go(-2);",
|
||||
"img": "fa fa-arrow-left",
|
||||
"class": "btn-default",
|
||||
"label": _("Back")},
|
||||
{"name": "sendmail",
|
||||
"url": "",
|
||||
"img": "fa fa-send",
|
||||
"class": "btn-default btn-primary",
|
||||
"label": _("Send")},
|
||||
]
|
||||
context = {
|
||||
"selection": selection, "entries": entries, "user": user
|
||||
}
|
||||
context.update(kwargs)
|
||||
return render_to_string('webmail/compose_menubar.html', context)
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def listmailbox_menu(selection, folder, user):
|
||||
"""The menu of the listmailbox action."""
|
||||
entries = [
|
||||
{"name": "totrash",
|
||||
"title": _("Delete"),
|
||||
"class": "btn-danger",
|
||||
"img": "fa fa-trash",
|
||||
"url": reverse("webmail:mail_delete")
|
||||
},
|
||||
{"name": "actions",
|
||||
"label": _("Actions"),
|
||||
"class": "btn btn-default",
|
||||
"menu": [
|
||||
{"name": "mark-read",
|
||||
"label": _("Mark as read"),
|
||||
"url": "{0}?status=read".format(
|
||||
reverse("webmail:mail_mark", args=[folder]))},
|
||||
{"name": "mark-unread",
|
||||
"label": _("Mark as unread"),
|
||||
"url": "{0}?status=unread".format(
|
||||
reverse("webmail:mail_mark", args=[folder]))},
|
||||
]
|
||||
},
|
||||
]
|
||||
if folder == parameters.get_user(user, "TRASH_FOLDER"):
|
||||
entries[0]["class"] += " disabled"
|
||||
entries[1]["menu"] += [
|
||||
{"name": "empty",
|
||||
"label": _("Empty folder"),
|
||||
"url": "{0}?name={1}".format(
|
||||
reverse("webmail:trash_empty"), folder)}
|
||||
]
|
||||
return render_to_string('webmail/main_action_bar.html', {
|
||||
'selection': selection, 'entries': entries, 'user': user, 'css': "nav",
|
||||
'STATIC_URL': settings.STATIC_URL
|
||||
})
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def print_mailboxes(
|
||||
tree, selected=None, withunseen=False, selectonly=False,
|
||||
hdelimiter='.'):
|
||||
"""Display a tree of mailboxes and sub-mailboxes.
|
||||
|
||||
:param tree: the mailboxes to display
|
||||
"""
|
||||
result = ""
|
||||
|
||||
for mbox in tree:
|
||||
cssclass = ""
|
||||
name = mbox["path"] if "sub" in mbox else mbox["name"]
|
||||
label = separate_mailbox(mbox["name"], hdelimiter)[0]
|
||||
if mbox.get("removed", False):
|
||||
cssclass = "disabled"
|
||||
elif selected == name:
|
||||
cssclass = "active"
|
||||
result += "<li name='%s' class='droppable %s'>\n" % (name, cssclass)
|
||||
cssclass = ""
|
||||
extra_attrs = ""
|
||||
if withunseen and "unseen" in mbox:
|
||||
label += " (%d)" % mbox["unseen"]
|
||||
cssclass += " unseen"
|
||||
extra_attrs = ' data-toggle="%d"' % mbox["unseen"]
|
||||
|
||||
if "sub" in mbox:
|
||||
if selected is not None and selected != name and selected.count(
|
||||
name):
|
||||
ul_state = "visible"
|
||||
div_state = "expanded"
|
||||
else:
|
||||
ul_state = "hidden"
|
||||
div_state = "collapsed"
|
||||
result += "<div class='clickbox %s'></div>" % div_state
|
||||
|
||||
result += "<a href='%s' class='%s' name='%s'%s>" % (
|
||||
"path" in mbox and mbox["path"] or mbox["name"], cssclass,
|
||||
'selectfolder' if selectonly else 'loadfolder', extra_attrs
|
||||
)
|
||||
|
||||
iclass = mbox["class"] if "class" in mbox \
|
||||
else "fa fa-folder"
|
||||
result += "<span class='%s'></span> %s</a>" % (iclass, label)
|
||||
|
||||
if "sub" in mbox and mbox["sub"]:
|
||||
result += "<ul name='%s' class='nav nav-pills nav-stacked %s'>" % (
|
||||
mbox["path"], ul_state) + print_mailboxes(
|
||||
mbox["sub"], selected, withunseen, selectonly, hdelimiter
|
||||
) + "</ul>\n"
|
||||
result += "</li>\n"
|
||||
return result
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def mboxes_menu():
|
||||
"""Mailboxes menu."""
|
||||
entries = [
|
||||
{"name": "newmbox",
|
||||
"url": reverse("webmail:folder_add"),
|
||||
"img": "fa fa-plus",
|
||||
"label": _("Create a new mailbox"),
|
||||
"modal": True,
|
||||
"modalcb": "webmail.mboxform_cb",
|
||||
"closecb": "webmail.mboxform_close",
|
||||
"class": "btn-default btn-xs"},
|
||||
{"name": "editmbox",
|
||||
"url": reverse("webmail:folder_change"),
|
||||
"img": "fa fa-edit",
|
||||
"label": _("Edit the selected mailbox"),
|
||||
"class": "btn-default btn-xs"},
|
||||
{"name": "removembox",
|
||||
"url": reverse("webmail:folder_delete"),
|
||||
"img": "fa fa-trash",
|
||||
"label": _("Remove the selected mailbox"),
|
||||
"class": "btn-default btn-xs"},
|
||||
{"name": "compress",
|
||||
"img": "fa fa-compress",
|
||||
"label": _("Compress folder"),
|
||||
"class": "btn-default btn-xs",
|
||||
"url": reverse("webmail:folder_compress")}
|
||||
]
|
||||
|
||||
return render_to_string('common/menu.html', dict(
|
||||
entries=entries, css="dropdown-menu"
|
||||
))
|
||||
|
||||
|
||||
@register.filter
|
||||
def parse_imap_header(value, header):
|
||||
"""Simple template tag to display a IMAP header."""
|
||||
try:
|
||||
value = getattr(imapheader, "parse_%s" % header)(value)
|
||||
except AttributeError:
|
||||
pass
|
||||
return value
|
||||
@@ -1,25 +0,0 @@
|
||||
# coding: utf-8
|
||||
from django.conf.urls import patterns, url
|
||||
|
||||
urlpatterns = patterns(
|
||||
'modoboa.extensions.webmail.views',
|
||||
|
||||
url(r'^$', "index", name="index"),
|
||||
url(r'^submailboxes', "submailboxes", name="submailboxes_get"),
|
||||
url(r'^getmailcontent', 'getmailcontent', name="mailcontent_get"),
|
||||
url(r'^unseenmsgs', 'check_unseen_messages', name="unseen_messages_check"),
|
||||
|
||||
url(r'^delete/$', 'delete', name="mail_delete"),
|
||||
url(r'^move/$', "move", name="mail_move"),
|
||||
url(r'^mark/(?P<name>.+)/$', "mark", name="mail_mark"),
|
||||
|
||||
url(r'^newfolder/$', "newfolder", name="folder_add"),
|
||||
url(r'^editfolder/$', "editfolder", name="folder_change"),
|
||||
url(r'^delfolder/$', "delfolder", name="folder_delete"),
|
||||
url(r'^compressfolder/$', "folder_compress", name="folder_compress"),
|
||||
url(r'^emptytrash/$', "empty", name="trash_empty"),
|
||||
|
||||
url(r'^attachments/$', 'attachments', name="attachment_list"),
|
||||
url(r'^delattachment/$', 'delattachment', name="attachment_delete"),
|
||||
url(r'^getattachment/$', 'getattachment', name="attachment_get"),
|
||||
)
|
||||
@@ -1,618 +0,0 @@
|
||||
# coding: utf-8
|
||||
"""
|
||||
Webmail extension views.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from rfc6266 import build_header
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.template import Template, Context
|
||||
from django.utils.translation import ugettext as _, ungettext
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
|
||||
from modoboa.lib import parameters
|
||||
from modoboa.lib.exceptions import ModoboaException, BadRequest
|
||||
from modoboa.lib.paginator import Paginator
|
||||
from modoboa.lib.web_utils import (
|
||||
_render_to_string, ajax_response, render_to_json_response
|
||||
)
|
||||
from modoboa.extensions.admin.lib import needs_mailbox
|
||||
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
|
||||
)
|
||||
from .templatetags import webmail_tags
|
||||
|
||||
|
||||
@login_required
|
||||
@needs_mailbox()
|
||||
@gzip_page
|
||||
def getattachment(request):
|
||||
"""Fetch a message attachment
|
||||
|
||||
FIXME: par manque de caching, le bodystructure du message est
|
||||
redemandé pour accéder aux headers de cette pièce jointe.
|
||||
|
||||
:param request: a ``Request`` object
|
||||
"""
|
||||
mbox = request.GET.get("mbox", None)
|
||||
mailid = request.GET.get("mailid", None)
|
||||
pnum = request.GET.get("partnumber", None)
|
||||
fname = request.GET.get("fname", None)
|
||||
if not mbox or not mailid or not pnum or not fname:
|
||||
raise BadRequest(_("Invalid request"))
|
||||
|
||||
imapc = get_imapconnector(request)
|
||||
partdef, payload = imapc.fetchpart(mailid, mbox, pnum)
|
||||
resp = HttpResponse(decode_payload(partdef["encoding"], payload))
|
||||
resp["Content-Type"] = partdef["Content-Type"]
|
||||
resp["Content-Transfer-Encoding"] = partdef["encoding"]
|
||||
resp["Content-Disposition"] = build_header(fname)
|
||||
if int(partdef["size"]) < 200:
|
||||
resp["Content-Length"] = partdef["size"]
|
||||
return resp
|
||||
|
||||
|
||||
@login_required
|
||||
@needs_mailbox()
|
||||
def move(request):
|
||||
for arg in ["msgset", "to"]:
|
||||
if arg not in request.GET:
|
||||
raise BadRequest(_("Invalid request"))
|
||||
mbc = get_imapconnector(request)
|
||||
navparams = WebmailNavigationParameters(request)
|
||||
mbc.move(request.GET["msgset"], navparams.get('mbox'), request.GET["to"])
|
||||
resp = listmailbox(request, navparams.get('mbox'), update_session=False)
|
||||
return render_to_json_response(resp)
|
||||
|
||||
|
||||
@login_required
|
||||
@needs_mailbox()
|
||||
def delete(request):
|
||||
mbox = request.GET.get("mbox", None)
|
||||
selection = request.GET.getlist("selection[]", None)
|
||||
if mbox is None or selection is None:
|
||||
raise BadRequest(_("Invalid request"))
|
||||
selection = [item for item in selection if item.isdigit()]
|
||||
mbc = get_imapconnector(request)
|
||||
mbc.move(",".join(selection), mbox,
|
||||
parameters.get_user(request.user, "TRASH_FOLDER"))
|
||||
count = len(selection)
|
||||
message = ungettext("%(count)d message deleted",
|
||||
"%(count)d messages deleted",
|
||||
count) % {"count": count}
|
||||
return render_to_json_response(message)
|
||||
|
||||
|
||||
@login_required
|
||||
@needs_mailbox()
|
||||
def mark(request, name):
|
||||
status = request.GET.get("status", None)
|
||||
ids = request.GET.get("ids", None)
|
||||
if status is None or ids is None:
|
||||
raise BadRequest(_("Invalid request"))
|
||||
imapc = get_imapconnector(request)
|
||||
try:
|
||||
getattr(imapc, "mark_messages_%s" % status)(name, ids)
|
||||
except AttributeError:
|
||||
raise UnknownAction
|
||||
|
||||
return render_to_json_response({
|
||||
'action': status, 'mbox': name,
|
||||
'unseen': imapc.unseen_messages(name)
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
@needs_mailbox()
|
||||
def empty(request):
|
||||
"""Empty the trash folder."""
|
||||
name = request.GET.get("name", None)
|
||||
if name != parameters.get_user(request.user, "TRASH_FOLDER"):
|
||||
raise BadRequest(_("Invalid request"))
|
||||
get_imapconnector(request).empty(name)
|
||||
content = "<div class='alert alert-info'>%s</div>" % _("Empty mailbox")
|
||||
return render_to_json_response({
|
||||
'listing': content, 'mailbox': name, 'pages': [1]
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
@needs_mailbox()
|
||||
def folder_compress(request):
|
||||
"""Compress a mailbox."""
|
||||
name = request.GET.get("name", None)
|
||||
if name is None:
|
||||
raise BadRequest(_("Invalid request"))
|
||||
imapc = get_imapconnector(request)
|
||||
imapc.compact(name)
|
||||
return render_to_json_response({})
|
||||
|
||||
|
||||
@login_required
|
||||
@needs_mailbox()
|
||||
def newfolder(request, tplname="webmail/folder.html"):
|
||||
mbc = IMAPconnector(user=request.user.username,
|
||||
password=request.session["password"])
|
||||
|
||||
if request.method == "POST":
|
||||
form = FolderForm(request.POST)
|
||||
if form.is_valid():
|
||||
pf = request.POST.get("parent_folder", None)
|
||||
mbc.create_folder(form.cleaned_data["name"], pf)
|
||||
return render_to_json_response({
|
||||
'respmsg': _("Mailbox created"),
|
||||
'newmb': form.cleaned_data["name"], 'parent': pf
|
||||
})
|
||||
|
||||
return render_to_json_response({'form_errors': form.errors}, status=400)
|
||||
|
||||
ctx = {"title": _("Create a new mailbox"),
|
||||
"formid": "mboxform",
|
||||
"action": reverse("webmail:folder_add"),
|
||||
"action_label": _("Create"),
|
||||
"action_classes": "submit",
|
||||
"withunseen": False,
|
||||
"selectonly": True,
|
||||
"mboxes": mbc.getmboxes(request.user),
|
||||
"hdelimiter": mbc.hdelimiter,
|
||||
"form": FolderForm(),
|
||||
"selected": None}
|
||||
return render(request, tplname, ctx)
|
||||
|
||||
|
||||
@login_required
|
||||
@needs_mailbox()
|
||||
def editfolder(request, tplname="webmail/folder.html"):
|
||||
mbc = IMAPconnector(user=request.user.username,
|
||||
password=request.session["password"])
|
||||
ctx = {"title": _("Edit mailbox"),
|
||||
"formid": "mboxform",
|
||||
"action": reverse("webmail:folder_change"),
|
||||
"action_label": _("Update"),
|
||||
"action_classes": "submit",
|
||||
"withunseen": False,
|
||||
"selectonly": True,
|
||||
"hdelimiter": mbc.hdelimiter}
|
||||
|
||||
if request.method == "POST":
|
||||
form = FolderForm(request.POST)
|
||||
if form.is_valid():
|
||||
pf = request.POST.get("parent_folder", None)
|
||||
oldname, oldparent = separate_mailbox(
|
||||
request.POST["oldname"], sep=mbc.hdelimiter
|
||||
)
|
||||
res = {'respmsg': _("Mailbox updated")}
|
||||
if form.cleaned_data["name"] != oldname \
|
||||
or (pf != oldparent):
|
||||
newname = form.cleaned_data["name"] if pf is None \
|
||||
else mbc.hdelimiter.join([pf, form.cleaned_data["name"]])
|
||||
mbc.rename_folder(request.POST["oldname"], newname)
|
||||
res["oldmb"] = oldname
|
||||
res["newmb"] = form.cleaned_data["name"]
|
||||
res["oldparent"] = oldparent
|
||||
res["newparent"] = pf
|
||||
WebmailNavigationParameters(request).remove('mbox')
|
||||
return render_to_json_response(res)
|
||||
|
||||
return render_to_json_response({'form_errors': form.errors}, status=400)
|
||||
|
||||
name = request.GET.get("name", None)
|
||||
if name is None:
|
||||
raise BadRequest(_("Invalid request"))
|
||||
shortname, parent = separate_mailbox(name, sep=mbc.hdelimiter)
|
||||
ctx = {"title": _("Edit mailbox"),
|
||||
"formid": "mboxform",
|
||||
"action": reverse("webmail:folder_change"),
|
||||
"action_label": _("Update"),
|
||||
"action_classes": "submit",
|
||||
"withunseen": False,
|
||||
"selectonly": True,
|
||||
"hdelimiter": mbc.hdelimiter,
|
||||
"mboxes": mbc.getmboxes(request.user, until_mailbox=parent),
|
||||
"form": FolderForm(),
|
||||
"selected": parent}
|
||||
ctx["form"].fields["oldname"].initial = name
|
||||
ctx["form"].fields["name"].initial = shortname
|
||||
return render(request, tplname, ctx)
|
||||
|
||||
|
||||
@login_required
|
||||
@needs_mailbox()
|
||||
def delfolder(request):
|
||||
name = request.GET.get("name", None)
|
||||
if name is None:
|
||||
raise BadRequest(_("Invalid request"))
|
||||
mbc = IMAPconnector(user=request.user.username,
|
||||
password=request.session["password"])
|
||||
mbc.delete_folder(name)
|
||||
WebmailNavigationParameters(request).remove('mbox')
|
||||
return ajax_response(request)
|
||||
|
||||
|
||||
@login_required
|
||||
@csrf_exempt
|
||||
@needs_mailbox()
|
||||
def attachments(request, tplname="webmail/attachments.html"):
|
||||
if request.method == "POST":
|
||||
csuploader = AttachmentUploadHandler()
|
||||
request.upload_handlers.insert(0, csuploader)
|
||||
error = None
|
||||
form = AttachmentForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
try:
|
||||
fobj = request.FILES["attachment"]
|
||||
tmpname = save_attachment(fobj)
|
||||
request.session["compose_mail"]["attachments"] \
|
||||
+= [{"fname": str(fobj),
|
||||
"content-type": fobj.content_type,
|
||||
"size": fobj.size,
|
||||
"tmpname": os.path.basename(tmpname)}]
|
||||
request.session.modified = True
|
||||
return render(request, "webmail/upload_done.html", {
|
||||
"status": "ok", "fname": request.FILES["attachment"],
|
||||
"tmpname": os.path.basename(tmpname)
|
||||
})
|
||||
except ModoboaException as inst:
|
||||
error = _("Failed to save attachment: ") + str(inst)
|
||||
|
||||
if csuploader.toobig:
|
||||
error = _("Attachment is too big (limit: %s)"
|
||||
% parameters.get_admin("MAX_ATTACHMENT_SIZE"))
|
||||
return render(request, "webmail/upload_done.html", {
|
||||
"status": "ko", "error": error
|
||||
})
|
||||
ctx = {
|
||||
"title": _("Attachments"),
|
||||
"formid": "uploadfile",
|
||||
"target": "upload_target",
|
||||
"enctype": "multipart/form-data",
|
||||
"form": AttachmentForm(),
|
||||
"action": reverse("webmail:attachment_list"),
|
||||
"attachments": request.session["compose_mail"]["attachments"]
|
||||
}
|
||||
return render(request, tplname, ctx)
|
||||
|
||||
|
||||
@login_required
|
||||
@needs_mailbox()
|
||||
def delattachment(request):
|
||||
if "compose_mail" not in request.session \
|
||||
or "name" not in request.GET \
|
||||
or not request.GET["name"]:
|
||||
return ajax_response(request, "ko", respmsg=_("Bad query"))
|
||||
|
||||
error = None
|
||||
for att in request.session["compose_mail"]["attachments"]:
|
||||
if att["tmpname"] == request.GET["name"]:
|
||||
request.session["compose_mail"]["attachments"].remove(att)
|
||||
fullpath = os.path.join(
|
||||
settings.MEDIA_ROOT, "webmail", att["tmpname"]
|
||||
)
|
||||
try:
|
||||
os.remove(fullpath)
|
||||
except OSError as e:
|
||||
error = _("Failed to remove attachment: ") + str(e)
|
||||
break
|
||||
request.session.modified = True
|
||||
return ajax_response(request)
|
||||
if error is None:
|
||||
error = _("Unknown attachment")
|
||||
return ajax_response(request, "ko", respmsg=error)
|
||||
|
||||
|
||||
def render_mboxes_list(request, imapc):
|
||||
"""Return the HTML representation of a mailboxes list
|
||||
|
||||
:param request: a ``Request`` object
|
||||
:param imapc: an ``IMAPconnector` object
|
||||
:return: a string
|
||||
"""
|
||||
curmbox = WebmailNavigationParameters(request).get("mbox", "INBOX")
|
||||
return _render_to_string(request, "webmail/folders.html", {
|
||||
"selected": curmbox,
|
||||
"mboxes": imapc.getmboxes(request.user),
|
||||
"withunseen": True
|
||||
})
|
||||
|
||||
|
||||
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"))
|
||||
|
||||
paginator = Paginator(
|
||||
mbc.messages_count(folder=mbox, order=navparams.get("order")),
|
||||
int(parameters.get_user(request.user, "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(request, "webmail/email_list.html", {
|
||||
"email_list": email_list,
|
||||
"page": page_id,
|
||||
"with_top_div": request.GET.get("scroll", "false") == "false"
|
||||
})
|
||||
length = len(content)
|
||||
else:
|
||||
if page_id == 1:
|
||||
content = "<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]}
|
||||
|
||||
|
||||
def render_compose(request, form, posturl, email=None, insert_signature=False):
|
||||
"""Render the compose form."""
|
||||
resp = {}
|
||||
if email is None:
|
||||
body = u""
|
||||
textheader = u""
|
||||
else:
|
||||
body = email.body
|
||||
textheader = email.textheader
|
||||
if insert_signature:
|
||||
signature = EmailSignature(request.user)
|
||||
body += unicode(signature)
|
||||
randid = None
|
||||
if "id" not in request.GET:
|
||||
if "compose_mail" in request.session:
|
||||
clean_attachments(request.session["compose_mail"]["attachments"])
|
||||
randid = set_compose_session(request)
|
||||
elif "compose_mail" not in request.session \
|
||||
or request.session["compose_mail"]["id"] != request.GET["id"]:
|
||||
randid = set_compose_session(request)
|
||||
|
||||
attachment_list = request.session["compose_mail"]["attachments"]
|
||||
if attachment_list:
|
||||
resp["menuargs"] = {"attachment_counter": len(attachment_list)}
|
||||
|
||||
content = _render_to_string(request, "webmail/compose.html", {
|
||||
"form": form, "bodyheader": textheader,
|
||||
"body": body, "posturl": posturl
|
||||
})
|
||||
|
||||
resp.update({
|
||||
"listing": content,
|
||||
"editor": parameters.get_user(request.user, "EDITOR")
|
||||
})
|
||||
if randid is not None:
|
||||
resp["id"] = randid
|
||||
return resp
|
||||
|
||||
|
||||
def compose(request):
|
||||
url = "?action=compose"
|
||||
if request.method == "POST":
|
||||
form = ComposeMailForm(request.POST)
|
||||
status, resp = send_mail(request, form, posturl=url)
|
||||
return resp
|
||||
|
||||
form = ComposeMailForm()
|
||||
return render_compose(request, form, url, insert_signature=True)
|
||||
|
||||
|
||||
def get_mail_info(request):
|
||||
"""Retrieve a mailbox and an email ID from a request.
|
||||
"""
|
||||
mbox = request.GET.get("mbox", None)
|
||||
mailid = request.GET.get("mailid", None)
|
||||
if mbox is None or mailid is None:
|
||||
raise BadRequest(_("Invalid request"))
|
||||
return mbox, mailid
|
||||
|
||||
|
||||
def new_compose_form(request, action, mbox, mailid):
|
||||
"""Return a new composition form.
|
||||
|
||||
Valid for reply and forward actions only.
|
||||
"""
|
||||
form = ComposeMailForm()
|
||||
modclass = globals()["%sModifier" % action.capitalize()]
|
||||
email = modclass(form, request, True, "%s:%s" % (mbox, mailid), links="1")
|
||||
url = "?action=%s&mbox=%s&mailid=%s" % (action, mbox, mailid)
|
||||
return render_compose(request, form, url, email)
|
||||
|
||||
|
||||
def reply(request):
|
||||
"""Reply to email.
|
||||
"""
|
||||
mbox, mailid = get_mail_info(request)
|
||||
if request.method == "POST":
|
||||
url = "?action=reply&mbox=%s&mailid=%s" % (mbox, mailid)
|
||||
form = ComposeMailForm(request.POST)
|
||||
status, resp = send_mail(request, form, url)
|
||||
if status:
|
||||
get_imapconnector(request).msg_answered(mbox, mailid)
|
||||
return resp
|
||||
return new_compose_form(request, "reply", mbox, mailid)
|
||||
|
||||
|
||||
def forward(request):
|
||||
"""Forward email.
|
||||
"""
|
||||
mbox, mailid = get_mail_info(request)
|
||||
if request.method == "POST":
|
||||
url = "?action=forward&mbox=%s&mailid=%s" % (mbox, mailid)
|
||||
form = ForwardMailForm(request.POST)
|
||||
status, resp = send_mail(request, form, url)
|
||||
if status:
|
||||
get_imapconnector(request).msg_forwarded(mbox, mailid)
|
||||
return resp
|
||||
return new_compose_form(request, "forward", mbox, mailid)
|
||||
|
||||
|
||||
@login_required
|
||||
@needs_mailbox()
|
||||
def getmailcontent(request):
|
||||
mbox = request.GET.get("mbox", None)
|
||||
mailid = request.GET.get("mailid", None)
|
||||
if mbox is None or mailid is None:
|
||||
raise BadRequest(_("Invalid request"))
|
||||
email = ImapEmail(
|
||||
request, False, "%s:%s" % (mbox, mailid), dformat="DISPLAYMODE",
|
||||
links=int(request.GET["links"])
|
||||
)
|
||||
return render(request, "common/viewmail.html", {
|
||||
"headers": email.render_headers(folder=mbox, mail_id=mailid),
|
||||
"folder": mbox, "imapid": mailid,
|
||||
"mailbody": email.body if email.body else ""
|
||||
})
|
||||
|
||||
|
||||
def viewmail(request):
|
||||
mbox = request.GET.get("mbox", None)
|
||||
mailid = request.GET.get("mailid", None)
|
||||
if mbox is None or mailid is None:
|
||||
raise BadRequest(_("Invalid request"))
|
||||
links = request.GET.get("links", None)
|
||||
if links is None:
|
||||
links = 1 if parameters.get_user(
|
||||
request.user, "ENABLE_LINKS") == "yes" else 0
|
||||
else:
|
||||
links = int(links)
|
||||
|
||||
url = "{0}?mbox={1}&mailid={2}&links={3}".format(
|
||||
reverse("webmail:mailcontent_get"), mbox, mailid, links)
|
||||
content = Template("""
|
||||
<iframe src="{{ url }}" id="mailcontent"></iframe>
|
||||
""").render(Context({"url": url}))
|
||||
|
||||
return dict(listing=content, menuargs=dict(mail_id=mailid))
|
||||
|
||||
|
||||
@login_required
|
||||
@needs_mailbox()
|
||||
def submailboxes(request):
|
||||
"""Retrieve the sub mailboxes of a mailbox."""
|
||||
topmailbox = request.GET.get('topmailbox', '')
|
||||
with_unseen = request.GET.get('unseen', None)
|
||||
mboxes = get_imapconnector(request).getmboxes(
|
||||
request.user, topmailbox, unseen_messages=with_unseen == 'true')
|
||||
return render_to_json_response(mboxes)
|
||||
|
||||
|
||||
@login_required
|
||||
@needs_mailbox()
|
||||
def check_unseen_messages(request):
|
||||
mboxes = request.GET.get("mboxes", None)
|
||||
if not mboxes:
|
||||
raise BadRequest(_("Invalid request"))
|
||||
mboxes = mboxes.split(",")
|
||||
counters = dict()
|
||||
imapc = get_imapconnector(request)
|
||||
for mb in mboxes:
|
||||
counters[mb] = imapc.unseen_messages(mb)
|
||||
return render_to_json_response(counters)
|
||||
|
||||
|
||||
@login_required
|
||||
@needs_mailbox()
|
||||
def index(request):
|
||||
"""Webmail actions handler
|
||||
|
||||
Problèmes liés à la navigation 'anchor based'
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Lors d'un rafraichissemt complet, une première requête est envoyée
|
||||
vers /webmail/. On ne connait pas encore l'action qui va être
|
||||
demandée mais on peut déjà envoyer des informations indépendantes
|
||||
(comme les dossiers, le quota).
|
||||
|
||||
Si on se contente de cela, l'affichage donnera un aspect décomposé
|
||||
qui n'est pas très séduisant (à cause de la latence notamment). Il
|
||||
faudrait pouvoir envoyer le menu par la même occasion, le souci
|
||||
étant de savoir lequel...
|
||||
|
||||
Une solution possible : il suffirait de déplacer le menu vers la
|
||||
droite pour l'aligner avec le contenu, remonter la liste des
|
||||
dossiers (même hauteur que le menu) et renvoyer le menu en même
|
||||
temps que le contenu. Le rendu sera plus uniforme je pense.
|
||||
|
||||
"""
|
||||
action = request.GET.get("action", None)
|
||||
if action is not None:
|
||||
if action not in globals():
|
||||
raise UnknownAction
|
||||
response = globals()[action](request)
|
||||
else:
|
||||
if request.is_ajax():
|
||||
raise BadRequest(_("Invalid request"))
|
||||
response = dict(selection="webmail")
|
||||
|
||||
curmbox = WebmailNavigationParameters(request).get("mbox", "INBOX")
|
||||
if not request.is_ajax():
|
||||
request.session["lastaction"] = None
|
||||
imapc = get_imapconnector(request)
|
||||
response["hdelimiter"] = imapc.hdelimiter
|
||||
response["mboxes"] = render_mboxes_list(request, imapc)
|
||||
imapc.getquota(curmbox)
|
||||
response["refreshrate"] = \
|
||||
int(parameters.get_user(request.user, "REFRESH_INTERVAL"))
|
||||
response["quota"] = imapc.quota_usage
|
||||
trash = parameters.get_user(request.user, "TRASH_FOLDER")
|
||||
response["trash"] = trash
|
||||
response["ro_mboxes"] = [
|
||||
"INBOX", "Junk",
|
||||
parameters.get_user(request.user, "SENT_FOLDER"),
|
||||
trash,
|
||||
parameters.get_user(request.user, "DRAFTS_FOLDER")
|
||||
]
|
||||
response['mboxes_col_width'] = \
|
||||
int(parameters.get_user(request.user, 'MBOXES_COL_WIDTH'))
|
||||
return render(request, "webmail/index.html", response)
|
||||
|
||||
if action in ["reply", "forward"]:
|
||||
action = "compose"
|
||||
if request.session["lastaction"] != action:
|
||||
extra_args = {}
|
||||
if "menuargs" in response:
|
||||
extra_args = response["menuargs"]
|
||||
del response["menuargs"]
|
||||
try:
|
||||
menu = getattr(webmail_tags, "%s_menu" % action)
|
||||
response["menu"] = menu("", curmbox, request.user, **extra_args)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
response.update(callback=action)
|
||||
http_status = 200
|
||||
if "status" in response:
|
||||
del response['status']
|
||||
http_status = 400
|
||||
return render_to_json_response(response, status=http_status)
|
||||
Reference in New Issue
Block a user