mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-18 05:51:26 +02:00
315 lines
11 KiB
Python
315 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
import time
|
|
import re
|
|
import os
|
|
import time, random, hashlib
|
|
from email.header import decode_header
|
|
import lxml.html
|
|
from lxml import etree
|
|
from django.conf import settings
|
|
from django.template.loader import render_to_string
|
|
from django.template import Template, Context
|
|
from django.utils.translation import ugettext as _
|
|
from modoboa.lib import _render, decode
|
|
|
|
attached_map = {}
|
|
|
|
class MBconnector(object):
|
|
def __init__(self, address, port):
|
|
self.address = address
|
|
self.port = port
|
|
|
|
def messages_count(self, **kwargs):
|
|
pass
|
|
|
|
class Page(object):
|
|
def __init__(self, pageid, id_start, id_stop, items,
|
|
items_per_page, has_previous, has_next,
|
|
baseurl=None):
|
|
self.pageid = pageid
|
|
self.id_start = id_start
|
|
self.id_stop = id_stop
|
|
self.items = items
|
|
self.items_per_page = items_per_page
|
|
self.has_previous = has_previous
|
|
self.has_next = has_next
|
|
self.baseurl = baseurl
|
|
|
|
def previous_page(self):
|
|
if not self.has_previous:
|
|
return False
|
|
return self.pageid - 1
|
|
|
|
def next_page(self):
|
|
if not self.has_next:
|
|
return False
|
|
return self.pageid + 1
|
|
|
|
def last_page(self):
|
|
lid = self.items / self.items_per_page
|
|
if not lid:
|
|
return 1
|
|
if self.items % self.items_per_page:
|
|
lid += 1
|
|
return lid
|
|
|
|
class Paginator(object):
|
|
def __init__(self, total, elems_per_page):
|
|
self.total = total
|
|
self.elems_per_page = elems_per_page
|
|
self.baseurl = None
|
|
|
|
def _indexes(self, page):
|
|
id_start = self.elems_per_page * page + 1
|
|
id_stop = id_start + self.elems_per_page - 1
|
|
return (id_start, id_stop)
|
|
|
|
def getpage(self, page):
|
|
if page < 1:
|
|
return None
|
|
id_start, id_stop = self._indexes(page - 1)
|
|
has_previous = has_next = False
|
|
if id_start <= self.total:
|
|
if page > 1:
|
|
has_previous = True
|
|
if id_stop < self.total:
|
|
has_next = True
|
|
else:
|
|
id_stop = self.total
|
|
else:
|
|
return None
|
|
p = Page(page, id_start, id_stop, self.total,
|
|
self.elems_per_page, has_previous, has_next)
|
|
if self.baseurl:
|
|
p.baseurl = self.baseurl
|
|
return p
|
|
|
|
class EmailListing:
|
|
def __init__(self, baseurl=None, folder=None, elems_per_page=40,
|
|
navparams={}, **kwargs):
|
|
self.folder = folder
|
|
self.elems_per_page = elems_per_page
|
|
self.navparams = navparams
|
|
self.extravars = {}
|
|
order = "order" in kwargs.keys() and kwargs["order"] or None
|
|
self.empty = "empty" in kwargs.keys() and kwargs["empty"] or False
|
|
if not self.empty:
|
|
self.paginator = Paginator(self.mbc.messages_count(folder=self.folder,
|
|
order=order),
|
|
elems_per_page)
|
|
if baseurl:
|
|
self.paginator.baseurl = baseurl
|
|
|
|
def render_navbar(self, page):
|
|
if page is None:
|
|
return ""
|
|
context = {"page" : page, "MEDIA_URL" : settings.MEDIA_URL}
|
|
return render_to_string("common/navbar.html", context)
|
|
|
|
def fetch(self, request, id_start, id_stop):
|
|
table = self.tbltype(self.mbc.fetch(start=id_start, stop=id_stop,
|
|
folder=self.folder))
|
|
tpl = Template("""
|
|
<form method="POST" id="listingform">
|
|
{{ table }}
|
|
</form>""")
|
|
return tpl.render(Context({"table" : table.render(request)}))
|
|
|
|
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)
|
|
|
|
class Email(object):
|
|
def __init__(self, msg, mformat="plain", dformat="plain", links="0"):
|
|
self.attached_map = {}
|
|
self.contents = {"html" : "", "plain" : ""}
|
|
self.headers = []
|
|
self.attachments = {}
|
|
self.mformat = mformat
|
|
self.dformat = dformat
|
|
self.links = links
|
|
|
|
self.__parse(msg)
|
|
|
|
if not self.contents.has_key(mformat) or self.contents[mformat] == "":
|
|
# Fallback
|
|
self.mformat = mformat == "html" and "plain" or "html"
|
|
|
|
self.body = \
|
|
getattr(self, "viewmail_%s" % self.mformat) \
|
|
(self.contents[self.mformat], links=links)
|
|
|
|
def __parse_default(self, msg, level):
|
|
"""Default parser
|
|
|
|
All parts handled by this parser will be consireded as
|
|
attachments.
|
|
"""
|
|
fname = msg.get_filename()
|
|
if fname is not None:
|
|
decoded = decode_header(fname)
|
|
value = decoded[0][1] is None and decoded[0][0] \
|
|
or unicode(decoded[0][0], decoded[0][1])
|
|
else:
|
|
value = "part_%s" % level
|
|
self.attachments[level] = value
|
|
|
|
def _parse_text(self, msg, level):
|
|
"""Displayable content parser
|
|
|
|
text, html, calendar, etc. All those contents can be displayed
|
|
inside a navigator.
|
|
"""
|
|
if msg.get_content_subtype() not in ["plain", "html"]:
|
|
self.__parse_default(msg, level)
|
|
target = "plain"
|
|
else:
|
|
target = msg.get_content_subtype()
|
|
self.contents[target] += decode(msg.get_payload(decode=True),
|
|
charset=msg.get_content_charset())
|
|
|
|
def _parse_image(self, msg, level):
|
|
"""image/* parser
|
|
|
|
The only reason to make a specific parser for images is that,
|
|
sometimes, messages embark inline images, which means they
|
|
must be displayed and not attached.
|
|
"""
|
|
if self.dformat == "html" and self.links != "0" \
|
|
and msg.has_key("Content-Disposition"):
|
|
if msg["Content-Disposition"].startswith("inline"):
|
|
cid = None
|
|
if msg.has_key("Content-ID"):
|
|
m = re.match("<(.+)>", msg["Content-ID"])
|
|
cid = m is not None and m.group(1) or msg["Content-ID"]
|
|
fname = msg.get_filename()
|
|
if fname is None:
|
|
if msg.has_key("Content-Location"):
|
|
fname = msg["Content-Location"]
|
|
elif cid is not None:
|
|
fname = cid
|
|
else:
|
|
# I give up for now :p
|
|
return
|
|
self.attached_map[cid] = re.match("^http:", fname) and fname \
|
|
or self.__save_image(fname, msg)
|
|
return
|
|
self.__parse_default(msg, level)
|
|
|
|
def __parse(self, msg, level=None):
|
|
"""Recursive email parser
|
|
|
|
A message structure can be complex. To correctly handle
|
|
unknown MIME types, a simple rule is applied : if I don't know
|
|
how to display a specific part, it becomes an attachment! If
|
|
no name is specified for an attachment, the part number
|
|
described in the RFC 3501 (which retrieves BODY sections) is
|
|
used to build a file name (like part_1.1).
|
|
|
|
:param msg: message (or part) to parse
|
|
:param level: current part number
|
|
"""
|
|
if msg.is_multipart() and msg.get_content_maintype() != "message":
|
|
cpt = 1
|
|
for part in msg.get_payload():
|
|
nlevel = level is None and ("%d" % cpt) \
|
|
or "%s.%d" % (level, cpt)
|
|
self.__parse(part, nlevel)
|
|
cpt += 1
|
|
return
|
|
|
|
if level is None:
|
|
level = "1"
|
|
try:
|
|
getattr(self, "_parse_%s" % msg.get_content_maintype())(msg, level)
|
|
except AttributeError:
|
|
self.__parse_default(msg, level)
|
|
|
|
def __save_image(self, fname, part):
|
|
"""Save an inline image on the filesystem.
|
|
|
|
Some HTML messages are using inline images (attached images
|
|
with links on them inside the body). In order to display them,
|
|
images are saved on the filesystem and links contained in the
|
|
message are modified.
|
|
|
|
:param fname: the image associated filename
|
|
:param part: the email part that contains the image payload
|
|
"""
|
|
if re.search("\.\.", fname):
|
|
return None
|
|
path = "/static/tmp/" + fname
|
|
fp = open(settings.MODOBOA_DIR + path, "wb")
|
|
fp.write(part.get_payload(decode=True))
|
|
fp.close()
|
|
return path
|
|
|
|
def map_cid(self, url):
|
|
import re
|
|
|
|
m = re.match(".*cid:(.+)", url)
|
|
if m:
|
|
if self.attached_map.has_key(m.group(1)):
|
|
return self.attached_map[m.group(1)]
|
|
return url
|
|
|
|
def render_headers(self, **kwargs):
|
|
return render_to_string("common/mailheaders.html", {
|
|
"headers" : self.headers,
|
|
})
|
|
|
|
def viewmail_plain(self, content, **kwargs):
|
|
return self.dformat == "html" and "<pre>%s</pre>" % content \
|
|
or content
|
|
|
|
def viewmail_html(self, content, **kwargs):
|
|
if content is None or content == "":
|
|
return (False, "")
|
|
links = kwargs.has_key("links") and kwargs["links"] or "0"
|
|
html = lxml.html.fromstring(content)
|
|
if links == "0":
|
|
html.rewrite_links(lambda x: None)
|
|
else:
|
|
html.rewrite_links(self.map_cid)
|
|
body = html.find("body")
|
|
if body is None:
|
|
body = lxml.html.tostring(html)
|
|
else:
|
|
body = lxml.html.tostring(body)
|
|
body = re.sub("<(/?)body", lambda m: "<%sdiv" % m.group(1), body)
|
|
body = Template(body).render(Context({}))
|
|
return body
|
|
|
|
def parse_search_parameters(request):
|
|
if request.GET.has_key("pattern"):
|
|
request.session["pattern"] = re.escape(request.GET["pattern"])
|
|
if request.GET.has_key("criteria"):
|
|
request.session["criteria"] = request.GET["criteria"]
|
|
else:
|
|
request.session["criteria"] = ["from_addr"]
|
|
else:
|
|
for p in ["pattern", "criteria"]:
|
|
if p in request.session.keys():
|
|
del request.session[p]
|
|
|