Remove webmail extension.

This commit is contained in:
Antoine Nguyen
2015-02-11 10:34:25 +01:00
parent fe8030ac87
commit 965b88adbd
32 changed files with 0 additions and 4956 deletions
-162
View File
@@ -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']
-44
View File
@@ -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)
-211
View File
@@ -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)
-327
View File
@@ -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)
-788
View File
@@ -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
-46
View File
@@ -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
-25
View File
@@ -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"),
)
-618
View File
@@ -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)