diff --git a/extensions/webmail/exceptions.py b/extensions/webmail/exceptions.py new file mode 100644 index 000000000..f05766371 --- /dev/null +++ b/extensions/webmail/exceptions.py @@ -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) diff --git a/extensions/webmail/imaputils.py b/extensions/webmail/imaputils.py new file mode 100644 index 000000000..1750ac7f6 --- /dev/null +++ b/extensions/webmail/imaputils.py @@ -0,0 +1,488 @@ +# 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 + +#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 IMAPconnector(object): + __metaclass__ = ConnectionsManager + + list_base_pattern = r'\((?P.*?)\) "(?P.*)" "(?P[^"]*)"' + list_response_pattern = re.compile(list_base_pattern) + listextended_response_pattern = \ + re.compile(list_base_pattern + r'\s*(?P.*)') + 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(bodystruct[0], mtype, stype, nprefix) + if index: + return (index, encoding, size) + cpt += 1 + + if bodystruct[0] == mtype and bodystruct[1] == 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, 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, 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 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') + result += [msg] + return result + + def fetchmail(self, mbox, mailid, fmt="plain"): + """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)])") + content = data[int(mailid)]['BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)]'] + + fallback_fmt = "html" if fmt == "plain" else "plain" + (pnum, encoding, size) = (None, None, 0) + for f in [fmt, fallback_fmt]: + pnum, encoding, size = \ + self.__find_content_in_bodystruct(data[int(mailid)]['BODYSTRUCTURE'], + "text", f) + if pnum is not None and size: + break + if pnum is None: + return None + + data = self._cmd("FETCH", mailid, "(BODY.PEEK[%s])" % pnum) + + data = data[int(mailid)]['BODY[%s]' % pnum] + if encoding == "base64": + import base64 + content += base64.b64decode(data) + elif encoding == "quoted-printable": + import quopri + content += quopri.decodestring(data) + else: + content += data + + return email.message_from_string(content) + +def get_imapconnector(request): + imapc = IMAPconnector(user=request.user.username, + password=request.session["password"]) + return imapc diff --git a/extensions/webmail/lib.py b/extensions/webmail/lib.py index dd3e759b3..09317b315 100644 --- a/extensions/webmail/lib.py +++ b/extensions/webmail/lib.py @@ -22,19 +22,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" @@ -108,274 +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', md_folders=[]): - list_response_pattern = re.compile(r'\((?P.*?)\) "(?P.*)" (?P.*)') - (status, data) = self.m.list() - result = [] - 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 - present = False - descr = {"name" : name} - for mdf in md_folders: - if mdf["name"] == name: - present = True - break - if not present: - result += [descr] - from operator import itemgetter - return sorted(result, 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"}] - md_folders += self._listfolders(md_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": - 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 @@ -389,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 diff --git a/extensions/webmail/main.py b/extensions/webmail/main.py index 9caaf085c..1f59baa52 100644 --- a/extensions/webmail/main.py +++ b/extensions/webmail/main.py @@ -89,7 +89,7 @@ def menu(**kwargs): return [ {"name" : "webmail", "label" : _("Webmail"), - "url" : reverse(views.index), + "url" : reverse(views.newindex), "img" : static_url("pics/webmail.png")} ] diff --git a/extensions/webmail/middleware.py b/extensions/webmail/middleware.py index 528f65a99..37884dcab 100644 --- a/extensions/webmail/middleware.py +++ b/extensions/webmail/middleware.py @@ -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))) diff --git a/extensions/webmail/static/css/webmail.css b/extensions/webmail/static/css/webmail.css index 450941351..7c9a7c60c 100644 --- a/extensions/webmail/static/css/webmail.css +++ b/extensions/webmail/static/css/webmail.css @@ -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; +} \ No newline at end of file diff --git a/extensions/webmail/static/js/webmail.js b/extensions/webmail/static/js/webmail.js index 30a56a111..09fd0c762 100644 --- a/extensions/webmail/static/js/webmail.js +++ b/extensions/webmail/static/js/webmail.js @@ -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 */ diff --git a/extensions/webmail/templates/webmail/index.html b/extensions/webmail/templates/webmail/index.html index aadd73756..691b5141a 100644 --- a/extensions/webmail/templates/webmail/index.html +++ b/extensions/webmail/templates/webmail/index.html @@ -8,6 +8,7 @@ {{ block.super }} {% 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 %} -
-
+
{{ mboxes }}
{% endblock %} +{% block menu %}{{ menu }}{% endblock %} {% block footer %}
{% trans "Mailbox quota" %}
diff --git a/extensions/webmail/templatetags/webextras.py b/extensions/webmail/templatetags/webextras.py index 7c8dd1fd4..7022acd7f 100644 --- a/extensions/webmail/templatetags/webextras.py +++ b/extensions/webmail/templatetags/webextras.py @@ -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 += "
  • \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" diff --git a/extensions/webmail/urls.py b/extensions/webmail/urls.py index e950fd222..23b026016 100644 --- a/extensions/webmail/urls.py +++ b/extensions/webmail/urls.py @@ -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.+)/$', "mark"), (r'^empty/(?P.+)/$', "empty"), @@ -21,9 +26,9 @@ urlpatterns = patterns('modoboa.extensions.webmail.views', (r'^(?P.+)/(?P[\w\-\+]+)/part/$', 'getattachment'), (r'^(?P.+)/(?P[\w\-\+]+)/delete/$', 'delete'), - (r'^(?P.+)/(?P[\w\-\+]+)/content/$', 'getmailcontent'), + #(r'^(?P.+)/(?P[\w\-\+]+)/content/$', 'getmailcontent'), (r'^(?P.+)/(?P[\w\-\+]+)/forward/$', 'forward'), (r'^(?P.+)/(?P[\w\-\+]+)/reply/$', 'reply'), - (r'^(?P.+)/(?P[\w\-\+]+)/$', 'viewmail'), +# (r'^(?P.+)/(?P[\w\-\+]+)/$', 'viewmail'), (r'^(?P.+)/$', "folder"), ) diff --git a/extensions/webmail/views.py b/extensions/webmail/views.py index 1a76a3a9e..327e72c99 100644 --- a/extensions/webmail/views.py +++ b/extensions/webmail/views.py @@ -12,14 +12,14 @@ from django.contrib import messages from modoboa.admin.models import Mailbox 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.admin.lib import is_not_localadmin 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 @@ -124,51 +124,58 @@ 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(""" +# +# """).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 @is_not_localadmin() -def viewmail(request, folder, mail_id=None): - from templatetags.webextras import viewm_menu +def getmailcontent(request): + mbox = request.GET.get("mbox", None) + mailid = request.GET.get("mailid", None) + if mbox is None or mailid is None: + raise WebmailError(_("Invalid request")) - 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(""" - -""").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 -@is_not_localadmin() -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(content, request.user, 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 @@ -211,11 +218,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 @is_not_localadmin() @@ -229,87 +237,89 @@ def delete(request, fdname, mail_id): @login_required @is_not_localadmin() 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 @is_not_localadmin() 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 @is_not_localadmin() 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 -@is_not_localadmin() -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 @is_not_localadmin() @@ -497,3 +507,187 @@ 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(""" + +""").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: + try: + response = globals()[action](request) + except KeyError: + raise WebmailError(_("Undefined action")) + 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) diff --git a/lib/email_listing.py b/lib/email_listing.py index f85201f11..36795c2dd 100644 --- a/lib/email_listing.py +++ b/lib/email_listing.py @@ -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("""
    @@ -123,29 +123,37 @@ class EmailListing(object):
    """) 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"): diff --git a/static/css/default.css b/static/css/default.css index ecf374c79..912742e0d 100644 --- a/static/css/default.css +++ b/static/css/default.css @@ -220,7 +220,7 @@ div.right { .logo { float: right; /*margin: 5px 0 0 5px;*/ - height: 90px; + height: 80px; } .dropdown { diff --git a/static/css/spinner.gif b/static/css/spinner.gif index 88fac73dc..53dd589fa 100644 Binary files a/static/css/spinner.gif and b/static/css/spinner.gif differ diff --git a/static/js/anchor_nav.js b/static/js/anchor_nav.js index 055896f50..1b5a023f8 100644 --- a/static/js/anchor_nav.js +++ b/static/js/anchor_nav.js @@ -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,24 +213,40 @@ 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, 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"); } }); diff --git a/static/js/common.js b/static/js/common.js index 77c5ba472..70b1c21c9 100644 --- a/static/js/common.js +++ b/static/js/common.js @@ -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; diff --git a/static/js/email_listing.js b/static/js/email_listing.js index 97f561ecf..89b170616 100644 --- a/static/js/email_listing.js +++ b/static/js/email_listing.js @@ -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); } } diff --git a/templates/common/email_listing.html b/templates/common/email_listing.html index 54fce2f75..bdc66cc5b 100644 --- a/templates/common/email_listing.html +++ b/templates/common/email_listing.html @@ -8,7 +8,9 @@ {% block css %}{% endblock %}