mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
Merge with mainline
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
# coding: utf-8
|
||||
"""
|
||||
:mod:`exceptions` --- Webmail custom exceptions
|
||||
-----------------------------------------------
|
||||
|
||||
"""
|
||||
import re
|
||||
|
||||
class WebmailError(Exception):
|
||||
errorexpr = re.compile("\[([^\]]+)\]\s*([^\.]+)")
|
||||
|
||||
def __init__(self, reason, ajax=False):
|
||||
m = WebmailError.errorexpr.match(reason)
|
||||
if m is None:
|
||||
self.reason = reason
|
||||
else:
|
||||
self.reason = "%s: %s" % (_("Server response"), m.group(2))
|
||||
self.ajax = ajax
|
||||
|
||||
def __str__(self):
|
||||
return self.reason
|
||||
|
||||
class ImapError(Exception):
|
||||
def __init__(self, reason):
|
||||
self.reason = reason
|
||||
|
||||
def __str__(self):
|
||||
return str(self.reason)
|
||||
@@ -0,0 +1,173 @@
|
||||
# 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
|
||||
|
||||
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
|
||||
if buf[0] == '{':
|
||||
# Literal
|
||||
end = buf.find('}')
|
||||
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")
|
||||
return 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)]
|
||||
newret.append(subret)
|
||||
ret.append(newret)
|
||||
else:
|
||||
ret[-1].append(subret)
|
||||
pos += end + 1
|
||||
nb_bodystruct += 1
|
||||
continue
|
||||
|
||||
if c == ')':
|
||||
partnum = None
|
||||
if depth:
|
||||
if type(ret[0]) == list or len(ret) >= 7:
|
||||
partnum = prefix
|
||||
if partnum is not None:
|
||||
return [dict(partnum=partnum, struct=ret)], pos
|
||||
return ret, pos
|
||||
if c == ' ':
|
||||
pos += 1
|
||||
continue
|
||||
nb_bodystruct = 0
|
||||
end = parse_next_token(buf[pos:])
|
||||
ret.append(buf[pos:pos + end + 1].strip('"'))
|
||||
pos += end + 1
|
||||
|
||||
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 = {}
|
||||
for part in data[0:-1]:
|
||||
if part == ')':
|
||||
continue
|
||||
buf = "".join(part)
|
||||
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
|
||||
for pos, c in enumerate(response):
|
||||
if c == '(':
|
||||
parendepth += 1
|
||||
continue
|
||||
if c == ')':
|
||||
parendepth -= 1
|
||||
if parendepth == 0:
|
||||
end = pos + 1
|
||||
break
|
||||
else:
|
||||
end = parse_next_token(response)
|
||||
if response[0] == '{':
|
||||
datalen = int(response[1:end])
|
||||
response = response[end + 1:]
|
||||
end = datalen
|
||||
|
||||
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 type(bs[0]) == dict:
|
||||
print "%s :" % bs[0]["partnum"],
|
||||
struct = bs[0]["struct"]
|
||||
else:
|
||||
struct = bs
|
||||
|
||||
if type(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)'), ')']
|
||||
|
||||
print parse_fetch_response(resp)
|
||||
@@ -0,0 +1,550 @@
|
||||
# coding: utf-8
|
||||
"""
|
||||
:mod:`imaputils` --- Extra IMAPv4 utilities
|
||||
-------------------------------------------
|
||||
"""
|
||||
import imaplib, ssl, email
|
||||
import re
|
||||
from functools import wraps
|
||||
from imapclient.response_parser import parse_fetch_response
|
||||
from modoboa.lib import parameters
|
||||
from modoboa.lib.connections import *
|
||||
from modoboa.lib.webutils import static_url
|
||||
from exceptions import ImapError
|
||||
from fetch_parser import *
|
||||
|
||||
#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.m.capabilities:
|
||||
return method(cls, *args, **kwargs)
|
||||
return getattr(cls, self.fallback_method)(*args, **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 prefix: 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])
|
||||
subtype = definition[1].lower()
|
||||
mtype = "%s/%s" % (definition[0].lower(), subtype)
|
||||
if mtype in ("text/plain", "text/html"):
|
||||
if not self.contents.has_key(mtype):
|
||||
self.contents[subtype] = params
|
||||
return
|
||||
elif multisubtype in ["related"]:
|
||||
self.inlines[params["cid"].strip("<>")] = params
|
||||
return
|
||||
|
||||
if len(definition) > 7:
|
||||
params.update(md5=definition[7], disposition=definition[8],
|
||||
lang=definition[9], location=definition[10])
|
||||
self.attachments += [params]
|
||||
|
||||
def load_from_definition(self, definition, multisubtype=None):
|
||||
if type(definition) == dict:
|
||||
struct = definition["struct"]
|
||||
pnum = definition["partnum"]
|
||||
elif type(definition[0]) == dict:
|
||||
struct = definition[0]["struct"]
|
||||
pnum = definition[0]["partnum"]
|
||||
else:
|
||||
struct = definition
|
||||
pnum = None
|
||||
|
||||
if type(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)
|
||||
|
||||
class IMAPconnector(object):
|
||||
__metaclass__ = ConnectionsManager
|
||||
|
||||
list_base_pattern = r'\((?P<flags>.*?)\) "(?P<delimiter>.*)" "(?P<name>[^"]*)"'
|
||||
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.criterions = []
|
||||
self.address = parameters.get_admin("IMAP_SERVER")
|
||||
self.port = int(parameters.get_admin("IMAP_PORT"))
|
||||
status, msg = self.login(user, password)
|
||||
if not status:
|
||||
raise Exception(msg)
|
||||
|
||||
def _cmd(self, name, *args):
|
||||
"""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']:
|
||||
try:
|
||||
typ, data = self.m.uid(name, *args)
|
||||
except imaplib.IMAP4.error, 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, e:
|
||||
raise ImapError(e)
|
||||
if typ == "NO":
|
||||
raise ImapError(data)
|
||||
if not name in self.m.untagged_responses:
|
||||
return None
|
||||
return self.m.untagged_responses.pop(name)
|
||||
|
||||
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)
|
||||
|
||||
def refresh(self, user, password):
|
||||
"""Check if current connection needs a refresh
|
||||
|
||||
Is it really secure?
|
||||
"""
|
||||
if self.m is not None:
|
||||
try:
|
||||
self.m.select()
|
||||
return
|
||||
except imaplib.IMAP4.error, error:
|
||||
print error
|
||||
print self.login(user, password)
|
||||
|
||||
|
||||
def login(self, user, passwd):
|
||||
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), error:
|
||||
return False, _("Connection to IMAP server failed, check your configuration")
|
||||
try:
|
||||
self.m.login(user, passwd)
|
||||
except (imaplib.IMAP4.error, ssl.SSLError), error:
|
||||
return False, _("Authentication failed, check your configuration")
|
||||
return True, None
|
||||
|
||||
def logout(self):
|
||||
try:
|
||||
self.m.select()
|
||||
except imaplib.IMAP4.error:
|
||||
pass
|
||||
self.m.logout()
|
||||
del self.m
|
||||
self.m = None
|
||||
|
||||
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.keys() and kwargs["order"]:
|
||||
sign = kwargs["order"][:1]
|
||||
criterion = kwargs["order"][1:].upper()
|
||||
if sign == '-':
|
||||
criterion = "REVERSE %s" % criterion
|
||||
else:
|
||||
criterion = "REVERSE DATE"
|
||||
folder = kwargs.has_key("folder") and kwargs["folder"] or None
|
||||
self.select_mailbox(folder, 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):
|
||||
"""Issue a SELECT/EXAMINE command to the server
|
||||
|
||||
The given name is first 'imap-utf7' encoded.
|
||||
|
||||
:param name: mailbox's name
|
||||
:param readonly:
|
||||
"""
|
||||
name = name.encode("imap4-utf-7")
|
||||
if readonly:
|
||||
self._cmd("EXAMINE", name)
|
||||
else:
|
||||
self._cmd("SELECT", name)
|
||||
|
||||
def unseen_messages(self, mailbox):
|
||||
"""Return the number of unseen messages
|
||||
|
||||
:param mailbox: the mailbox's name
|
||||
:return: an integer
|
||||
"""
|
||||
# self.select_mailbox(mailbox)
|
||||
# data = self._cmd("SEARCH", "UTF-8", "(NOT DELETED UNSEEN)")
|
||||
# return len(data[0].split())
|
||||
data = self._cmd("STATUS", mailbox.encode("imap4-utf-7"), "(UNSEEN)")
|
||||
m = self.unseen_pattern.match(data[0])
|
||||
if m is None:
|
||||
return 0
|
||||
return int(m.group(1))
|
||||
|
||||
def _encodefolder(self, folder):
|
||||
if not folder:
|
||||
return "INBOX"
|
||||
return folder.encode("imap4-utf-7")
|
||||
|
||||
def _parse_folder_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_folder_name(sdescr["sub"], path, delimiter, parts[1:]):
|
||||
sdescr["class"] = "subfolders"
|
||||
return True
|
||||
|
||||
def _listfolders_simple(self, topmailbox='INBOX', md_mailboxes=[]):
|
||||
(status, data) = self.m.list()
|
||||
result = []
|
||||
for mb in data:
|
||||
flags, delimiter, name = self.list_response_pattern.match(mb).groups()
|
||||
name = name.strip('"').decode("imap4-utf-7")
|
||||
if re.search("\%s" % delimiter, name):
|
||||
parts = name.split(".")
|
||||
if not descr.has_key("path"):
|
||||
descr["path"] = parts[0]
|
||||
descr["sub"] = []
|
||||
if self._parse_folder_name(descr["sub"], parts[0], delimiter,
|
||||
parts[1:]):
|
||||
descr["class"] = "subfolders"
|
||||
continue
|
||||
present = False
|
||||
descr = {"name" : name}
|
||||
for mdf in md_mailboxes:
|
||||
if mdf["name"] == name:
|
||||
present = True
|
||||
break
|
||||
if not present:
|
||||
result += [descr]
|
||||
from operator import itemgetter
|
||||
return sorted(result, key=itemgetter("name"))
|
||||
|
||||
@capability('LIST-EXTENDED', '_listfolders_simple')
|
||||
def _listfolders(self, topmailbox='', md_mailboxes=[]):
|
||||
pattern = ("%s.%%" % topmailbox.encode("imap4-utf-7")) if len(topmailbox) else "%"
|
||||
resp = self._cmd("LSUB", "", pattern, "RETURN", "(CHILDREN)")
|
||||
result = []
|
||||
for mb in resp:
|
||||
flags, delimiter, name, childinfo = \
|
||||
self.listextended_response_pattern.match(mb).groups()
|
||||
flags = flags.split(' ')
|
||||
name = name.decode("imap4-utf-7")
|
||||
present = False
|
||||
for mdm in md_mailboxes:
|
||||
if mdm["name"] == name:
|
||||
present = True
|
||||
break
|
||||
if present:
|
||||
continue
|
||||
descr = dict(name=name)
|
||||
if r'\HasChildren' in flags:
|
||||
descr["path"] = name
|
||||
descr["sub"] = []
|
||||
descr["class"] = "subfolders"
|
||||
result += [descr]
|
||||
from operator import itemgetter
|
||||
return sorted(result, key=itemgetter("name"))
|
||||
|
||||
def getfolders(self, user, topmailbox='', unseen_messages=True):
|
||||
if len(topmailbox):
|
||||
md_mailboxes = []
|
||||
else:
|
||||
md_mailboxes = [{"name" : "INBOX", "class" : "inbox"},
|
||||
{"name" : parameters.get_user(user, "DRAFTS_FOLDER"),
|
||||
"class" : "drafts"},
|
||||
{"name" : 'Junk'},
|
||||
{"name" : parameters.get_user(user, "SENT_FOLDER")},
|
||||
{"name" : parameters.get_user(user, "TRASH_FOLDER"),
|
||||
"class" : "trash"}]
|
||||
md_mailboxes += self._listfolders(topmailbox, md_mailboxes)
|
||||
if unseen_messages:
|
||||
for fd in md_mailboxes:
|
||||
key = fd.has_key("path") and "path" or "name"
|
||||
count = self.unseen_messages(fd[key])
|
||||
if count == 0:
|
||||
continue
|
||||
fd["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 msgforwarded(self, folder, imapid):
|
||||
self._add_flag(folder, imapid, '($Forwarded)')
|
||||
|
||||
def msg_answered(self, folder, imapid):
|
||||
self._add_flag(folder, imapid, 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)')
|
||||
|
||||
def push_mail(self, folder, msg):
|
||||
now = imaplib.Time2Internaldate(time.time())
|
||||
self.m.append(self._encodefolder(folder), r'(\Seen)', now, str(msg))
|
||||
|
||||
def empty(self, mbox):
|
||||
self.select_mailbox(mbox, False)
|
||||
resp = self._cmd("SEARCH", "ALL")
|
||||
print resp
|
||||
|
||||
# for num in data[0].split():
|
||||
# self.m.store(num, "+FLAGS", r'(\Deleted)')
|
||||
# self.m.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" % (parent, name)
|
||||
typ, data = self.m.create(self._encodefolder(name))
|
||||
if typ == "NO":
|
||||
raise WebmailError(data[0])
|
||||
return True
|
||||
|
||||
def rename_folder(self, oldname, newname):
|
||||
typ, data = self.m.rename(self._encodefolder(oldname),
|
||||
self._encodefolder(newname))
|
||||
if typ == "NO":
|
||||
raise WebmailError(data[0], ajax=True)
|
||||
return True
|
||||
|
||||
def delete_folder(self, name):
|
||||
typ, data = self.m.delete(self._encodefolder(name))
|
||||
if typ == "NO":
|
||||
raise WebmailError(data[0])
|
||||
return True
|
||||
|
||||
def getquota(self, folder):
|
||||
if not "QUOTA" in self.m.capabilities:
|
||||
self.quota_limit = self.quota_actual = None
|
||||
return
|
||||
|
||||
status, data = self.m.getquotaroot(self._encodefolder(folder))
|
||||
if status == "OK":
|
||||
quotadef = data[1][0]
|
||||
m = re.match("[^\s]+ \(STORAGE (\d+) (\d+)\)", quotadef)
|
||||
if not m:
|
||||
print "Problem while parsing quota def"
|
||||
return
|
||||
self.quota_limit = int(m.group(2))
|
||||
self.quota_actual = int(m.group(1))
|
||||
|
||||
def fetchpart(self, uid, mbox, partnum):
|
||||
self.select_mailbox(mbox, False)
|
||||
data = self._cmd("FETCH", uid, "(BODY[%s])" % partnum)
|
||||
return data[int(uid)]["BODY[%s]" % partnum]
|
||||
|
||||
def fetch(self, start, stop=None, mbox=None, **kwargs):
|
||||
"""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]
|
||||
range = ",".join(submessages)
|
||||
else:
|
||||
submessages = [start]
|
||||
range = start
|
||||
query = '(FLAGS BODYSTRUCTURE BODY.PEEK[HEADER.FIELDS (DATE FROM TO CC SUBJECT)])'
|
||||
data = self._cmd("FETCH", range, 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 not r'\Seen' in data[int(uid)]['FLAGS']:
|
||||
msg['class'] = 'unseen'
|
||||
if r'\Answered' in data[int(uid)]['FLAGS']:
|
||||
msg['img_flags'] = static_url('pics/answered.png')
|
||||
bs = BodyStructure(data[int(uid)]['BODYSTRUCTURE'])
|
||||
if bs.has_attachments():
|
||||
msg['img_withatts'] = static_url('pics/attachment.png')
|
||||
result += [msg]
|
||||
return result
|
||||
|
||||
def fetchmail(self, mbox, mailid):
|
||||
"""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 fmt: the desired content's format
|
||||
"""
|
||||
self.select_mailbox(mbox, False)
|
||||
data = self._cmd("FETCH", mailid, "(BODYSTRUCTURE BODY.PEEK[HEADER.FIELDS (DATE FROM TO CC SUBJECT)])")
|
||||
return data[int(mailid)]
|
||||
|
||||
def get_imapconnector(request):
|
||||
imapc = IMAPconnector(user=request.user.username,
|
||||
password=request.session["password"])
|
||||
return imapc
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import doctest
|
||||
doctest.testmod()
|
||||
|
||||
+102
-294
@@ -7,7 +7,6 @@ sys.setdefaultencoding("utf-8")
|
||||
import re
|
||||
import time
|
||||
import socket
|
||||
import imaplib
|
||||
import ssl
|
||||
from datetime import datetime, timedelta
|
||||
import email
|
||||
@@ -22,19 +21,8 @@ from modoboa.lib.connections import ConnectionsManager
|
||||
from modoboa.lib.email_listing import MBconnector, EmailListing
|
||||
from modoboa.lib.emailutils import *
|
||||
|
||||
class WebmailError(Exception):
|
||||
errorexpr = re.compile("\[([^\]]+)\]\s*([^\.]+)")
|
||||
|
||||
def __init__(self, reason, ajax=False):
|
||||
m = WebmailError.errorexpr.match(reason)
|
||||
if m is None:
|
||||
self.reason = reason
|
||||
else:
|
||||
self.reason = "%s: %s" % (_("Server response"), m.group(2))
|
||||
self.ajax = ajax
|
||||
|
||||
def __str__(self):
|
||||
return self.reason
|
||||
from imaputils import *
|
||||
from exceptions import *
|
||||
|
||||
class WMtable(tables.Table):
|
||||
tableid = "emails"
|
||||
@@ -42,12 +30,13 @@ class WMtable(tables.Table):
|
||||
select = tables.ImgColumn("select", cssclass="draggable left", width="1%",
|
||||
defvalue="%spics/grippy.png" % settings.MEDIA_URL,
|
||||
header="<input type='checkbox' name='toggleselect' id='toggleselect' />")
|
||||
flags = tables.ImgColumn("flags", width="4%")
|
||||
flags = tables.ImgColumn("flags", width="2%")
|
||||
withatts = tables.ImgColumn("withatts", width="2%")
|
||||
subject = tables.Column("subject", label=__("Subject"), width="50%")
|
||||
from_ = tables.Column("from", width="20%", label=__("From"))
|
||||
date = tables.Column("date", width="20%", label=__("Date"))
|
||||
|
||||
cols_order = ["select", "flags", "subject", "from_", "date"]
|
||||
cols_order = ["select", "withatts", "flags", "subject", "from_", "date"]
|
||||
|
||||
def parse(self, header, value):
|
||||
try:
|
||||
@@ -108,278 +97,6 @@ class IMAPheader(object):
|
||||
def parse_subject(value, **kwargs):
|
||||
return decode(u2u_decode.u2u_decode(value))
|
||||
|
||||
class IMAPconnector(object):
|
||||
__metaclass__ = ConnectionsManager
|
||||
|
||||
def __init__(self, user=None, password=None):
|
||||
self.criterions = []
|
||||
self.address = parameters.get_admin("IMAP_SERVER")
|
||||
self.port = int(parameters.get_admin("IMAP_PORT"))
|
||||
status, msg = self.login(user, password)
|
||||
if not status:
|
||||
raise Exception(msg)
|
||||
|
||||
def refresh(self, user, password):
|
||||
"""Check if current connection needs a refresh
|
||||
|
||||
Is it really secure?
|
||||
"""
|
||||
if self.m is not None:
|
||||
try:
|
||||
self.m.select()
|
||||
return
|
||||
except imaplib.IMAP4.error, error:
|
||||
print error
|
||||
print self.login(user, password)
|
||||
|
||||
|
||||
def login(self, user, passwd):
|
||||
import imaplib
|
||||
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), error:
|
||||
return False, _("Connection to IMAP server failed, check your configuration")
|
||||
try:
|
||||
self.m.login(user, passwd)
|
||||
except (imaplib.IMAP4.error, ssl.SSLError), error:
|
||||
return False, _("Authentication failed, check your configuration")
|
||||
return True, None
|
||||
|
||||
def logout(self):
|
||||
try:
|
||||
self.m.select()
|
||||
except imaplib.IMAP4.error:
|
||||
pass
|
||||
self.m.logout()
|
||||
del self.m
|
||||
self.m = None
|
||||
|
||||
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.keys() and kwargs["order"]:
|
||||
sign = kwargs["order"][:1]
|
||||
criterion = kwargs["order"][1:].upper()
|
||||
if sign == '-':
|
||||
criterion = "REVERSE %s" % criterion
|
||||
else:
|
||||
criterion = "REVERSE DATE"
|
||||
folder = kwargs.has_key("folder") and kwargs["folder"] or None
|
||||
(status, data) = self.m.select(self._encodefolder(folder))
|
||||
(status, data) = self.m.sort("(%s)" % criterion, "UTF-8", "(NOT DELETED)",
|
||||
*self.criterions)
|
||||
self.messages = data[0].split()
|
||||
self.getquota(folder)
|
||||
return len(self.messages)
|
||||
|
||||
def unseen_messages(self, folder):
|
||||
"""Return the number of unseen messages for folder"""
|
||||
self.m.select(self._encodefolder(folder), True)
|
||||
status, data = self.m.search("UTF-8", "(NOT DELETED UNSEEN)")
|
||||
if status != "OK":
|
||||
return
|
||||
return len(data[0].split())
|
||||
|
||||
def _encodefolder(self, folder):
|
||||
if not folder:
|
||||
return "INBOX"
|
||||
return folder.encode("imap4-utf-7")
|
||||
|
||||
def _parse_folder_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_folder_name(sdescr["sub"], path, delimiter, parts[1:]):
|
||||
sdescr["class"] = "subfolders"
|
||||
return True
|
||||
|
||||
def _listfolders(self, topfolder='INBOX', folders=[]):
|
||||
list_response_pattern = re.compile(r'\((?P<flags>.*?)\) "(?P<delimiter>.*)" (?P<name>.*)')
|
||||
(status, data) = self.m.list()
|
||||
for mb in data:
|
||||
flags, delimiter, name = list_response_pattern.match(mb).groups()
|
||||
name = name.strip('"').decode("imap4-utf-7")
|
||||
if re.search("\%s" % delimiter, name):
|
||||
parts = name.split(".")
|
||||
if not descr.has_key("path"):
|
||||
descr["path"] = parts[0]
|
||||
descr["sub"] = []
|
||||
if self._parse_folder_name(descr["sub"], parts[0], delimiter,
|
||||
parts[1:]):
|
||||
descr["class"] = "subfolders"
|
||||
continue
|
||||
|
||||
pos = -1
|
||||
descr = {"name" : name}
|
||||
for idx, mdf in enumerate(folders):
|
||||
if mdf["name"] == name:
|
||||
pos = idx
|
||||
break
|
||||
if pos == -1:
|
||||
folders += [descr]
|
||||
else:
|
||||
folders[idx].update(**descr)
|
||||
|
||||
from operator import itemgetter
|
||||
return sorted(folders, key=itemgetter("name"))
|
||||
|
||||
def getfolders(self, user, unseen_messages=True):
|
||||
md_folders = [{"name" : "INBOX", "class" : "inbox"},
|
||||
{"name" : parameters.get_user(user, "DRAFTS_FOLDER"),
|
||||
"class" : "drafts"},
|
||||
{"name" : 'Junk'},
|
||||
{"name" : parameters.get_user(user, "SENT_FOLDER")},
|
||||
{"name" : parameters.get_user(user, "TRASH_FOLDER"),
|
||||
"class" : "trash"}]
|
||||
self._listfolders(folders=md_folders)
|
||||
|
||||
if unseen_messages:
|
||||
for fd in md_folders:
|
||||
key = fd.has_key("path") and "path" or "name"
|
||||
count = self.unseen_messages(fd[key])
|
||||
if count == 0:
|
||||
continue
|
||||
fd["unseen"] = count
|
||||
return md_folders
|
||||
|
||||
def _add_flag(self, folder, mail_id, flag):
|
||||
self.m.select(self._encodefolder(folder))
|
||||
self.m.store(mail_id, "+FLAGS", flag)
|
||||
|
||||
def msg_unread(self, folder, msgset):
|
||||
self.m.select(self._encodefolder(folder))
|
||||
self.m.store(msgset, "-FLAGS", r'(\Seen)')
|
||||
|
||||
def msg_read(self, folder, msgset):
|
||||
self._add_flag(folder, msgset, r'(\Seen)')
|
||||
|
||||
def msgforwarded(self, folder, imapid):
|
||||
self._add_flag(folder, imapid, '($Forwarded)')
|
||||
|
||||
def msg_answered(self, folder, imapid):
|
||||
self._add_flag(folder, imapid, r'(\Answered)')
|
||||
|
||||
def move(self, msgset, oldfolder, newfolder):
|
||||
self.m.select(self._encodefolder(oldfolder))
|
||||
status, data = self.m.copy(msgset, self._encodefolder(newfolder))
|
||||
if status == 'OK':
|
||||
self.m.store(msgset, "+FLAGS", r'(\Deleted)')
|
||||
|
||||
def push_mail(self, folder, msg):
|
||||
now = imaplib.Time2Internaldate(time.time())
|
||||
self.m.append(self._encodefolder(folder), r'(\Seen)', now, str(msg))
|
||||
|
||||
def empty(self, folder):
|
||||
self.m.select(self._encodefolder(folder))
|
||||
typ, data = self.m.search(None, 'ALL')
|
||||
for num in data[0].split():
|
||||
self.m.store(num, "+FLAGS", r'(\Deleted)')
|
||||
self.m.expunge()
|
||||
|
||||
def compact(self, folder):
|
||||
self.m.select(self._encodefolder(folder))
|
||||
self.m.expunge()
|
||||
|
||||
def create_folder(self, name, parent=None):
|
||||
if parent is not None:
|
||||
name = "%s.%s" % (parent, name)
|
||||
typ, data = self.m.create(self._encodefolder(name))
|
||||
if typ == "NO":
|
||||
raise WebmailError(data[0])
|
||||
return True
|
||||
|
||||
def rename_folder(self, oldname, newname):
|
||||
typ, data = self.m.rename(self._encodefolder(oldname),
|
||||
self._encodefolder(newname))
|
||||
if typ == "NO":
|
||||
raise WebmailError(data[0], ajax=True)
|
||||
return True
|
||||
|
||||
def delete_folder(self, name):
|
||||
typ, data = self.m.delete(self._encodefolder(name))
|
||||
if typ == "NO":
|
||||
raise WebmailError(data[0])
|
||||
return True
|
||||
|
||||
def getquota(self, folder):
|
||||
if not "QUOTA" in self.m.capabilities:
|
||||
self.quota_limit = self.quota_actual = None
|
||||
return
|
||||
|
||||
status, data = self.m.getquotaroot(self._encodefolder(folder))
|
||||
if status == "OK" and data[1][0] is not None:
|
||||
quotadef = data[1][0]
|
||||
m = re.match("[^\s]+ \(STORAGE (\d+) (\d+)\)", quotadef)
|
||||
if not m:
|
||||
print "Problem while parsing quota def"
|
||||
return
|
||||
self.quota_limit = int(m.group(2))
|
||||
self.quota_actual = int(m.group(1))
|
||||
|
||||
def fetchpart(self, uid, folder, part):
|
||||
self.m.select(self._encodefolder(folder), True)
|
||||
typ, data = self.m.fetch(uid, "(BODY[%(p)s.MIME] BODY[%(p)s])" \
|
||||
% {"p" : part})
|
||||
if typ != "OK":
|
||||
return None
|
||||
msg = email.message_from_string(data[0][1] + data[1][1])
|
||||
return msg
|
||||
|
||||
def fetch(self, start=None, stop=None, folder=None, all=False, **kwargs):
|
||||
if not start and not stop:
|
||||
return []
|
||||
result = []
|
||||
self.m.select(self._encodefolder(folder), True)
|
||||
if start and stop:
|
||||
submessages = self.messages[start - 1:stop]
|
||||
range = ",".join(submessages)
|
||||
else:
|
||||
submessages = [start]
|
||||
range = start
|
||||
if not all:
|
||||
query = '(FLAGS BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)])'
|
||||
else:
|
||||
query = '(RFC822)'
|
||||
typ, data = self.m.fetch(range, query)
|
||||
if not folder:
|
||||
folder = "INBOX"
|
||||
tmpdict = {}
|
||||
|
||||
for response_part in data:
|
||||
if isinstance(response_part, tuple):
|
||||
imapid = response_part[0].split()[0]
|
||||
flags = imaplib.ParseFlags(response_part[0])
|
||||
msg = email.message_from_string(response_part[1])
|
||||
msg["imapid"] = imapid
|
||||
if not "\\Seen" in flags:
|
||||
msg["class"] = "unseen"
|
||||
if "\\Answered" in flags:
|
||||
msg["img_flags"] = static_url("pics/answered.png")
|
||||
tmpdict[imapid] = msg
|
||||
for id in submessages:
|
||||
result += [tmpdict[id]]
|
||||
return result
|
||||
|
||||
class ImapListing(EmailListing):
|
||||
tpl = "webmail/index.html"
|
||||
tbltype = WMtable
|
||||
@@ -393,8 +110,9 @@ class ImapListing(EmailListing):
|
||||
if kwargs.has_key("pattern"):
|
||||
self.parse_search_parameters(kwargs["criteria"],
|
||||
kwargs["pattern"])
|
||||
if kwargs.has_key("reset"):
|
||||
else:
|
||||
self.mbc.criterions = []
|
||||
|
||||
EmailListing.__init__(self, **kwargs)
|
||||
self.extravars["refreshrate"] = \
|
||||
int(parameters.get_user(user, "REFRESH_INTERVAL")) * 1000
|
||||
@@ -430,12 +148,40 @@ class ImapListing(EmailListing):
|
||||
return ImapListing.computequota(self.mbc)
|
||||
|
||||
class ImapEmail(Email):
|
||||
def __init__(self, msg, user, dformat="DISPLAYMODE", addrfull=False,
|
||||
*args, **kwargs):
|
||||
mformat = parameters.get_user(user, "DISPLAYMODE")
|
||||
dformat = parameters.get_user(user, dformat)
|
||||
Email.__init__(self, msg, mformat, dformat, *args, **kwargs)
|
||||
def __init__(self, mbox, mailid, request, dformat="DISPLAYMODE", addrfull=False,
|
||||
links=0):
|
||||
mformat = parameters.get_user(request.user, "DISPLAYMODE")
|
||||
dformat = parameters.get_user(request.user, dformat)
|
||||
|
||||
self.headers = []
|
||||
self.attachments = {}
|
||||
self.imapc = get_imapconnector(request)
|
||||
msg = self.imapc.fetchmail(mbox, mailid)
|
||||
self.mbox = mbox
|
||||
self.mailid = mailid
|
||||
headers = msg['BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)]']
|
||||
fallback_fmt = "html" if dformat == "plain" else "plain"
|
||||
|
||||
self.bs = BodyStructure(msg['BODYSTRUCTURE'])
|
||||
data = None
|
||||
|
||||
mformat = dformat if self.bs.contents.has_key(dformat) else fallback_fmt
|
||||
|
||||
pnum = self.bs.contents[mformat]['pnum']
|
||||
data = self.imapc._cmd("FETCH", mailid, "(BODY.PEEK[%s])" % pnum)
|
||||
content = self._decode_content(self.bs.contents[mformat]['encoding'],
|
||||
data[int(mailid)]['BODY[%s]' % pnum])
|
||||
charset = self._find_content_charset(mformat)
|
||||
if charset is not None:
|
||||
content = content.decode(charset)
|
||||
|
||||
self._find_attachments()
|
||||
self._fetch_inlines()
|
||||
|
||||
self.body = \
|
||||
getattr(self, "viewmail_%s" % mformat)(content, links=links)
|
||||
|
||||
msg = email.message_from_string(headers)
|
||||
fields = ["Subject", "From", "To", "Reply-To", "Cc", "Date"]
|
||||
for f in fields:
|
||||
label = f
|
||||
@@ -455,6 +201,68 @@ class ImapEmail(Email):
|
||||
except:
|
||||
pass
|
||||
|
||||
def _find_content_charset(self, subtype):
|
||||
for pos, elem in enumerate(self.bs.contents[subtype]["params"]):
|
||||
if elem == "charset":
|
||||
return self.bs.contents[subtype]["params"][pos + 1]
|
||||
return None
|
||||
|
||||
def _decode_content(self, encoding, content):
|
||||
encoding = encoding.lower()
|
||||
if encoding == "base64":
|
||||
import base64
|
||||
return base64.b64decode(content)
|
||||
elif encoding == "quoted-printable":
|
||||
import quopri
|
||||
return quopri.decodestring(content)
|
||||
return content
|
||||
|
||||
def _find_attachments(self):
|
||||
for att in self.bs.attachments:
|
||||
attname = "part_%s" % att["pnum"]
|
||||
params = None
|
||||
key = None
|
||||
if att.has_key("params") and att["params"] != "NIL":
|
||||
params = att["params"]
|
||||
key = "name"
|
||||
|
||||
if key is None and \
|
||||
att.has_key("disposition") and len(att["disposition"]) > 1:
|
||||
params = att["disposition"][1]
|
||||
key = "filename"
|
||||
|
||||
if key and params:
|
||||
for pos, value in enumerate(params):
|
||||
if value == key:
|
||||
attname = u2u_decode.u2u_decode(params[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("\.\.", cid):
|
||||
continue
|
||||
fname = "static/tmp/%s_%s" % (self.mailid, cid)
|
||||
path = os.path.join(settings.MODOBOA_DIR, fname)
|
||||
params["fname"] = "/%s" % fname
|
||||
if os.path.exists(path):
|
||||
continue
|
||||
|
||||
content = self.imapc.fetchpart(self.mailid, self.mbox, params["pnum"])
|
||||
fp = open(path, "wb")
|
||||
fp.write(self._decode_content(params["encoding"], content))
|
||||
fp.close()
|
||||
|
||||
|
||||
def map_cid(self, url):
|
||||
import re
|
||||
|
||||
m = re.match(".*cid:(.+)", url)
|
||||
if m:
|
||||
if self.bs.inlines.has_key(m.group(1)):
|
||||
return self.bs.inlines[m.group(1)]["fname"]
|
||||
return url
|
||||
|
||||
def render_headers(self, **kwargs):
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
# coding: utf-8
|
||||
|
||||
from modoboa.lib.webutils import ajax_response
|
||||
from modoboa.extensions.webmail.lib import WebmailError
|
||||
from modoboa.lib.webutils import ajax_response, _render_error
|
||||
from modoboa.extensions.webmail.exceptions import *
|
||||
|
||||
class WebmailErrorMiddleware(object):
|
||||
def process_exception(self, request, exception):
|
||||
if not isinstance(exception, WebmailError):
|
||||
if not isinstance(exception, (WebmailError, ImapError)):
|
||||
return None
|
||||
return ajax_response(request, status="ko",
|
||||
respmsg=str(exception), norefresh=True)
|
||||
json = request.GET.get("json", False) if request.method == "GET" \
|
||||
else request.POST.get("json", False)
|
||||
if json:
|
||||
return ajax_response(request, status="ko",
|
||||
respmsg=str(exception), norefresh=True)
|
||||
return _render_error(request, user_context=dict(error=str(exception)))
|
||||
|
||||
@@ -12,6 +12,14 @@ html > body #listing {
|
||||
bottom: 35px;
|
||||
}
|
||||
|
||||
body.loading {
|
||||
display: block;
|
||||
filter: alpha(opacity=50);
|
||||
opacity: 0.5;
|
||||
z-index: 3;
|
||||
background: #FFF url("../../css/spinner.gif") center center no-repeat;
|
||||
}
|
||||
|
||||
.clearfix:after {
|
||||
content: ".";
|
||||
display: block;
|
||||
@@ -230,3 +238,16 @@ textarea:hover {
|
||||
.folder_menubar {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.dragbox {
|
||||
position : absolute;
|
||||
opacity : 0.8;
|
||||
width : 200px;
|
||||
padding : 5px;
|
||||
text-align : center;
|
||||
height : 25px;
|
||||
border : 1px solid #333;
|
||||
background : white;
|
||||
display: none;
|
||||
z-index : 3;
|
||||
}
|
||||
@@ -1,3 +1,660 @@
|
||||
var Webmail = new Class({
|
||||
Implements: [Options],
|
||||
|
||||
/*
|
||||
* Default values for options
|
||||
*/
|
||||
defaults: {
|
||||
refreshRate: 0,
|
||||
modify_url: "",
|
||||
delete_url: "",
|
||||
move_url: "",
|
||||
submboxes_url: ""
|
||||
},
|
||||
|
||||
/*
|
||||
* Constructor
|
||||
*/
|
||||
initialize: function(options) {
|
||||
this.setOptions(options);
|
||||
|
||||
this.rtimer = null;
|
||||
this.editorid = "id_body";
|
||||
|
||||
this.init_folders_buttons();
|
||||
this.init_folders_browsing();
|
||||
|
||||
/* FIXME: à faire uniquement pour le mode pushState */
|
||||
/*this.init_emails_table();*/
|
||||
|
||||
current_anchor.disable_spinner();
|
||||
current_anchor.register_callback("listmailbox",
|
||||
this.listmailbox_callback.bind(this));
|
||||
current_anchor.register_callback("viewmail", this.viewmail_callback.bind(this));
|
||||
current_anchor.register_callback("compose", this.compose_callback.bind(this));
|
||||
current_anchor.register_callback("reply", this.compose_callback.bind(this));
|
||||
current_anchor.register_callback("replyall", this.compose_callback.bind(this));
|
||||
current_anchor.register_callback("forward", this.compose_callback.bind(this));
|
||||
|
||||
this.gspinner = new Spinner("document.body", {
|
||||
message: gettext("Loading, please wait..."),
|
||||
'class': "spinner-noop"
|
||||
});
|
||||
this.gspinner.addEvent("hide", function(target) {
|
||||
this.gspinner.destroy();
|
||||
delete(this.gspinner);
|
||||
current_anchor.enable_spinner();
|
||||
}.bind(this));
|
||||
this.gspinner.show();
|
||||
},
|
||||
|
||||
send_mark_request: function(evt) {
|
||||
evt.stop();
|
||||
if (!this.emails_table._selectedRows.length) {
|
||||
return;
|
||||
}
|
||||
var ids = new Array();
|
||||
|
||||
this.emails_table._selectedRows.each(function(item, index) {
|
||||
ids.include(item.get("id"));
|
||||
});
|
||||
this.simple_request(evt.target.get("href"), {
|
||||
ids : ids.join()
|
||||
}, function(response) {
|
||||
if (response.action == "read") {
|
||||
this.emails_table._selectedRows.removeClass("unseen");
|
||||
} else {
|
||||
this.emails_table._selectedRows.addClass("unseen");
|
||||
}
|
||||
/*
|
||||
* MAJ de l'affichage du nombre de messages non lus
|
||||
* (faire fonction à part)
|
||||
*/
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/*
|
||||
* Open/Close the menu containing folder buttons
|
||||
*/
|
||||
toggle_folder_buttons: function(evt) {
|
||||
var parent = evt.target.getParent();
|
||||
var fdname = parent.getFirst("a[name=loadfolder]").get("href");
|
||||
|
||||
this.fdmenu.toggle(evt.target, fdname);
|
||||
evt.stop();
|
||||
},
|
||||
|
||||
/*
|
||||
* Initializes the misc. buttons available to manipulate folders.
|
||||
*/
|
||||
init_folders_buttons: function() {
|
||||
$$("li[class*=clickable]").removeEvents();
|
||||
|
||||
$$("a[name=loadfolder]").addEvent("click", this.listmailbox_loader.bind(this));
|
||||
|
||||
SqueezeBox.assign($("folders").getElements('a[class=boxed]'), {
|
||||
parse: 'rel'
|
||||
});
|
||||
|
||||
this.fdmenu = new FdMenu({
|
||||
modify_url: this.options.modify_url,
|
||||
delete_url: this.options.delete_url
|
||||
});
|
||||
|
||||
$$("img[class*=footer]")
|
||||
.addEvent("click", this.toggle_folder_buttons.bind(this));
|
||||
},
|
||||
|
||||
/*
|
||||
* Open/Close a mailbox with children
|
||||
*/
|
||||
toggle_mbox_state: function(div, ul) {
|
||||
if (ul.hasClass("hidden")) {
|
||||
div.removeClass("collapsed").addClass("expanded");
|
||||
ul.removeClass("hidden").addClass("visible");
|
||||
} else {
|
||||
div.removeClass("expanded").addClass("collapsed");
|
||||
ul.removeClass("visible").addClass("hidden");
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Inject new mailboxes under a given parent in the tree.
|
||||
*/
|
||||
inject_mailboxes: function(parent, mboxes) {
|
||||
var ul = new Element("ul", {
|
||||
name: parent.get("name"),
|
||||
'class': "hidden"
|
||||
}).inject(parent);
|
||||
|
||||
for (var i = 0; i < mboxes.length; i++) {
|
||||
var mbname = mboxes[i].name;
|
||||
var li = new Element("li", {
|
||||
name: mbname,
|
||||
'class': "droppable folder"
|
||||
}).inject(ul);
|
||||
var img = new Element("img", {
|
||||
'class': "footer",
|
||||
src: static_url("pics/go-down.png")
|
||||
}).inject(li);
|
||||
var link = new Element("a", {
|
||||
name: "loadfolder",
|
||||
'class': "block",
|
||||
href: mbname
|
||||
}).inject(li);
|
||||
var parts = mbname.split(".");
|
||||
var displayname = parts[parts.length - 1];
|
||||
|
||||
if (mboxes[i].unseen != undefined) {
|
||||
link.addClass("unseen");
|
||||
link.set("html", displayname + " (" + mboxes[i].unseen + ")");
|
||||
} else {
|
||||
link.set("html", displayname);
|
||||
}
|
||||
|
||||
link.addEvent("click", this.listmailbox_loader.bind(this));
|
||||
img.addEvent("click", this.toggle_folder_buttons.bind(this));
|
||||
}
|
||||
this.init_draggables();
|
||||
this.toggle_mbox_state(parent.getFirst("div"), ul);
|
||||
},
|
||||
|
||||
/*
|
||||
* Download sub-mailboxes from the server
|
||||
*/
|
||||
get_mailboxes: function(parent) {
|
||||
new Request.JSON({
|
||||
url: this.options.submboxes_url,
|
||||
onSuccess: function(resp) {
|
||||
if (resp.status == "ko") {
|
||||
return;
|
||||
}
|
||||
this.inject_mailboxes(parent, resp.mboxes);
|
||||
}.bind(this)
|
||||
}).get({topmailbox: parent.get("name")});
|
||||
},
|
||||
|
||||
/*
|
||||
* Enables folders opening/closing
|
||||
*
|
||||
* Available for folders with subfolders.
|
||||
*/
|
||||
init_folders_browsing: function() {
|
||||
$$("div[class*=clickbox]").addEvent("click", function(evt) {
|
||||
var div = evt.target;
|
||||
var parent = div.getParent();
|
||||
var ul = parent.getFirst("ul[name=" + parent.get("name") + "]");
|
||||
|
||||
if (ul == undefined) {
|
||||
this.get_mailboxes(parent);
|
||||
return;
|
||||
}
|
||||
this.toggle_mbox_state(div, ul);
|
||||
evt.stop();
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/*
|
||||
* Select a particular folder
|
||||
*/
|
||||
select_folder: function(obj) {
|
||||
$$("a[name=loadfolder]").each(function(item) {
|
||||
item.getParent().removeClass("selected");
|
||||
item.getParent().addClass("droppable");
|
||||
});
|
||||
obj.getParent().removeClass("droppable");
|
||||
obj.getParent().addClass("selected");
|
||||
|
||||
$$(obj.getParents("ul[class*=hidden]")).each(function(item) {
|
||||
item.removeClass("hidden");
|
||||
item.addClass("visible");
|
||||
$$("li[name=" + item.get("name") + "]").each(function(item) {
|
||||
item.setStyle(
|
||||
"background",
|
||||
"url('" + static_url("pics/folder-open.png") + "') no-repeat"
|
||||
);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/*
|
||||
* Automatic folder listing refresh
|
||||
*
|
||||
* Called at a regular frequency to make an asynchronous request that
|
||||
* retrieves a folder's content (ie. mails)
|
||||
*/
|
||||
refresh_folder: function() {
|
||||
var query = current_anchor.serialized.substring(1);
|
||||
|
||||
clearInterval(this.rtimer);
|
||||
infobox.show(gettext("Reloading..."), {
|
||||
profile : "gray",
|
||||
spinner : true
|
||||
});
|
||||
new Request.JSON({url: query, noCache : true, onSuccess: function(resp) {
|
||||
var callback = ($defined(resp.callback)) ? resp.callback : "default";
|
||||
|
||||
current_anchor.get_callback(callback)(resp);
|
||||
infobox.info(gettext("Done"));
|
||||
infobox.hide(1);
|
||||
}}).get();
|
||||
},
|
||||
|
||||
init_automatic_refresh: function() {
|
||||
this.rtimer = this.refresh_folder
|
||||
.periodical(this.options.refreshRate);
|
||||
},
|
||||
|
||||
/*
|
||||
* Emails table initialization.
|
||||
*/
|
||||
init_emails_table: function() {
|
||||
this.emails_table = new HtmlTable($("emails"), {
|
||||
selectable: true,
|
||||
shiftForMultiSelect: true
|
||||
}).addEvents({
|
||||
rowFocus: function(tr) {
|
||||
clearInterval(this.rtimer);
|
||||
}.bind(this),
|
||||
rowUnfocus: function(tr) {
|
||||
if (this.emails_table._selectedRows.length == 0) {
|
||||
this.init_automatic_refresh();
|
||||
}
|
||||
}.bind(this)
|
||||
});
|
||||
|
||||
/* The IE way for disabling default selection */
|
||||
/* Actually it doesn't work :p */
|
||||
$$("tbody>tr").set("unselectable", "on");
|
||||
|
||||
if ($("toggleselect")) {
|
||||
$("toggleselect").addEvent("click", function(evt) {
|
||||
if (evt.target.checked) {
|
||||
this.emails_table.selectAll();
|
||||
} else {
|
||||
this.emails_table.selectNone();
|
||||
}
|
||||
evt.stopPropagation();
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
init_sortable_columns();
|
||||
update_sortable_column();
|
||||
|
||||
this.init_draggables();
|
||||
|
||||
$$("tbody>tr").addEvent("dblclick", this.viewmail_loader);
|
||||
},
|
||||
|
||||
/*
|
||||
* Drag & Drop feature
|
||||
*/
|
||||
init_draggables: function(pattern) {
|
||||
if (this.dragobjs == undefined) {
|
||||
this.dragobjs = new Array();
|
||||
} else {
|
||||
this.dragobjs.each(function(item) {
|
||||
item.detach();
|
||||
delete(item);
|
||||
});
|
||||
this.dragobjs = new Array();
|
||||
}
|
||||
|
||||
$$("td[class*=draggable]").each(function(item) {
|
||||
var pid = item.getParent().get("id");
|
||||
|
||||
this.dragobjs[pid] = item.makeDraggable({
|
||||
droppables: ".droppable"
|
||||
}).addEvents({
|
||||
beforeStart: this.bstart_dragging.bind(this),
|
||||
start: this.start_dragging.bind(this),
|
||||
enter: this.enter_droppable,
|
||||
leave: this.leave_droppable,
|
||||
drop: this.drop_element.bind(this),
|
||||
cancel: this.cancel_dragging.bind(this)
|
||||
});
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* 'before start' event callback
|
||||
*/
|
||||
bstart_dragging: function(item) {
|
||||
var dragobj = this.dragobjs[item.getParent().get("id")];
|
||||
var block = new Element("div", {id: "draggeditems", 'class': "dragbox"})
|
||||
.setStyles({
|
||||
top : dragobj.element.getCoordinates()['top'],
|
||||
left : dragobj.element.getCoordinates()['left']
|
||||
}).inject(document.body);
|
||||
|
||||
block.store("parent", dragobj.element);
|
||||
dragobj.element = block;
|
||||
clearInterval(this.rtimer);
|
||||
},
|
||||
|
||||
start_dragging: function(item, event) {
|
||||
var tr = event.target.getParent();
|
||||
|
||||
if (!this.emails_table.isSelected(tr)) {
|
||||
this.emails_table.selectNone();
|
||||
this.emails_table.selectRow(tr);
|
||||
}
|
||||
item.set("html", "Moving " + this.emails_table._selectedRows.length + " messages");
|
||||
item.setStyle("display", "block");
|
||||
},
|
||||
|
||||
enter_droppable: function(item, droppable) {
|
||||
droppable.addClass("dragging");
|
||||
},
|
||||
|
||||
leave_droppable: function(item, droppable) {
|
||||
droppable.removeClass("dragging");
|
||||
},
|
||||
|
||||
restore_parent: function(item) {
|
||||
var parent = item.retrieve("parent");
|
||||
var drag = this.dragobjs[parent.getParent().get("id")];
|
||||
|
||||
drag.element = parent;
|
||||
this.init_automatic_refresh();
|
||||
},
|
||||
|
||||
drop_element: function(item, droppable, event) {
|
||||
item.dispose();
|
||||
if (!droppable) {
|
||||
this.restore_parent(item);
|
||||
return;
|
||||
}
|
||||
droppable.removeClass("dragging");
|
||||
var dest = gethref(droppable.getFirst("a"));
|
||||
var msgset = new Array();
|
||||
|
||||
this.emails_table._selectedRows.each(function(item, index) {
|
||||
msgset.include(item.get("id"));
|
||||
});
|
||||
this.simple_request(this.options.move_url, {
|
||||
msgset : msgset.join(),
|
||||
to : dest
|
||||
});
|
||||
},
|
||||
|
||||
cancel_dragging: function(item) {
|
||||
item.dispose();
|
||||
this.restore_parent(item);
|
||||
},
|
||||
|
||||
/*
|
||||
* Bottom left quota bar initialisation.
|
||||
*/
|
||||
init_quota_bar: function() {
|
||||
this.quota_pb = new ProgressBar({
|
||||
container: $("quotabox"),
|
||||
startPercentage: 0,
|
||||
boxID: "box",
|
||||
percentageID: "perc",
|
||||
displayText: true,
|
||||
displayID: "quotatext"
|
||||
});
|
||||
},
|
||||
|
||||
/*
|
||||
*
|
||||
*/
|
||||
page_update: function(response) {
|
||||
if (this.gspinner) {
|
||||
this.gspinner.hide();
|
||||
}
|
||||
updatelisting(response);
|
||||
},
|
||||
|
||||
/*
|
||||
* Webmail actions start here
|
||||
*/
|
||||
|
||||
/*
|
||||
* Onclick callback used to load the content of a particular
|
||||
* folder. (activated when clicking on a folder's name)
|
||||
*/
|
||||
_listmailbox_loader: function(event, obj) {
|
||||
event.stop();
|
||||
if (obj == undefined) {
|
||||
obj = $(event.target);
|
||||
}
|
||||
current_anchor.reset().setparams({
|
||||
action: "listmailbox",
|
||||
name: gethref(obj)
|
||||
}).update();
|
||||
},
|
||||
|
||||
listmailbox_loader: function(event) {
|
||||
this.select_folder(event.target);
|
||||
this._listmailbox_loader(event, event.target);
|
||||
},
|
||||
|
||||
/*
|
||||
* Callback for 'folder' action
|
||||
*/
|
||||
listmailbox_callback: function(resp) {
|
||||
window.removeEvents("resize");
|
||||
this.page_update(resp);
|
||||
if (resp.menu != undefined) {
|
||||
searchbox_init();
|
||||
$$("a[name=compose]").addEvent("click", this.compose_loader);
|
||||
$$("a[name=mark-read]")
|
||||
.addEvent("click", this.send_mark_request.bind(this));
|
||||
$$("a[name=mark-unread]")
|
||||
.addEvent("click", this.send_mark_request.bind(this));
|
||||
$$("a[name=fdaction]").addEvent("click", function(evt) {
|
||||
this.simple_request(evt.target.get("href"), {});
|
||||
evt.stop();
|
||||
}.bind(this));
|
||||
}
|
||||
this.init_emails_table();
|
||||
},
|
||||
|
||||
/*
|
||||
* Loader of the 'viemmail' action
|
||||
*/
|
||||
viewmail_loader: function(evt) {
|
||||
evt.stop();
|
||||
current_anchor.reset().setparams({
|
||||
action: "viewmail",
|
||||
mailid: this.get("id")
|
||||
}).update();
|
||||
},
|
||||
|
||||
/*
|
||||
* Callback of the 'viewmail' action
|
||||
*/
|
||||
viewmail_callback: function(resp) {
|
||||
/*wm_updatelisting(resp, "hidden");*/
|
||||
this.page_update(resp);
|
||||
|
||||
//$$("a[name=back]").addEvent("click", this._listmailbox_loader);
|
||||
$$("a[name=reply]").addEvent("click", this.reply_loader);
|
||||
$$("a[name=replyall]").addEvent("click", this.replyall_loader);
|
||||
$$("a[name=forward]").addEvent("click", this.forward_loader);
|
||||
$$("a[name=delete]").addEvent("click", function(event) {
|
||||
var lnk = event.target;
|
||||
event.stop();
|
||||
this.simple_request(lnk.get("href"));
|
||||
}.bind(this));
|
||||
$$("a[name=activate_links]").addEvent("click", function(evt) {
|
||||
evt.stop();
|
||||
current_anchor.setparam("links", "1").update();
|
||||
});
|
||||
$$("a[name=disable_links]").addEvent("click", function(evt) {
|
||||
evt.stop();
|
||||
current_anchor.setparam("links", "0").update();
|
||||
});
|
||||
setDivHeight("mailcontent", 5, 0);
|
||||
},
|
||||
|
||||
/*
|
||||
* Loader of the 'compose' action (called when the associated button
|
||||
* is clicked)
|
||||
*/
|
||||
compose_loader: function(event) {
|
||||
event.stop();
|
||||
current_anchor.reset().setparam("action", this.get("href")).update();
|
||||
},
|
||||
|
||||
/*
|
||||
* Loader of the 'reply' action (called when the associated button
|
||||
* is clicked).
|
||||
*/
|
||||
reply_loader: function(event) {
|
||||
event.stop();
|
||||
location.hash += event.target.get("href");
|
||||
},
|
||||
|
||||
/*
|
||||
* Loader of the 'replyall' action (called when the associated button
|
||||
* is clicked).
|
||||
*/
|
||||
replyall_loader: function(event) {
|
||||
event.stop();
|
||||
location.hash += event.target.get("href") + "?all=1";
|
||||
},
|
||||
|
||||
/*
|
||||
* Loader of the 'forward' action (called when the associated button
|
||||
* is clicked).
|
||||
*/
|
||||
forward_loader: function(event) {
|
||||
event.stop();
|
||||
location.hash += event.target.get("href");
|
||||
},
|
||||
|
||||
/*
|
||||
* Resize the current editor on page resizing.
|
||||
*/
|
||||
resize_window_callback: function(event) {
|
||||
CKEDITOR.instances[this.editorid]
|
||||
.resize("100%", $("body_container").getSize().y);
|
||||
},
|
||||
|
||||
/*
|
||||
* Callback of the 'compose' action.
|
||||
*
|
||||
* It is also shared with other similar actions : reply, forward.
|
||||
*/
|
||||
compose_callback: function(resp) {
|
||||
//wm_updatelisting(resp);
|
||||
this.page_update(resp);
|
||||
|
||||
window.addEvent("resize", this.resize_window_callback.bind(this));
|
||||
|
||||
var editormode = resp.editor;
|
||||
|
||||
if (editormode == "html") {
|
||||
var instance = CKEDITOR.instances[editorid];
|
||||
if (instance) {
|
||||
CKEDITOR.remove(instance);
|
||||
}
|
||||
CKEDITOR.replace(editorid, {
|
||||
customConfig: static_url("js/editor_config.js")
|
||||
});
|
||||
CKEDITOR.on("instanceReady", function(evt) {
|
||||
this.resize_window_callback();
|
||||
}.bind(this));
|
||||
}
|
||||
if (resp.id) {
|
||||
current_anchor.setparam("id", resp.id).update(0, 1);
|
||||
}
|
||||
$("attachments").addEvent("click", function(evt) {
|
||||
SqueezeBox.open(this.get("name"), {
|
||||
handler: "iframe",
|
||||
size: {x: 350, y: 400},
|
||||
closeBtn: false
|
||||
});
|
||||
});
|
||||
|
||||
/*$$("a[name=back]")
|
||||
.removeEvents("click")
|
||||
.addEvent("click", this._listmailbox_loader);*/
|
||||
$$("a[name=sendmail]")
|
||||
.store("editormode", editormode)
|
||||
.removeEvents("click")
|
||||
.addEvent("click", this.sendmail_callback.bind(this));
|
||||
},
|
||||
|
||||
/*
|
||||
* Callback of the 'sendmail' action.
|
||||
*/
|
||||
sendmail_callback: function(evt) {
|
||||
evt.stop();
|
||||
infobox.show(gettext("Sending..."), {
|
||||
profile : "gray",
|
||||
spinner : true
|
||||
});
|
||||
disable_link(this);
|
||||
$("composemail").set("send", {
|
||||
onSuccess: function(resp) {
|
||||
resp = JSON.decode(resp);
|
||||
if (resp.status == "ko") {
|
||||
current_anchor.get_callback("compose")(resp);
|
||||
if ($defined(resp.respmsg)) {
|
||||
infobox.error(resp.respmsg);
|
||||
}
|
||||
enable_link(this, sendmail_callback);
|
||||
return;
|
||||
}
|
||||
current_anchor.parse_string(resp.url, true).setparams(navparams);
|
||||
current_anchor.update();
|
||||
this.eliminate("editormode");
|
||||
}.bind(this)
|
||||
});
|
||||
if (this.retrieve("editormode") == "html") {
|
||||
CKEDITOR.instances[editorid].updateElement();
|
||||
}
|
||||
$("composemail").send();
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* Simple AJAX request
|
||||
*
|
||||
* Initialize the infobox with a loading message and wait for a JSON
|
||||
* encoded answer. The infobox is cleared by the request callback
|
||||
* function.
|
||||
*/
|
||||
simple_request: function(url, params, callback) {
|
||||
infobox.show(gettext("Waiting..."), {
|
||||
profile: "gray",
|
||||
spinner: true
|
||||
});
|
||||
params["json"] = "1";
|
||||
new Request.JSON({
|
||||
url : url,
|
||||
onSuccess : function(response) {
|
||||
if (response.status == "ok") {
|
||||
if (response.next == undefined) {
|
||||
if (callback != undefined) {
|
||||
callback(response);
|
||||
} else {
|
||||
current_anchor.get_callback("listmailbox")(response);
|
||||
}
|
||||
}
|
||||
infobox.info(gettext("Done"));
|
||||
if ($defined(response.next)) {
|
||||
(function(response) {
|
||||
current_anchor.parse_string(response.next).update();
|
||||
select_folder($$("a[href=" + current_anchor.getbaseurl() + "]"));
|
||||
}).delay(500, this, response);
|
||||
} else {
|
||||
infobox.hide(1);
|
||||
}
|
||||
} else {
|
||||
infobox.error(response.respmsg);
|
||||
}
|
||||
}
|
||||
}).get(params);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/*
|
||||
* Globals
|
||||
*/
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<script type="text/javascript" src="{{ MEDIA_URL }}webmail/js/webmail.js"></script>
|
||||
{{ block.super }}
|
||||
<script type="text/javascript">
|
||||
|
||||
function wm_updatelisting(resp) {
|
||||
window.removeEvents("resize");
|
||||
updatelisting(resp);
|
||||
@@ -49,7 +50,7 @@ function wm_updatelisting(resp) {
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block js %}
|
||||
quota_pb = new ProgressBar({
|
||||
/*quota_pb = new ProgressBar({
|
||||
container: $("quotabox"),
|
||||
startPercentage: 0,
|
||||
boxID: "box",
|
||||
@@ -69,6 +70,14 @@ function wm_updatelisting(resp) {
|
||||
clearInterval(rtimer);
|
||||
rtimer = null;
|
||||
}
|
||||
});*/
|
||||
|
||||
new Webmail({
|
||||
refreshRate: {{ refreshrate }},
|
||||
modify_url: "{% url extensions.webmail.views.editfolder %}",
|
||||
delete_url: "{% url extensions.webmail.views.delfolder %}",
|
||||
move_url: "{% url extensions.webmail.views.move %}",
|
||||
submboxes_url: "{% url extensions.webmail.views.submailboxes %}"
|
||||
});
|
||||
|
||||
current_anchor.register_callback("folder", function(resp) {
|
||||
@@ -211,9 +220,9 @@ function wm_updatelisting(resp) {
|
||||
{% endblock %}
|
||||
{% block middletitle %}{% trans "Webmail" %}{% endblock %}
|
||||
{% block leftcol %}
|
||||
<div id="folders" class="box">
|
||||
</div>
|
||||
<div id="folders" class="box">{{ mboxes }}</div>
|
||||
{% endblock %}
|
||||
{% block menu %}{{ menu }}{% endblock %}
|
||||
{% block footer %}
|
||||
<div id="quotalabel">{% trans "Mailbox quota" %}</div>
|
||||
<div id="quotabox"></div>
|
||||
|
||||
@@ -11,10 +11,10 @@ from modoboa.lib.webutils import static_url
|
||||
register = template.Library()
|
||||
|
||||
@register.simple_tag
|
||||
def viewm_menu(selection, backurl, folder, mail_id, user):
|
||||
def viewmail_menu(selection, folder, user, mail_id=None):
|
||||
entries = [
|
||||
{"name" : "back",
|
||||
"url" : backurl,
|
||||
"url" : "javascript:history.go(-1);",
|
||||
"img" : static_url("pics/back.png"),
|
||||
"label" : _("Back")},
|
||||
{"name" : "reply",
|
||||
@@ -40,10 +40,12 @@ def viewm_menu(selection, backurl, folder, mail_id, user):
|
||||
"menu" : [
|
||||
{"name" : "activate_links",
|
||||
"label" : _("Activate links"),
|
||||
"url" : reverse(webmail.views.viewmail, args=[folder, mail_id]) + "?links=1"},
|
||||
"url" : ""},
|
||||
#reverse(webmail.views.viewmail, args=[folder, mail_id]) + "?links=1"},
|
||||
{"name" : "disable_links",
|
||||
"label" : _("Disable links"),
|
||||
"url" : reverse(webmail.views.viewmail, args=[folder, mail_id]) + "?links=0"},
|
||||
"url" : ""}
|
||||
#reverse(webmail.views.viewmail, args=[folder, mail_id]) + "?links=0"},
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -56,7 +58,7 @@ def viewm_menu(selection, backurl, folder, mail_id, user):
|
||||
def compose_menu(selection, backurl, user):
|
||||
entries = [
|
||||
{"name" : "back",
|
||||
"url" : backurl,
|
||||
"url" : "javascript:history.go(-2);",#backurl,
|
||||
"img" : static_url("pics/back.png"),
|
||||
"label" : _("Back")},
|
||||
{"name" : "sendmail",
|
||||
@@ -69,10 +71,10 @@ def compose_menu(selection, backurl, user):
|
||||
"user" : user})
|
||||
|
||||
@register.simple_tag
|
||||
def listing_menu(selection, folder, user):
|
||||
def listmailbox_menu(selection, folder, user):
|
||||
entries = [
|
||||
{"name" : "compose",
|
||||
"url" : reverse(webmail.views.compose),
|
||||
"url" : "compose",
|
||||
"img" : static_url("pics/edit.png"),
|
||||
"label" : _("New message")},
|
||||
{"name" : "mark",
|
||||
@@ -127,7 +129,7 @@ def print_folders(folders, selected=None, withunseen=False, withmenu=False):
|
||||
if selected == name:
|
||||
cssclass += " selected"
|
||||
result += "<li name='%s' class='droppable %s'>\n" % (name, cssclass)
|
||||
if fd.has_key("sub") and len(fd["sub"]):
|
||||
if fd.has_key("sub"):
|
||||
if selected is not None and selected != name and selected.count(name):
|
||||
ul_state = "visible"
|
||||
div_state = "expanded"
|
||||
|
||||
@@ -6,8 +6,13 @@ from django.conf.urls.defaults import *
|
||||
# "attachments"), the associated folder will not be accessible.
|
||||
#
|
||||
urlpatterns = patterns('modoboa.extensions.webmail.views',
|
||||
(r'^$', "index"),
|
||||
(r'^compose/$', "compose"),
|
||||
#(r'^$', "index"),
|
||||
(r'^$', "newindex"),
|
||||
(r'^submailboxes', "submailboxes"),
|
||||
(r'^mailcontent', "getmailcontent"),
|
||||
|
||||
|
||||
# (r'^compose/$', "compose"),
|
||||
(r'^move/$', "move"),
|
||||
(r'^mark/(?P<name>.+)/$', "mark"),
|
||||
(r'^empty/(?P<name>.+)/$', "empty"),
|
||||
@@ -21,9 +26,9 @@ urlpatterns = patterns('modoboa.extensions.webmail.views',
|
||||
|
||||
(r'^(?P<folder>.+)/(?P<mail_id>[\w\-\+]+)/part/$', 'getattachment'),
|
||||
(r'^(?P<fdname>.+)/(?P<mail_id>[\w\-\+]+)/delete/$', 'delete'),
|
||||
(r'^(?P<folder>.+)/(?P<mail_id>[\w\-\+]+)/content/$', 'getmailcontent'),
|
||||
#(r'^(?P<folder>.+)/(?P<mailid>[\w\-\+]+)/content/$', 'getmailcontent'),
|
||||
(r'^(?P<folder>.+)/(?P<mail_id>[\w\-\+]+)/forward/$', 'forward'),
|
||||
(r'^(?P<folder>.+)/(?P<mail_id>[\w\-\+]+)/reply/$', 'reply'),
|
||||
(r'^(?P<folder>.+)/(?P<mail_id>[\w\-\+]+)/$', 'viewmail'),
|
||||
# (r'^(?P<folder>.+)/(?P<mail_id>[\w\-\+]+)/$', 'viewmail'),
|
||||
(r'^(?P<name>.+)/$', "folder"),
|
||||
)
|
||||
|
||||
+300
-110
@@ -11,14 +11,14 @@ from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import messages
|
||||
from modoboa.lib import parameters
|
||||
from modoboa.lib.webutils import _render, _render_error, \
|
||||
getctx, _render_to_string, ajax_response
|
||||
getctx, _render_to_string, ajax_response, ajax_simple_response
|
||||
from modoboa.lib.emailutils import split_mailbox
|
||||
from modoboa.lib.email_listing import parse_search_parameters, Paginator
|
||||
from modoboa.lib.decorators import needs_mailbox
|
||||
from modoboa.auth.lib import *
|
||||
from lib import *
|
||||
from forms import *
|
||||
from templatetags.webextras import *
|
||||
from templatetags import webextras
|
||||
|
||||
def __render_common_components(request, folder_name, lst=None, content=None, menu=None):
|
||||
"""Render all components that are common to all pages in the webmail
|
||||
@@ -123,51 +123,55 @@ def index(request):
|
||||
return _render_error(request, user_context={"error" : exp})
|
||||
return lst.render(request)
|
||||
|
||||
def fetchmail(request, folder, mail_id, all=False):
|
||||
res = IMAPconnector(user=request.user.username,
|
||||
password=request.session["password"]).fetch(start=mail_id,
|
||||
folder=folder,
|
||||
all=all)
|
||||
if len(res):
|
||||
return res[0]
|
||||
return None
|
||||
def fetchmail(request, mbox, mailid, all=False):
|
||||
imapc = get_imapconnector(request)
|
||||
res = imapc.fetchmail(mbox, mailid)
|
||||
|
||||
return res
|
||||
# res = IMAPconnector(user=request.user.username,
|
||||
# password=request.session["password"]).fetch(start=mail_id,
|
||||
# folder=folder,
|
||||
# all=all)
|
||||
# if len(res):
|
||||
# return res[0]
|
||||
# return None
|
||||
|
||||
# @login_required
|
||||
# @is_not_localadmin()
|
||||
# def viewmail(request, folder, mail_id=None):
|
||||
# from templatetags.webextras import viewm_menu
|
||||
|
||||
# if request.GET.has_key("links"):
|
||||
# links = int(request.GET["links"])
|
||||
# else:
|
||||
# links = parameters.get_user(request.user, "ENABLE_LINKS") == "yes" and 1 or 0
|
||||
# url = reverse(getmailcontent, args=[folder, mail_id]) + ("?links=%d" % links)
|
||||
# content = Template("""
|
||||
# <iframe width="100%" frameBorder="0" src="{{ url }}" id="mailcontent"></iframe>
|
||||
# """).render(Context({"url" : url}))
|
||||
# menu = viewm_menu("", get_current_url(request), folder, mail_id,
|
||||
# request.user.get_all_permissions())
|
||||
# mbc = IMAPconnector(user=request.user.username,
|
||||
# password=request.session["password"])
|
||||
# ctx = getctx("ok", **__render_common_components(request, folder,
|
||||
# menu=menu, content=content))
|
||||
# return HttpResponse(simplejson.dumps(ctx), mimetype="application/json")
|
||||
|
||||
@login_required
|
||||
@needs_mailbox()
|
||||
def viewmail(request, folder, mail_id=None):
|
||||
from templatetags.webextras import viewm_menu
|
||||
|
||||
if request.GET.has_key("links"):
|
||||
links = int(request.GET["links"])
|
||||
else:
|
||||
links = parameters.get_user(request.user, "ENABLE_LINKS") == "yes" and 1 or 0
|
||||
url = reverse(getmailcontent, args=[folder, mail_id]) + ("?links=%d" % links)
|
||||
content = Template("""
|
||||
<iframe width="100%" frameBorder="0" src="{{ url }}" id="mailcontent"></iframe>
|
||||
""").render(Context({"url" : url}))
|
||||
menu = viewm_menu("", get_current_url(request), folder, mail_id,
|
||||
request.user.get_all_permissions())
|
||||
mbc = IMAPconnector(user=request.user.username,
|
||||
password=request.session["password"])
|
||||
ctx = getctx("ok", **__render_common_components(request, folder,
|
||||
menu=menu, content=content))
|
||||
return HttpResponse(simplejson.dumps(ctx), mimetype="application/json")
|
||||
#content = fetchmail(request, mbox, mailid, True)
|
||||
|
||||
@login_required
|
||||
@needs_mailbox()
|
||||
def getmailcontent(request, folder, mail_id):
|
||||
msg = fetchmail(request, folder, mail_id, True)
|
||||
if "class" in msg.keys() and msg["class"] == "unseen":
|
||||
IMAPconnector(user=request.user.username,
|
||||
password=request.session["password"]).msg_read(folder, mail_id)
|
||||
email = ImapEmail(msg, request.user, links=request.GET["links"])
|
||||
try:
|
||||
pageid = request.session["page"]
|
||||
except KeyError:
|
||||
pageid = "1"
|
||||
# if "class" in msg.keys() and msg["class"] == "unseen":
|
||||
# IMAPconnector(user=request.user.username,
|
||||
# password=request.session["password"]).msg_read(folder, mail_id)
|
||||
|
||||
email = ImapEmail(mbox, mailid, request, links=request.GET["links"])
|
||||
return _render(request, "common/viewmail.html", {
|
||||
"headers" : email.render_headers(folder=folder, mail_id=mail_id),
|
||||
"folder" : folder, "imapid" : mail_id, "mailbody" : email.body
|
||||
"headers" : email.render_headers(folder=mbox, mail_id=mailid),
|
||||
"folder" : mbox, "imapid" : mailid, "mailbody" : email.body
|
||||
})
|
||||
|
||||
@login_required
|
||||
@@ -210,11 +214,12 @@ def getattachment(request, folder, mail_id):
|
||||
def move(request):
|
||||
for arg in ["msgset", "to"]:
|
||||
if not request.GET.has_key(arg):
|
||||
return
|
||||
mbc = IMAPconnector(user=request.user.username,
|
||||
password=request.session["password"])
|
||||
mbc.move(request.GET["msgset"], request.session["folder"], request.GET["to"])
|
||||
return folder(request, request.session["folder"], False)
|
||||
raise WebmailError(_("Invalid request"))
|
||||
mbc = get_imapconnector(request)
|
||||
mbc.move(request.GET["msgset"], request.session["mbox"], request.GET["to"])
|
||||
resp = listmailbox(request, request.session["mbox"])
|
||||
resp.update(status="ok")
|
||||
return ajax_simple_response(resp)
|
||||
|
||||
@login_required
|
||||
@needs_mailbox()
|
||||
@@ -228,87 +233,89 @@ def delete(request, fdname, mail_id):
|
||||
@login_required
|
||||
@needs_mailbox()
|
||||
def mark(request, name):
|
||||
if not request.GET.has_key("status") or not request.GET.has_key("ids"):
|
||||
return
|
||||
mbc = IMAPconnector(user=request.user.username,
|
||||
password=request.session["password"])
|
||||
status = request.GET.get("status", None)
|
||||
ids = request.GET.get("ids", None)
|
||||
if status is None or ids is None:
|
||||
raise WebmailError(_("Invalid request"))
|
||||
imapc = get_imapconnector(request)
|
||||
try:
|
||||
getattr(mbc, "msg_%s" % request.GET["status"])(name, request.GET["ids"])
|
||||
getattr(imapc, "mark_messages_%s" % status)(name, ids)
|
||||
except AttributeError:
|
||||
pass
|
||||
return folder(request, name, False)
|
||||
raise WebmailError(_("Unknown action"))
|
||||
|
||||
return ajax_simple_response(
|
||||
dict(status="ok", action=status, mbox=name,
|
||||
unseen=imapc.unseen_messages(name))
|
||||
)
|
||||
|
||||
@login_required
|
||||
@needs_mailbox()
|
||||
def empty(request, name):
|
||||
if name == parameters.get_user(request.user, "TRASH_FOLDER"):
|
||||
mbc = IMAPconnector(user=request.user.username,
|
||||
password=request.session["password"])
|
||||
mbc.empty(name)
|
||||
return folder(request, name, False)
|
||||
get_imapconnector(request).empty(name)
|
||||
return ajax_simple_response(dict(status="ok"))
|
||||
|
||||
@login_required
|
||||
@needs_mailbox()
|
||||
def compact(request, name):
|
||||
mbc = IMAPconnector(user=request.user.username,
|
||||
password=request.session["password"])
|
||||
mbc.compact(name)
|
||||
return folder(request, name, False)
|
||||
imapc = get_imapconnector(request)
|
||||
imapc.compact(name)
|
||||
return ajax_simple_response(dict(status="ok"))
|
||||
|
||||
def render_compose(request, form, posturl, email=None, insert_signature=False):
|
||||
menu = compose_menu("", get_current_url(request),
|
||||
request.user.get_all_permissions())
|
||||
editor = parameters.get_user(request.user, "EDITOR")
|
||||
if email is None:
|
||||
body = ""
|
||||
textheader = ""
|
||||
else:
|
||||
body = email.body
|
||||
textheader = email.textheader
|
||||
if insert_signature:
|
||||
signature = EmailSignature(request.user)
|
||||
body += str(signature)
|
||||
randid = None
|
||||
if not request.GET.has_key("id"):
|
||||
if request.session.has_key("compose_mail"):
|
||||
clean_attachments(request.session["compose_mail"]["attachments"])
|
||||
randid = set_compose_session(request)
|
||||
elif not request.session.has_key("compose_mail") \
|
||||
or request.session["compose_mail"]["id"] != request.GET["id"]:
|
||||
randid = set_compose_session(request)
|
||||
# def render_compose(request, form, posturl, email=None, insert_signature=False):
|
||||
# menu = compose_menu("", get_current_url(request),
|
||||
# request.user.get_all_permissions())
|
||||
# editor = parameters.get_user(request.user, "EDITOR")
|
||||
# if email is None:
|
||||
# body = ""
|
||||
# textheader = ""
|
||||
# else:
|
||||
# body = email.body
|
||||
# textheader = email.textheader
|
||||
# if insert_signature:
|
||||
# signature = EmailSignature(request.user)
|
||||
# body += str(signature)
|
||||
# randid = None
|
||||
# if not request.GET.has_key("id"):
|
||||
# if request.session.has_key("compose_mail"):
|
||||
# clean_attachments(request.session["compose_mail"]["attachments"])
|
||||
# randid = set_compose_session(request)
|
||||
# elif not request.session.has_key("compose_mail") \
|
||||
# or request.session["compose_mail"]["id"] != request.GET["id"]:
|
||||
# randid = set_compose_session(request)
|
||||
|
||||
attachments = request.session["compose_mail"]["attachments"]
|
||||
if len(attachments):
|
||||
short_att_list = "(%s)" \
|
||||
% ", ".join(map(lambda att: att["fname"],
|
||||
attachments[:2] + [{"fname" : "..."}] \
|
||||
if len(attachments) > 2 else attachments))
|
||||
else:
|
||||
short_att_list = ""
|
||||
content = _render_to_string(request, "webmail/compose.html", {
|
||||
"form" : form, "bodyheader" : textheader,
|
||||
"body" : body, "posturl" : posturl,
|
||||
"attachments" : attachments, "short_att_list" : short_att_list
|
||||
})
|
||||
mbc = IMAPconnector(user=request.user.username,
|
||||
password=request.session["password"])
|
||||
ctx = getctx("ok", level=2, editor=editor,
|
||||
**__render_common_components(request, request.session["folder"],
|
||||
menu=menu, content=content))
|
||||
if randid is not None:
|
||||
ctx["id"] = randid
|
||||
return HttpResponse(simplejson.dumps(ctx), mimetype="application/json")
|
||||
# attachments = request.session["compose_mail"]["attachments"]
|
||||
# if len(attachments):
|
||||
# short_att_list = "(%s)" \
|
||||
# % ", ".join(map(lambda att: att["fname"],
|
||||
# attachments[:2] + [{"fname" : "..."}] \
|
||||
# if len(attachments) > 2 else attachments))
|
||||
# else:
|
||||
# short_att_list = ""
|
||||
# content = _render_to_string(request, "webmail/compose.html", {
|
||||
# "form" : form, "bodyheader" : textheader,
|
||||
# "body" : body, "posturl" : posturl,
|
||||
# "attachments" : attachments, "short_att_list" : short_att_list
|
||||
# })
|
||||
# mbc = IMAPconnector(user=request.user.username,
|
||||
# password=request.session["password"])
|
||||
# ctx = getctx("ok", level=2, editor=editor,
|
||||
# **__render_common_components(request, request.session["folder"],
|
||||
# menu=menu, content=content))
|
||||
# if randid is not None:
|
||||
# ctx["id"] = randid
|
||||
# return HttpResponse(simplejson.dumps(ctx), mimetype="application/json")
|
||||
|
||||
@login_required
|
||||
@needs_mailbox()
|
||||
def compose(request):
|
||||
if request.method == "POST":
|
||||
status, resp = send_mail(request, posturl=reverse(compose))
|
||||
return resp
|
||||
# @login_required
|
||||
# @is_not_localadmin()
|
||||
# def compose(request):
|
||||
# if request.method == "POST":
|
||||
# status, resp = send_mail(request, posturl=reverse(compose))
|
||||
# return resp
|
||||
|
||||
form = ComposeMailForm()
|
||||
form.fields["from_"].initial = request.user.username
|
||||
return render_compose(request, form, reverse(compose), insert_signature=True)
|
||||
# form = ComposeMailForm()
|
||||
# form.fields["from_"].initial = request.user.username
|
||||
# return render_compose(request, form, reverse(compose), insert_signature=True)
|
||||
|
||||
@login_required
|
||||
@needs_mailbox()
|
||||
@@ -496,3 +503,186 @@ def delattachment(request):
|
||||
if error is None:
|
||||
error = _("Unknown attachment")
|
||||
return ajax_response(request, "ko", respmsg=error)
|
||||
|
||||
#
|
||||
# NEW CODE STARTS HERE
|
||||
#
|
||||
def render_mboxes_list(request, imapc):
|
||||
"""Return the HTML representation of a mailboxes list
|
||||
|
||||
:param request: a ``Request`` object
|
||||
:param imap: an ``IMAPconnector` object
|
||||
:return: a string
|
||||
"""
|
||||
curmbox = request.session.get("mbox", "INBOX")
|
||||
return _render_to_string(request, "webmail/folders.html", {
|
||||
"titlebar" : True,
|
||||
"selected" : curmbox,
|
||||
"folders" : imapc.getfolders(request.user),
|
||||
"withmenu" : True,
|
||||
"withunseen" : True
|
||||
})
|
||||
|
||||
def set_nav_params(request):
|
||||
if not request.session.has_key("navparams"):
|
||||
request.session["navparams"] = {}
|
||||
|
||||
request.session["pageid"] = \
|
||||
int(request.GET.get("page", 1))
|
||||
if not request.GET.get("order", False):
|
||||
if not request.session["navparams"].has_key("order"):
|
||||
request.session["navparams"]["order"] = "-date"
|
||||
else:
|
||||
request.session["navparams"]["order"] = request.GET.get("order")
|
||||
|
||||
for p in ["pattern", "criteria"]:
|
||||
if request.GET.get(p, False):
|
||||
request.session["navparams"][p] = request.GET[p]
|
||||
elif request.session["navparams"].has_key(p):
|
||||
del request.session["navparams"][p]
|
||||
|
||||
def listmailbox(request, defmailbox="INBOX"):
|
||||
"""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
|
||||
"""
|
||||
mbox = request.GET.get("name", defmailbox)
|
||||
request.session["mbox"] = mbox
|
||||
set_nav_params(request)
|
||||
|
||||
lst = ImapListing(request.user, request.session["password"],
|
||||
baseurl="?action=listmailbox&name=%s&" % mbox,
|
||||
folder=mbox, **request.session["navparams"])
|
||||
|
||||
return lst.render(request, request.session["pageid"])
|
||||
|
||||
def render_compose(request, form, posturl, email=None, insert_signature=False):
|
||||
editor = parameters.get_user(request.user, "EDITOR")
|
||||
if email is None:
|
||||
body = ""
|
||||
textheader = ""
|
||||
else:
|
||||
body = email.body
|
||||
textheader = email.textheader
|
||||
if insert_signature:
|
||||
signature = EmailSignature(request.user)
|
||||
body += str(signature)
|
||||
randid = None
|
||||
if not request.GET.has_key("id"):
|
||||
if request.session.has_key("compose_mail"):
|
||||
clean_attachments(request.session["compose_mail"]["attachments"])
|
||||
randid = set_compose_session(request)
|
||||
elif not request.session.has_key("compose_mail") \
|
||||
or request.session["compose_mail"]["id"] != request.GET["id"]:
|
||||
randid = set_compose_session(request)
|
||||
|
||||
attachments = request.session["compose_mail"]["attachments"]
|
||||
if len(attachments):
|
||||
short_att_list = "(%s)" \
|
||||
% ", ".join(map(lambda att: att["fname"],
|
||||
attachments[:2] + [{"fname" : "..."}] \
|
||||
if len(attachments) > 2 else attachments))
|
||||
else:
|
||||
short_att_list = ""
|
||||
content = _render_to_string(request, "webmail/compose.html", {
|
||||
"form" : form, "bodyheader" : textheader,
|
||||
"body" : body, "posturl" : posturl,
|
||||
"attachments" : attachments, "short_att_list" : short_att_list
|
||||
})
|
||||
|
||||
ctx = dict(listing=content, editor=editor)
|
||||
if randid is not None:
|
||||
ctx["id"] = randid
|
||||
return ctx
|
||||
|
||||
def compose(request):
|
||||
form = ComposeMailForm()
|
||||
form.fields["from_"].initial = request.user.username
|
||||
return render_compose(request, form, "compose", insert_signature=True)
|
||||
|
||||
def viewmail(request):
|
||||
mailid = request.GET.get("mailid", None)
|
||||
if mailid is None:
|
||||
raise WebmailError(_("Invalid request"))
|
||||
|
||||
links = 1
|
||||
url = reverse(getmailcontent) + "?mbox=%s&mailid=%s&links=%d" % \
|
||||
(request.session["mbox"], mailid, links)
|
||||
content = Template("""
|
||||
<iframe width="100%" frameBorder="0" src="{{ url }}" id="mailcontent"></iframe>
|
||||
""").render(Context({"url" : url}))
|
||||
|
||||
return dict(listing=content, menuargs=dict(mail_id=mailid))
|
||||
|
||||
@login_required
|
||||
@is_not_localadmin()
|
||||
def submailboxes(request):
|
||||
topmailbox = request.GET.get('topmailbox', '')
|
||||
mboxes = get_imapconnector(request).getfolders(request.user, topmailbox)
|
||||
return ajax_simple_response(dict(status="ok", mboxes=mboxes))
|
||||
|
||||
@login_required
|
||||
@is_not_localadmin()
|
||||
def newindex(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)
|
||||
json = request.GET.get("json", False)
|
||||
|
||||
if action is not None:
|
||||
if not globals().has_key(action):
|
||||
raise WebmailError(_("Undefined action"))
|
||||
response = globals()[action](request)
|
||||
else:
|
||||
if json:
|
||||
raise WebmailError(_("Bad request"))
|
||||
response = dict(deflocation="?action=listmailbox",
|
||||
defcallback="wm_updatelisting")
|
||||
|
||||
curmbox = request.session.get("mbox", "INBOX")
|
||||
if not json:
|
||||
request.session["lastaction"] = None
|
||||
imapc = get_imapconnector(request)
|
||||
response["refreshrate"] = \
|
||||
int(parameters.get_user(request.user, "REFRESH_INTERVAL")) * 1000
|
||||
response["mboxes"] = render_mboxes_list(request, imapc)
|
||||
return _render(request, "webmail/index.html", response)
|
||||
|
||||
if request.session["lastaction"] != action:
|
||||
extra_args = {}
|
||||
if response.has_key("menuargs"):
|
||||
extra_args = response["menuargs"]
|
||||
del response["menuargs"]
|
||||
try:
|
||||
response["menu"] = \
|
||||
getattr(webextras, "%s_menu" % action)("", curmbox, request.user, **extra_args)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
response.update(status="ok", callback=action)
|
||||
return ajax_simple_response(response)
|
||||
|
||||
+31
-23
@@ -115,7 +115,7 @@ class EmailListing(object):
|
||||
def fetch(self, request, id_start, id_stop):
|
||||
table = self.tbltype(request,
|
||||
self.mbc.fetch(start=id_start, stop=id_stop,
|
||||
folder=self.folder,
|
||||
mbox=self.folder,
|
||||
nbelems=self.elems_per_page))
|
||||
tpl = Template("""
|
||||
<form method="POST" id="listingform">
|
||||
@@ -123,29 +123,37 @@ class EmailListing(object):
|
||||
</form>""")
|
||||
return tpl.render(Context({"table" : table.render()}))
|
||||
|
||||
# def render(self, request, pageid=1, **kwargs):
|
||||
# listing = ""
|
||||
# page = None
|
||||
# if not self.empty:
|
||||
# page = self.paginator.getpage(pageid)
|
||||
# if not page:
|
||||
# listing = _("This folder contains no messages")
|
||||
# else:
|
||||
# listing = self.fetch(request, page.id_start, page.id_stop)
|
||||
# elapsed = kwargs.has_key("start") and time.time() - kwargs["start"] or 0
|
||||
# context = {
|
||||
# "listing" : listing,
|
||||
# "elapsed" : elapsed,
|
||||
# "navbar" : self.render_navbar(page),
|
||||
# "selection" : self.folder,
|
||||
# "navparams" : self.navparams,
|
||||
# "deflocation" : self.deflocation,
|
||||
# "defcallback" : self.defcallback,
|
||||
# "reset_wm_url" : self.reset_wm_url
|
||||
# }
|
||||
# for k, v in self.extravars.iteritems():
|
||||
# context[k] = v
|
||||
# return _render(request, self.tpl, context)
|
||||
|
||||
def render(self, request, pageid=1, **kwargs):
|
||||
listing = ""
|
||||
page = None
|
||||
if not self.empty:
|
||||
page = self.paginator.getpage(pageid)
|
||||
if not page:
|
||||
listing = _("This folder contains no messages")
|
||||
else:
|
||||
listing = self.fetch(request, page.id_start, page.id_stop)
|
||||
elapsed = kwargs.has_key("start") and time.time() - kwargs["start"] or 0
|
||||
context = {
|
||||
"listing" : listing,
|
||||
"elapsed" : elapsed,
|
||||
"navbar" : self.render_navbar(page),
|
||||
"selection" : self.folder,
|
||||
"navparams" : self.navparams,
|
||||
"deflocation" : self.deflocation,
|
||||
"defcallback" : self.defcallback,
|
||||
"reset_wm_url" : self.reset_wm_url
|
||||
}
|
||||
for k, v in self.extravars.iteritems():
|
||||
context[k] = v
|
||||
return _render(request, self.tpl, context)
|
||||
page = self.paginator.getpage(pageid)
|
||||
if not page:
|
||||
listing = _("This folder contains no message")
|
||||
else:
|
||||
listing = self.fetch(request, page.id_start, page.id_stop)
|
||||
return dict(listing=listing, navbar=self.render_navbar(page))
|
||||
|
||||
def parse_search_parameters(request):
|
||||
if request.GET.has_key("pattern"):
|
||||
|
||||
+1
-1
@@ -166,7 +166,7 @@ class Email(object):
|
||||
|
||||
def viewmail_html(self, content, **kwargs):
|
||||
if content is None or content == "":
|
||||
return (False, "")
|
||||
return ""
|
||||
links = kwargs.has_key("links") and kwargs["links"] or "0"
|
||||
html = lxml.html.fromstring(content)
|
||||
if links == "0":
|
||||
|
||||
@@ -225,7 +225,7 @@ div.right {
|
||||
.logo {
|
||||
float: right;
|
||||
/*margin: 5px 0 0 5px;*/
|
||||
height: 90px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.5 KiB |
+50
-20
@@ -9,7 +9,9 @@ var AnchorNavigation = new Class({
|
||||
checkinterval: 300,
|
||||
defloadingtext: gettext("Loading..."),
|
||||
defloadingcolor: "gray",
|
||||
defcallback: null
|
||||
defcallback: null,
|
||||
spin_disabled: false,
|
||||
spin_target: undefined
|
||||
},
|
||||
|
||||
initialize: function(deflocation, options) {
|
||||
@@ -29,13 +31,24 @@ var AnchorNavigation = new Class({
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Disable the spinner.
|
||||
*/
|
||||
disable_spinner: function() {
|
||||
this.options.spin_disabled = true;
|
||||
},
|
||||
|
||||
enable_spinner: function() {
|
||||
this.options.spin_disabled = false;
|
||||
},
|
||||
|
||||
reset_loading_infos: function() {
|
||||
this.loading_message = this.options.defloadingtext;
|
||||
this.loading_color = this.options.defloadingcolor;
|
||||
},
|
||||
|
||||
reset: function() {
|
||||
this.base = null;
|
||||
this.base = '';
|
||||
this.params.empty();
|
||||
return this;
|
||||
},
|
||||
@@ -53,11 +66,13 @@ var AnchorNavigation = new Class({
|
||||
if (reset) {
|
||||
this.reset();
|
||||
}
|
||||
this.base = (splits[0] == "") ? this.deflocation : splits[0];
|
||||
this.base = splits[0];
|
||||
|
||||
var re = new RegExp("/$");
|
||||
if (!this.base.match(re)) {
|
||||
this.base += '/';
|
||||
if (this.base != "") {
|
||||
var re = new RegExp("/$");
|
||||
if (!this.base.match(re)) {
|
||||
this.base += '/';
|
||||
}
|
||||
}
|
||||
if (splits.length > 1) {
|
||||
var params = splits[1].split('&');
|
||||
@@ -112,6 +127,7 @@ var AnchorNavigation = new Class({
|
||||
for (var i = 0; i < elems.length; i++) {
|
||||
this.setparamfromstring(elems[i]);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
setparamfromstring: function(str) {
|
||||
@@ -182,16 +198,14 @@ var AnchorNavigation = new Class({
|
||||
},
|
||||
|
||||
check_anchor: function() {
|
||||
if (this.serialized == location.hash
|
||||
&& !$defined(this.force)) {
|
||||
if (this.serialized == location.hash && this.force == undefined) {
|
||||
return;
|
||||
}
|
||||
delete(this.force);
|
||||
this.from_string(location.hash);
|
||||
if (!this.serialized) {
|
||||
location.hash = this.deflocation;
|
||||
return;
|
||||
}
|
||||
|
||||
this.from_string((location.hash != "") ? location.hash : "#" + this.deflocation);
|
||||
location.hash = this.serialized;
|
||||
|
||||
if (!this.updatenext) {
|
||||
this.updatenext = true;
|
||||
return;
|
||||
@@ -199,10 +213,17 @@ var AnchorNavigation = new Class({
|
||||
|
||||
var query = this.serialized.substring(1);
|
||||
|
||||
infobox.show(this.loading_message, {
|
||||
profile: this.loading_color,
|
||||
spinner: true
|
||||
});
|
||||
if (!this.options.spin_disabled) {
|
||||
var target = this.options.spin_target ? this.options.spin_target
|
||||
: document.body;
|
||||
$(target).spin({
|
||||
message: this.loading_message
|
||||
});
|
||||
/*infobox.show(this.loading_message, {
|
||||
profile: this.loading_color,
|
||||
spinner: true
|
||||
});*/
|
||||
}
|
||||
new Request.JSON({
|
||||
url: query,
|
||||
noCache : true,
|
||||
@@ -210,16 +231,25 @@ var AnchorNavigation = new Class({
|
||||
ajax_login_redirect();
|
||||
},
|
||||
onSuccess: function(resp) {
|
||||
if (!this.options.spin_disabled) {
|
||||
var target = this.options.spin_target ? this.options.spin_target
|
||||
: document.body;
|
||||
$(target).unspin();
|
||||
}
|
||||
if (resp.status == "ko") {
|
||||
infobox.error(resp.respmsg);
|
||||
return;
|
||||
}
|
||||
var callback = ($defined(resp.callback)) ? resp.callback : "default";
|
||||
this.callbacks[callback](resp);
|
||||
infobox.info(gettext("Done"));
|
||||
infobox.hide(1);
|
||||
/*infobox.info(gettext("Done"));
|
||||
infobox.hide(1);*/
|
||||
this.reset_loading_infos();
|
||||
}.bind(this),
|
||||
onFailure: function(xhr) {
|
||||
$(document.body).set("html", xhr.responseText);
|
||||
$(document.body).setStyle("overflow", "auto");
|
||||
}
|
||||
}).get();
|
||||
}).get("json=1");
|
||||
}
|
||||
});
|
||||
|
||||
+16
-11
@@ -153,8 +153,10 @@ checkTableSelection = function(text) {
|
||||
return confirm(text);
|
||||
};
|
||||
|
||||
searchbox_init = function() {
|
||||
function searchbox_init() {
|
||||
var myFx = new Fx.Slide($("searchparams"));
|
||||
var $searchfield = $("searchfield");
|
||||
|
||||
myFx.hide();
|
||||
$("searchopts").addEvent("click", function(event) {
|
||||
myFx.cancel();
|
||||
@@ -164,28 +166,31 @@ searchbox_init = function() {
|
||||
myFx.cancel();
|
||||
myFx.toggle();
|
||||
});
|
||||
$("searchfield").addEvent("change", function(event) {
|
||||
$searchfield.addEvent("change", function(event) {
|
||||
var criteria = "";
|
||||
var pattern = this.get("value");
|
||||
|
||||
$$("input[name=scriteria]:checked").each(function(obj) {
|
||||
criteria = obj.value;
|
||||
});
|
||||
var pattern = $("searchfield").get("value");
|
||||
if (pattern != "") {
|
||||
var params = "pattern=" + pattern + "&criteria=" + criteria;
|
||||
current_anchor.updateparams(params);
|
||||
current_anchor.setparams({
|
||||
pattern: pattern,
|
||||
criteria: criteria
|
||||
});
|
||||
} else {
|
||||
current_anchor.deleteParam("pattern");
|
||||
current_anchor.deleteParam("criteria");
|
||||
}
|
||||
current_anchor.update();
|
||||
});
|
||||
$("searchfield").addEvent("click", function(event) {
|
||||
$("searchfield").select();
|
||||
$searchfield.addEvent("click", function(event) {
|
||||
$searchfield.select();
|
||||
});
|
||||
|
||||
$("searchfield").addEvent("blur", function(event) {
|
||||
if ($("searchfield").value == "") {
|
||||
$("searchfield").set("value", gettext("Search..."));
|
||||
$searchfield.addEvent("blur", function(event) {
|
||||
if ($searchfield.value == "") {
|
||||
$searchfield.set("value", gettext("Search..."));
|
||||
}
|
||||
});
|
||||
$("searchbox").setStyle("float", "left");
|
||||
@@ -193,7 +198,7 @@ searchbox_init = function() {
|
||||
if (frag.indexOf('?') != -1) {
|
||||
var params = frag.substring(frag.indexOf('?') + 1).parseQueryString();
|
||||
if ($defined(params.pattern)) {
|
||||
$("searchfield").set("value", params.pattern);
|
||||
$searchfield.set("value", params.pattern);
|
||||
}
|
||||
if ($defined(params.criteria)) {
|
||||
$("crit_" + params.criteria).checked = true;
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
*/
|
||||
|
||||
function navbarcallback(event) {
|
||||
event.stop();
|
||||
current_anchor.setparams(navparams);
|
||||
current_anchor.parse_string($(event.target).getParent().get("href")).update();
|
||||
event.stop();
|
||||
current_anchor.reset()
|
||||
.setparams(navparams)
|
||||
.updateparams(this.get("href"))
|
||||
.update();
|
||||
}
|
||||
|
||||
function updatelisting(resp) {
|
||||
@@ -23,7 +25,7 @@ function updatelisting(resp) {
|
||||
}
|
||||
if ($defined(resp.navbar)) {
|
||||
$("navbar").set("html", resp.navbar);
|
||||
$("navbar").getChildren("a").addEvent("click", navbarcallback);
|
||||
$$("#navbar a").addEvent("click", navbarcallback);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
<script type="text/javascript" src="{{ MEDIA_URL }}js/email_listing.js"></script>
|
||||
{% block css %}{% endblock %}
|
||||
<script type="text/javascript">
|
||||
var current_anchor = new AnchorNavigation("{{ deflocation }}");
|
||||
var current_anchor = new AnchorNavigation("{{ deflocation }}", {
|
||||
spin_target: "page"
|
||||
});
|
||||
|
||||
function viewmail(event) {
|
||||
event.stop();
|
||||
@@ -56,7 +58,7 @@ function update_sortable_column() {
|
||||
|
||||
window.addEvent("domready", function() {
|
||||
navparams = new Hash({
|
||||
{% join navparams %}
|
||||
{% if navparams %}{% join navparams %}{% endif %}
|
||||
});
|
||||
{% block js %}{% endblock %}
|
||||
current_anchor.set_default_callback({{ defcallback }});
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
<label>{% trans "Messages" %} {{ page.id_start }} {% trans "to" %} {{ page.id_stop }} {% trans "of" %} {{ page.items }}</label>
|
||||
|
||||
{% if page.previous_page %}
|
||||
<a href="{{ page.baseurl }}?page=1">
|
||||
<a href="{{ page.baseurl }}page=1">
|
||||
<img src="{{ MEDIA_URL }}pics/first.png" border="0" title="{% trans "First" %}" />
|
||||
</a>
|
||||
<a href="{{ page.baseurl }}?page={{ page.previous_page }}">
|
||||
<a href="{{ page.baseurl }}page={{ page.previous_page }}">
|
||||
<img src="{{ MEDIA_URL }}pics/back.png" border="0" title="{% trans "Previous" %}" />
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if page.next_page %}
|
||||
<a href="{{ page.baseurl }}?page={{ page.next_page }}">
|
||||
<a href="{{ page.baseurl }}page={{ page.next_page }}">
|
||||
<img src="{{ MEDIA_URL }}pics/next.png" border="0" title="{% trans "Next" %}" />
|
||||
</a>
|
||||
<a href="{{ page.baseurl }}?page={{ page.last_page }}">
|
||||
<a href="{{ page.baseurl }}page={{ page.last_page }}">
|
||||
<img src="{{ MEDIA_URL }}pics/last.png" border="0" title="{% trans "Last" %}" />
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<link rel="icon" type="image/png" href="{{ MEDIA_URL }}css/logo-icon.png" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/default.css" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/SqueezeBox.css" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/spinner.css" />
|
||||
<script src="{{ MEDIA_URL }}js/mootools.js" type="text/javascript"></script>
|
||||
<script src="{{ MEDIA_URL }}js/mootools-more.js" type="text/javascript"></script>
|
||||
<script src="{{ MEDIA_URL }}js/SqueezeBox.js" type="text/javascript"></script>
|
||||
|
||||
Reference in New Issue
Block a user