diff --git a/modoboa/extensions/amavis/sql_listing.py b/modoboa/extensions/amavis/sql_listing.py
index b7842eddb..edc06fbb7 100644
--- a/modoboa/extensions/amavis/sql_listing.py
+++ b/modoboa/extensions/amavis/sql_listing.py
@@ -306,18 +306,16 @@ class SQLlisting(EmailListing):
class SQLemail(Email):
- def __init__(self, msg, *args, **kwargs):
- super(SQLemail, self).__init__(msg, *args, **kwargs)
+ def __init__(self, *args, **kwargs):
+ super(SQLemail, self).__init__(*args, **kwargs)
fields = ["From", "To", "Cc", "Date", "Subject"]
for f in fields:
label = f
- self.headers += [{"name": label, "value": self.get_header(msg, f)}]
- try:
- label = re.sub("-", "_", label)
- setattr(self, label, msg[f])
- except:
- pass
- qreason = self.get_header(msg, "X-Amavis-Alert")
+ self.headers += [
+ {"name": label, "value": self.get_header(self.msg, f)}
+ ]
+ setattr(self, label, self.msg[f])
+ qreason = self.get_header(self.msg, "X-Amavis-Alert")
self.qtype = ""
self.qreason = ""
if qreason != "":
@@ -328,11 +326,19 @@ class SQLemail(Email):
self.qtype = "BAD HEADER SECTION"
self.qreason = qreason[19:]
- def get_header(self, msg, name):
- for k in [name, name.upper()]:
- if k in msg:
- return msg[k]
- return ""
+ @property
+ def msg(self):
+ """
+ """
+ import email
+
+ if self._msg is None:
+ qmails = get_wrapper().get_mail_content(self.mailid)
+ self._msg = email.message_from_string(
+ "".join([qm.mail_text for qm in qmails])
+ )
+ self._parse(self._msg)
+ return self._msg
def render_headers(self, **kwargs):
return render_to_string("amavis/mailheaders.html", {
diff --git a/modoboa/extensions/amavis/views.py b/modoboa/extensions/amavis/views.py
index c4d7f51d1..d57b4af98 100644
--- a/modoboa/extensions/amavis/views.py
+++ b/modoboa/extensions/amavis/views.py
@@ -92,12 +92,7 @@ def index(request):
def getmailcontent_selfservice(request, mail_id):
- qmails = get_wrapper().get_mail_content(mail_id)
- content = ""
- for qm in qmails:
- content += qm.mail_text
- msg = email.message_from_string(content)
- mail = SQLemail(msg, mformat="plain", links="0")
+ mail = SQLemail(mail_id, mformat="plain", links="0")
return render(request, "common/viewmail.html", {
"headers": mail.render_headers(),
"mailbody": mail.body
@@ -106,12 +101,7 @@ def getmailcontent_selfservice(request, mail_id):
@selfservice(getmailcontent_selfservice)
def getmailcontent(request, mail_id):
- qmails = get_wrapper().get_mail_content(mail_id)
- content = ""
- for qm in qmails:
- content += qm.mail_text
- msg = email.message_from_string(content)
- mail = SQLemail(msg, mformat="plain", links="0")
+ mail = SQLemail(mail_id, mformat="plain", links="0")
return render(request, "common/viewmail.html", {
"headers": mail.render_headers(),
"mailbody": mail.body
diff --git a/modoboa/extensions/webmail/forms.py b/modoboa/extensions/webmail/forms.py
index aa6a0bb04..d543e2971 100644
--- a/modoboa/extensions/webmail/forms.py
+++ b/modoboa/extensions/webmail/forms.py
@@ -1,9 +1,84 @@
# coding: utf-8
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
+
+import lxml.html
+
from django import forms
+from django.conf import settings
from django.utils.translation import ugettext_lazy as _
+from modoboa.lib import parameters
+from modoboa.lib.emailutils import set_email_headers
+from .lib import (
+ ImapEmail, create_mail_attachment
+)
+
+
+def html2plaintext(content):
+ """HTML to plain text translation.
+
+ :param content: some HTML content
+ """
+ if not content:
+ return ""
+ html = lxml.html.fromstring(content)
+ plaintext = ""
+ for ch in html.iter():
+ p = None
+ if ch.text is not None:
+ p = ch.text.strip('\r\t\n')
+ if ch.tag == "img":
+ p = ch.get("alt")
+ if p is None:
+ continue
+ plaintext += p + "\n"
+ return plaintext
+
+
+def make_body_images_inline(body):
+ """Looks for images inside the body and make them inline.
+
+ Before sending a message in HTML format, it is necessary to find
+ all img tags contained in the body in order to rewrite them. For
+ example, icons provided by CKeditor are stored on the server
+ filesystem and not accessible from the outside. We must embark
+ them as parts of the MIME message if we want recipients to
+ display them correctly.
+
+ :param body: the HTML body to parse
+ """
+ import os
+ from email.mime.image import MIMEImage
+ from urlparse import urlparse
+
+ html = lxml.html.fromstring(body)
+ parts = []
+ for tag in html.iter("img"):
+ src = tag.get("src")
+ if src is None:
+ continue
+ o = urlparse(src)
+ path = os.path.join(settings.MODOBOA_DIR, o.path[1:])
+ if not os.path.exists(path):
+ continue
+ fname = os.path.basename(path)
+ cid = "%s@modoboa" % os.path.splitext(fname)[0]
+ tag.set("src", "cid:%s" % cid)
+ with open(path, "rb") as fp:
+ part = MIMEImage(fp.read())
+ part["Content-ID"] = "<%s>" % cid
+ part.replace_header(
+ "Content-Type", '%s; name="%s"' % (part["Content-Type"], fname)
+ )
+ part["Content-Disposition"] = "inline"
+ parts.append(part)
+ return lxml.html.tostring(html), parts
+
class ComposeMailForm(forms.Form):
+ """Compose mail form.
+ """
to = forms.CharField(label=_("To"))
cc = forms.CharField(label=_("Cc"), required=False)
cci = forms.CharField(label=_("Cci"), required=False)
@@ -13,6 +88,103 @@ class ComposeMailForm(forms.Form):
origmsgid = forms.CharField(
label="", widget=forms.HiddenInput(), required=False
)
+ body = forms.CharField(widget=forms.widgets.Textarea)
+
+ def _html_msg(self):
+ """Create a multipart message.
+
+ We attach two alternatives:
+ * text/html
+ * text/plain
+ """
+ msg = MIMEMultipart(_subtype="related")
+ submsg = MIMEMultipart(_subtype="alternative")
+ body = self.cleaned_data["body"]
+ charset = "utf-8"
+ if body:
+ tbody = html2plaintext(body)
+ body, images = make_body_images_inline(body)
+ else:
+ tbody = ""
+ images = []
+ submsg.attach(
+ MIMEText(tbody.encode(charset), _subtype="plain", _charset=charset)
+ )
+ submsg.attach(
+ MIMEText(body.encode(charset), _subtype="html", _charset=charset)
+ )
+ msg.attach(submsg)
+ for img in images:
+ msg.attach(img)
+ return msg
+
+ def _plain_msg(self):
+ """Create a simple text message.
+ """
+ charset = "utf-8"
+ text = MIMEText(self.cleaned_data["body"].encode(charset),
+ _subtype="plain", _charset=charset)
+ return text
+
+ def _build_msg(self, request):
+ """Convert form's content to a MIME message.
+ """
+ editormode = parameters.get_user(request.user, "EDITOR")
+ msg = getattr(self, "_%s_msg" % editormode)()
+
+ if request.session["compose_mail"]["attachments"]:
+ wrapper = MIMEMultipart(_subtype="mixed")
+ wrapper.attach(msg)
+ for attdef in request.session["compose_mail"]["attachments"]:
+ wrapper.attach(create_mail_attachment(attdef))
+ msg = wrapper
+ return msg
+
+ def to_msg(self, request):
+ """Convert form's content to an object ready to send.
+
+ We set headers at the end to be sure no one will override
+ them.
+
+ """
+ msg = self._build_msg(request)
+ set_email_headers(
+ msg, self.cleaned_data["subject"], request.user.encoded_address,
+ self.cleaned_data['to']
+ )
+ origmsgid = self.cleaned_data.get("origmsgid", None)
+ if origmsgid:
+ msg["References"] = msg["In-Reply-To"] = origmsgid
+ return msg
+
+
+class ForwardMailForm(ComposeMailForm):
+ """Forward mail form.
+ """
+
+ def _build_msg(self, request):
+ """Convert form's content to a MIME message.
+
+ We also add original attachments (if any) to the new message.
+ """
+ from modoboa.extensions.webmail.lib import decode_payload
+
+ mbox = request.GET.get("mbox", None)
+ mailid = request.GET.get("mailid", None)
+ msg = super(ForwardMailForm, self)._build_msg(request)
+ origmsg = ImapEmail(request, False, "%s:%s" % (mbox, mailid))
+ if origmsg.attachments:
+ if not msg.is_multipart or not msg.get_content_subtype() == "mixed":
+ wrapper = MIMEMultipart(_subtype="mixed")
+ wrapper.attach(msg)
+ msg = wrapper
+ for attpart, fname in origmsg.attachments.items():
+ attdef, payload = origmsg.fetch_attachment(attpart)
+ attdef["fname"] = fname
+ msg.attach(create_mail_attachment(
+ attdef, decode_payload(attdef["encoding"], payload)
+ ))
+ return msg
class FolderForm(forms.Form):
diff --git a/modoboa/extensions/webmail/lib.py b/modoboa/extensions/webmail/lib.py
deleted file mode 100644
index 18f9c67d5..000000000
--- a/modoboa/extensions/webmail/lib.py
+++ /dev/null
@@ -1,773 +0,0 @@
-# coding: utf-8
-import os
-import re
-from datetime import datetime, timedelta
-import email
-import lxml.html
-import chardet
-from rfc6266 import build_header, parse_headers
-from django.core.files.uploadhandler import FileUploadHandler, SkipFile
-from django.utils.translation import ugettext as _, ugettext_lazy
-from django.conf import settings
-from modoboa.lib import u2u_decode, tables, parameters
-from modoboa.lib.webutils import size2integer, NavigationParameters
-from modoboa.lib.exceptions import InternalError
-from modoboa.lib.email_listing import EmailListing
-from modoboa.lib.emailutils import (
- EmailAddress, Email, prepare_addresses, set_email_headers
-)
-from modoboa.extensions.webmail.exceptions import WebmailInternalError
-from modoboa.extensions.webmail.imaputils import (
- IMAPconnector, get_imapconnector, BodyStructure
-)
-
-
-class WMtable(tables.Table):
- tableid = "emails"
- styles = "table-condensed"
- idkey = "imapid"
- drag = tables.ImgColumn(
- "drag", cssclass="draggable left", width="1%",
- defvalue="%spics/grippy.png" % settings.STATIC_URL,
- header=""
- )
- selection = tables.SelectionColumn(
- "selection", safe=True, width='1%', header=None, sortable=False
- )
- flags = tables.ImgColumn("flags", width="3%")
- withatts = tables.ImgColumn("withatts", width="2%")
- subject = tables.Column(
- "subject", label=ugettext_lazy("Subject"), width="50%", limit=60,
- cssclass="openable"
- )
- from_ = tables.Column(
- "from", width="20%", label=ugettext_lazy("From"), limit=30,
- cssclass="openable"
- )
- date = tables.Column(
- "date", width="15%", label=ugettext_lazy("Date"), cssclass="openable"
- )
-
- cols_order = ["drag", "selection", "withatts", "flags", "subject", "from_", "date"]
-
- def parse(self, header, value):
- if value is None:
- return ""
- try:
- value = getattr(IMAPheader, "parse_%s" % header)(value)
- except AttributeError:
- pass
- return value
-
-
-class IMAPheader(object):
- @staticmethod
- def to_unicode(value):
- if value is None or type(value) is unicode:
- return value
- try:
- value = value.decode('utf-8')
- except UnicodeDecodeError:
- pass
- else:
- return value
- try:
- res = chardet.detect(value)
- except UnicodeDecodeError:
- return value
- if res["encoding"] == "ascii":
- return value
- return value.decode(res["encoding"])
-
- @staticmethod
- def parse_address(value, **kwargs):
- addr = EmailAddress(value)
- if "full" in kwargs.keys() and kwargs["full"]:
- return IMAPheader.to_unicode(addr.fulladdress)
- result = addr.name and addr.name or addr.fulladdress
- return IMAPheader.to_unicode(result)
-
- @staticmethod
- def parse_address_list(values, **kwargs):
- lst = values.split(",")
- result = ""
- for addr in lst:
- if result != "":
- result += ", "
- result += IMAPheader.parse_address(addr, **kwargs)
- return result
-
- @staticmethod
- def parse_from(value, **kwargs):
- return IMAPheader.parse_address(value, **kwargs)
-
- @staticmethod
- def parse_to(value, **kwargs):
- return IMAPheader.parse_address_list(value, **kwargs)
-
- @staticmethod
- def parse_cc(value, **kwargs):
- return IMAPheader.parse_address_list(value, **kwargs)
-
- @staticmethod
- def parse_reply_to(value, **kwargs):
- return IMAPheader.parse_address_list(value, **kwargs)
-
- @staticmethod
- def parse_date(value, **kwargs):
- tmp = email.utils.parsedate_tz(value)
- if not tmp:
- return value
- ndate = datetime(*(tmp)[:7])
- now = datetime.now()
- try:
- if now - ndate > timedelta(7):
- return ndate.strftime("%d.%m.%Y %H:%M")
- return ndate.strftime("%a %H:%M")
- except ValueError:
- return value
-
- @staticmethod
- def parse_message_id(value, **kwargs):
- return value.strip('\n')
-
- @staticmethod
- def parse_subject(value, **kwargs):
- try:
- subject = u2u_decode.u2u_decode(value)
- except UnicodeDecodeError:
- subject = value
- return IMAPheader.to_unicode(subject)
-
-
-class ImapListing(EmailListing):
- tpl = "webmail/index.html"
- tbltype = WMtable
- deflocation = "INBOX/"
- defcallback = "wm_updatelisting"
- reset_wm_url = False
-
- def __init__(self, user, password, **kwargs):
- self.user = user
- self.mbc = IMAPconnector(user=user.username, password=password)
- if "pattern" in kwargs and kwargs["pattern"]:
- self.parse_search_parameters(
- kwargs["criteria"], kwargs["pattern"]
- )
- else:
- self.mbc.criterions = []
-
- super(ImapListing, self).__init__(**kwargs)
- self.extravars["refreshrate"] = \
- int(parameters.get_user(user, "REFRESH_INTERVAL")) * 1000
-
- def parse_search_parameters(self, criterion, pattern):
-
- def or_criterion(old, c):
- if old == "":
- return c
- return "OR (%s) (%s)" % (old, c)
-
- if criterion == u"both":
- criterion = u"from_addr, subject"
- criterions = ""
- if type(pattern) is unicode:
- pattern = pattern.encode("utf-8")
- if type(criterion) is unicode:
- criterion = criterion.encode("utf-8")
- for c in criterion.split(','):
- if c == "from_addr":
- key = "FROM"
- elif c == "subject":
- key = "SUBJECT"
- else:
- continue
- criterions = \
- or_criterion(criterions, '(%s "%s")' % (key, pattern))
- self.mbc.criterions = [criterions]
-
- @staticmethod
- def computequota(mbc):
- try:
- return int(float(mbc.quota_actual) \
- / float(mbc.quota_limit) * 100)
- except (AttributeError, TypeError):
- return -1
-
- def getquota(self):
- return ImapListing.computequota(self.mbc)
-
-
-class ImapEmail(Email):
- headernames = [
- ('From', True),
- ('To', True),
- ('Cc', True),
- ('Date', True),
- ('Subject', True),
- ]
-
- def __init__(self, mbox, mailid, request, dformat="DISPLAYMODE", addrfull=False,
- links=0):
- mformat = parameters.get_user(request.user, "DISPLAYMODE")
- self.dformat = parameters.get_user(request.user, dformat)
-
- self.headers = []
- self.attachments = {}
- self.imapc = get_imapconnector(request)
- msg = self.imapc.fetchmail(mbox, mailid, readonly=False,
- headers=self.headers_as_list)
- self.mbox = mbox
- self.mailid = mailid
- headers = msg['BODY[HEADER.FIELDS (%s)]' % self.headers_as_text]
- fallback_fmt = "html" if self.dformat == "plain" else "plain"
-
- self.bs = BodyStructure(msg['BODYSTRUCTURE'])
- data = None
-
- mformat = self.dformat if self.dformat in self.bs.contents else fallback_fmt
-
- if len(self.bs.contents):
- bodyc = u''
- for part in self.bs.contents[mformat]:
- pnum = part['pnum']
- data = self.imapc._cmd("FETCH", mailid, "(BODY.PEEK[%s])" % pnum)
- content = decode_payload(part['encoding'],
- data[int(mailid)]['BODY[%s]' % pnum])
- charset = self._find_content_charset(part)
- if charset is not None:
- try:
- content = content.decode(charset)
- except (UnicodeDecodeError, LookupError):
- result = chardet.detect(content)
- content = content.decode(result['encoding'])
- bodyc += content
-
- self._fetch_inlines()
- self.body = \
- getattr(self, "viewmail_%s" % mformat)(bodyc, links=links)
- else:
- self.body = None
-
- self._find_attachments()
-
- msg = email.message_from_string(headers)
- for hdr in self.headernames:
- label = hdr[0]
- name = hdr[0]
- if not name in msg.keys():
- name = name.upper()
- if not name in msg.keys():
- continue
- try:
- key = re.sub("-", "_", name).lower()
- value = getattr(IMAPheader, "parse_%s" % key)(msg[name], full=addrfull)
- except AttributeError:
- value = msg[name]
- if hdr[1]:
- self.headers += [{"name": label, "value": value}]
- try:
- label = re.sub("-", "_", label)
- setattr(self, label, value)
- except:
- pass
-
- @property
- def headers_as_list(self):
- return [hdr[0].upper() for hdr in self.headernames]
-
- @property
- def headers_as_text(self):
- return " ".join(self.headers_as_list)
-
- def _find_content_charset(self, part):
- for pos, elem in enumerate(part["params"]):
- if elem == "charset":
- return part["params"][pos + 1]
- return None
-
- def _find_attachments(self):
- """Retrieve attachments from the parsed body structure.
-
- We try to find and decode a file name for each attachment. If
- we failed, a generic name will be used (ie. part_1, part_2, ...).
- """
- for att in self.bs.attachments:
- attname = "part_%s" % att["pnum"]
- if "params" in att and att["params"] != "NIL":
- attname = u2u_decode.u2u_decode(att["params"][1]) \
- .strip("\r\t\n")
- elif "disposition" in att and len(att["disposition"]) > 1:
- for pos, value in enumerate(att["disposition"][1]):
- if not value.startswith("filename"):
- continue
- header = "%s; %s=%s" \
- % (att['disposition'][0],
- value,
- att["disposition"][1][pos + 1].strip("\r\t\n"))
- attname = parse_headers(header).filename_unsafe
- if attname is None:
- attname = u2u_decode.u2u_decode(
- att["disposition"][1][pos + 1]
- ).strip("\r\t\n")
- break
- self.attachments[att["pnum"]] = attname
-
- def _fetch_inlines(self):
- for cid, params in self.bs.inlines.iteritems():
- if re.search("\.\.", cid):
- continue
- fname = "webmail/%s_%s" % (self.mailid, cid)
- path = os.path.join(settings.MEDIA_ROOT, fname)
- params["fname"] = os.path.join(settings.MEDIA_URL, fname)
- if os.path.exists(path):
- continue
-
- pdef, content = self.imapc.fetchpart(self.mailid, self.mbox, params["pnum"])
- fp = open(path, "wb")
- fp.write(decode_payload(params["encoding"], content))
- fp.close()
-
- def map_cid(self, url):
- m = re.match(".*cid:(.+)", url)
- if m:
- if m.group(1) in self.bs.inlines:
- return self.bs.inlines[m.group(1)]["fname"]
- return url
-
- def render_headers(self, **kwargs):
- from django.template.loader import render_to_string
-
- res = render_to_string("webmail/headers.html", {
- "headers": self.headers,
- "folder": kwargs["folder"], "mail_id": kwargs["mail_id"],
- "attachments": self.attachments != {} and self.attachments or None
- })
- return res
-
-
-class Modifier(ImapEmail):
- def __init__(self, *args, **kwargs):
- super(Modifier, self).__init__(*args, **kwargs)
- getattr(self, "_modify_%s" % self.dformat)()
-
- def _modify_plain(self):
- self.body = re.sub("?pre>", "", self.body)
-
- def _modify_html(self):
- pass
-
-
-class ReplyModifier(Modifier):
- headernames = ImapEmail.headernames + \
- [("Reply-To", True),
- ("Message-ID", False)]
-
- def __init__(self, mbox, mailid, request, form, **kwargs):
- super(ReplyModifier, self).__init__(
- mbox, mailid, request, dformat="EDITOR", **kwargs
- )
-
- self.textheader = "%s %s" % (self.From, _("wrote:"))
-
- if hasattr(self, "Message_ID"):
- form.fields["origmsgid"].initial = self.Message_ID
- if not hasattr(self, "Reply_To"):
- form.fields["to"].initial = self.From
- else:
- form.fields["to"].initial = self.Reply_To
- if request.GET.get("all", "0") == "1": # reply-all
- form.fields["cc"].initial = ""
- toparse = self.To.split(",")
- if hasattr(self, 'Cc'):
- toparse += self.Cc.split(",")
- for addr in toparse:
- tmp = EmailAddress(addr)
- if tmp.address and tmp.address == request.user.username:
- continue
- if form.fields["cc"].initial != "":
- form.fields["cc"].initial += ", "
- form.fields["cc"].initial += tmp.fulladdress
- m = re.match("re\s*:\s*.+", self.Subject.lower())
- if m:
- form.fields["subject"].initial = self.Subject
- else:
- form.fields["subject"].initial = "Re: %s" % self.Subject
-
- def _modify_plain(self):
- super(ReplyModifier, self)._modify_plain()
- lines = self.body.split('\n')
- body = ""
- for l in lines:
- if body != "":
- body += "\n"
- body += ">%s" % l
- self.body = body
-
-
-class ForwardModifier(Modifier):
- def __init__(self, mbox, mailid, request, form, **kwargs):
- super(ForwardModifier, self).__init__(
- mbox, mailid, request, dformat="EDITOR", **kwargs
- )
-
- self._header()
- form.fields["subject"].initial = "Fwd: %s" % self.Subject
-
- def __getfunc(self, name):
- return getattr(self, "%s_%s" % (name, self.dformat))
-
- def _header(self):
- self.textheader = self.__getfunc("_header_begin")() + "\n"
- self.textheader += \
- self.__getfunc("_header_line")(_("Subject"), self.Subject) + "\n"
- self.textheader += \
- self.__getfunc("_header_line")(_("Date"), self.Date) + "\n"
- for hdr in ["From", "To", "Reply-To"]:
- try:
- key = re.sub("-", "_", hdr)
- value = getattr(self, key)
- self.textheader += \
- self.__getfunc("_header_line")(_(hdr), value) + "\n"
- except:
- pass
- self.textheader += self.__getfunc("_header_end")()
-
- def _header_begin_plain(self):
- return "----- %s -----" % _("Original message")
-
- def _header_begin_html(self):
- return """----- %s -----
-
| %s | %s |
%s: %s
" % (key, value) + + def _header_end_plain(self): + return "\n" + + def _header_end_html(self): + return "" diff --git a/modoboa/extensions/webmail/lib/imapheader.py b/modoboa/extensions/webmail/lib/imapheader.py new file mode 100644 index 000000000..2106ef8e3 --- /dev/null +++ b/modoboa/extensions/webmail/lib/imapheader.py @@ -0,0 +1,114 @@ +""" +Set of functions used to parse and transform email headers. +""" +import chardet +from modoboa.lib.emailutils import EmailAddress + +__all__ = [ + 'parse_from', 'parse_to', 'parse_message_id', 'parse_date', + 'parse_reply_to', 'parse_cc', 'parse_subject' +] + + +def to_unicode(value): + """Try to convert a string to unicode. + """ + if value is None or type(value) is unicode: + return value + try: + value = value.decode('utf-8') + except UnicodeDecodeError: + pass + else: + return value + try: + res = chardet.detect(value) + except UnicodeDecodeError: + return value + if res["encoding"] == "ascii": + return value + return value.decode(res["encoding"]) + + +def parse_address(value, **kwargs): + """Parse an email address. + """ + addr = EmailAddress(value) + if "full" in kwargs.keys() and kwargs["full"]: + return to_unicode(addr.fulladdress) + result = addr.name and addr.name or addr.fulladdress + return to_unicode(result) + + +def parse_address_list(values, **kwargs): + """Parse a list of email addresses. + """ + lst = values.split(",") + result = "" + for addr in lst: + if result != "": + result += ", " + result += parse_address(addr, **kwargs) + return result + + +def parse_from(value, **kwargs): + """Parse a From: header. + """ + return parse_address(value, **kwargs) + + +def parse_to(value, **kwargs): + """Parse a To: header. + """ + return parse_address_list(value, **kwargs) + + +def parse_cc(value, **kwargs): + """Parse a Cc: header. + """ + return parse_address_list(value, **kwargs) + + +def parse_reply_to(value, **kwargs): + """Parse a Reply-To: header. + """ + return parse_address_list(value, **kwargs) + + +def parse_date(value, **kwargs): + """Parse a Date: header. + """ + import datetime + import email + + tmp = email.utils.parsedate_tz(value) + if not tmp: + return value + ndate = datetime.datetime(*(tmp)[:7]) + now = datetime.datetime.now() + try: + if now - ndate > datetime.timedelta(7): + return ndate.strftime("%d.%m.%Y %H:%M") + return ndate.strftime("%a %H:%M") + except ValueError: + return value + + +def parse_message_id(value, **kwargs): + """Parse a Message-ID: header. + """ + return value.strip('\n') + + +def parse_subject(value, **kwargs): + """Parse a Subject: header. + """ + from modoboa.lib import u2u_decode + + try: + subject = u2u_decode.u2u_decode(value) + except UnicodeDecodeError: + subject = value + return to_unicode(subject) + diff --git a/modoboa/extensions/webmail/imaputils.py b/modoboa/extensions/webmail/lib/imaputils.py similarity index 99% rename from modoboa/extensions/webmail/imaputils.py rename to modoboa/extensions/webmail/lib/imaputils.py index 67065363a..c32584b13 100644 --- a/modoboa/extensions/webmail/imaputils.py +++ b/modoboa/extensions/webmail/lib/imaputils.py @@ -14,8 +14,8 @@ from modoboa.lib import parameters, imap_utf7 from modoboa.lib.exceptions import InternalError from modoboa.lib.connections import ConnectionsManager from modoboa.lib.webutils import static_url -from exceptions import ImapError, WebmailInternalError -from fetch_parser import parse_fetch_response +from ..exceptions import ImapError, WebmailInternalError +from .fetch_parser import parse_fetch_response #imaplib.Debug = 4 diff --git a/modoboa/extensions/webmail/lib/listing.py b/modoboa/extensions/webmail/lib/listing.py new file mode 100644 index 000000000..6a49fa149 --- /dev/null +++ b/modoboa/extensions/webmail/lib/listing.py @@ -0,0 +1,107 @@ +from django.conf import settings +from django.utils.translation import ugettext_lazy + +from modoboa.lib import parameters, tables +from modoboa.lib.email_listing import EmailListing +from . import IMAPconnector, imapheader + + +class WMtable(tables.Table): + """ + Webmail listing table. + """ + tableid = "emails" + styles = "table-condensed" + idkey = "imapid" + drag = tables.ImgColumn( + "drag", cssclass="draggable left", width="1%", + defvalue="%spics/grippy.png" % settings.STATIC_URL, + header="" + ) + selection = tables.SelectionColumn( + "selection", safe=True, width='1%', header=None, sortable=False + ) + flags = tables.ImgColumn("flags", width="3%") + withatts = tables.ImgColumn("withatts", width="2%") + subject = tables.Column( + "subject", label=ugettext_lazy("Subject"), width="50%", limit=60, + cssclass="openable" + ) + from_ = tables.Column( + "from", width="20%", label=ugettext_lazy("From"), limit=30, + cssclass="openable" + ) + date = tables.Column( + "date", width="15%", label=ugettext_lazy("Date"), cssclass="openable" + ) + + cols_order = [ + "drag", "selection", "withatts", "flags", "subject", "from_", "date" + ] + + def parse(self, header, value): + if value is None: + return "" + try: + value = getattr(imapheader, "parse_%s" % header)(value) + except AttributeError: + pass + return value + + +class ImapListing(EmailListing): + tpl = "webmail/index.html" + tbltype = WMtable + deflocation = "INBOX/" + defcallback = "wm_updatelisting" + reset_wm_url = False + + def __init__(self, user, password, **kwargs): + self.user = user + self.mbc = IMAPconnector(user=user.username, password=password) + if "pattern" in kwargs and kwargs["pattern"]: + self.parse_search_parameters( + kwargs["criteria"], kwargs["pattern"] + ) + else: + self.mbc.criterions = [] + + super(ImapListing, self).__init__(**kwargs) + self.extravars["refreshrate"] = \ + int(parameters.get_user(user, "REFRESH_INTERVAL")) * 1000 + + def parse_search_parameters(self, criterion, pattern): + + def or_criterion(old, c): + if old == "": + return c + return "OR (%s) (%s)" % (old, c) + + if criterion == u"both": + criterion = u"from_addr, subject" + criterions = "" + if type(pattern) is unicode: + pattern = pattern.encode("utf-8") + if type(criterion) is unicode: + criterion = criterion.encode("utf-8") + for c in criterion.split(','): + if c == "from_addr": + key = "FROM" + elif c == "subject": + key = "SUBJECT" + else: + continue + criterions = \ + or_criterion(criterions, '(%s "%s")' % (key, pattern)) + self.mbc.criterions = [criterions] + + @staticmethod + def computequota(mbc): + try: + return int(float(mbc.quota_actual) \ + / float(mbc.quota_limit) * 100) + except (AttributeError, TypeError): + return -1 + + def getquota(self): + return ImapListing.computequota(self.mbc) diff --git a/modoboa/extensions/webmail/lib/sendmail.py b/modoboa/extensions/webmail/lib/sendmail.py new file mode 100644 index 000000000..ba654cc02 --- /dev/null +++ b/modoboa/extensions/webmail/lib/sendmail.py @@ -0,0 +1,65 @@ +import smtplib +from modoboa.lib import parameters +from modoboa.lib.emailutils import prepare_addresses +from modoboa.lib.webutils import _render_to_string +from modoboa.lib.cryptutils import get_password +from modoboa.extensions.webmail.exceptions import WebmailInternalError +from . import get_imapconnector, clean_attachments + + +def send_mail(request, form, posturl=None): + """Email verification and sending. + + If the form does not present any error, a new MIME message is + constructed. Then, a connection is established with the defined + SMTP server and the message is finally sent. + + :param request: a Request object + :param posturl: the url to post the message form to + :return: a 2-uple (True|False, HttpResponse) + """ + if not form.is_valid(): + editormode = parameters.get_user(request.user, "EDITOR") + listing = _render_to_string( + request, "webmail/compose.html", + {"form": form, "noerrors": True, + "body": form.cleaned_data.get("body", "").strip(), + "posturl": posturl} + ) + return False, dict(status="ko", listing=listing, editor=editormode) + + msg = form.to_msg(request) + rcpts = prepare_addresses(form.cleaned_data["to"], "envelope") + for hdr in ["cc", "cci"]: + if form.cleaned_data[hdr]: + msg[hdr.capitalize()] = prepare_addresses(form.cleaned_data[hdr]) + rcpts += prepare_addresses(form.cleaned_data[hdr], "envelope") + try: + secmode = parameters.get_admin("SMTP_SECURED_MODE") + if secmode == "ssl": + s = smtplib.SMTP_SSL(parameters.get_admin("SMTP_SERVER"), + int(parameters.get_admin("SMTP_PORT"))) + else: + s = smtplib.SMTP(parameters.get_admin("SMTP_SERVER"), + int(parameters.get_admin("SMTP_PORT"))) + if secmode == "starttls": + s.starttls() + except Exception as text: + raise WebmailInternalError(str(text)) + + if parameters.get_admin("SMTP_AUTHENTICATION") == "yes": + try: + s.login(request.user.username, get_password(request)) + except smtplib.SMTPException as err: + raise WebmailInternalError(str(err)) + try: + s.sendmail(request.user.email, rcpts, msg.as_string()) + s.quit() + except smtplib.SMTPException as err: + raise WebmailInternalError(str(err)) + + sentfolder = parameters.get_user(request.user, "SENT_FOLDER") + get_imapconnector(request).push_mail(sentfolder, msg) + clean_attachments(request.session["compose_mail"]["attachments"]) + del request.session["compose_mail"] + return True, {} diff --git a/modoboa/extensions/webmail/lib/signature.py b/modoboa/extensions/webmail/lib/signature.py new file mode 100644 index 000000000..359801a1f --- /dev/null +++ b/modoboa/extensions/webmail/lib/signature.py @@ -0,0 +1,29 @@ +import re +from modoboa.lib import parameters + + +class EmailSignature(object): + """User signature + + :param user: User object + """ + def __init__(self, user): + self._sig = "" + dformat = parameters.get_user(user, "EDITOR") + content = parameters.get_user(user, "SIGNATURE") + if len(content): + getattr(self, "_format_sig_%s" % dformat)(content) + + def _format_sig_plain(self, content): + self._sig = """ +--- +%s""" % content + + def _format_sig_html(self, content): + content = re.sub("\n", "