Merge with mainline

This commit is contained in:
Antoine Nguyen
2012-02-01 09:41:28 +01:00
21 changed files with 1985 additions and 490 deletions
+28
View File
@@ -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)
+173
View File
@@ -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)
+550
View File
@@ -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
View File
@@ -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
+9 -5
View File
@@ -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)))
+21
View File
@@ -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;
}
+657
View File
@@ -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>
+10 -8
View File
@@ -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"
+9 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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":
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+6 -4
View File
@@ -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);
}
}
+4 -2
View File
@@ -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 }});
+4 -4
View File
@@ -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 %}
+1
View File
@@ -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>