# -*- coding: utf-8 -*- import sys, os reload(sys) sys.setdefaultencoding("utf-8") import re import time import socket import ssl from datetime import datetime, timedelta import email import lxml from django.core.files.uploadhandler import FileUploadHandler, StopUpload, SkipFile from django.http import HttpResponse from django.utils.translation import ugettext as _, ugettext_noop as __ from django.conf import settings from modoboa.lib import u2u_decode, tables, imap_utf7, parameters from modoboa.lib.webutils import static_url, size2integer from modoboa.lib.connections import ConnectionsManager from modoboa.lib.email_listing import MBconnector, EmailListing from modoboa.lib.emailutils import * from imaputils import * from exceptions import * class WMtable(tables.Table): tableid = "emails" idkey = "imapid" select = tables.ImgColumn("select", cssclass="draggable left", width="1%", defvalue="%spics/grippy.png" % settings.MEDIA_URL, header="") flags = tables.ImgColumn("flags", width="2%") withatts = tables.ImgColumn("withatts", width="2%") subject = tables.Column("subject", label=__("Subject"), width="50%") from_ = tables.Column("from", width="20%", label=__("From")) date = tables.Column("date", width="20%", label=__("Date")) cols_order = ["select", "withatts", "flags", "subject", "from_", "date"] def parse(self, header, value): try: return getattr(IMAPheader, "parse_%s" % header)(value) except AttributeError: return value class IMAPheader(object): @staticmethod def parse_address(value, **kwargs): addr = EmailAddress(value) if "full" in kwargs.keys() and kwargs["full"]: return addr.fulladdress return addr.name and addr.name or addr.fulladdress @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_subject(value, **kwargs): return decode(u2u_decode.u2u_decode(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 kwargs.has_key("pattern"): self.parse_search_parameters(kwargs["criteria"], kwargs["pattern"]) else: self.mbc.criterions = [] EmailListing.__init__(self, **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 == "both": criterion = "from_addr, subject" criterions = "" for c in criterion.split(','): if c == "from_addr": key = "FROM" elif c == "subject": key = "SUBJECT" else: continue criterions = \ unicode(or_criterion(criterions, '%s "%s"' % (key, pattern))) self.mbc.criterions = [unicode("(%s)" % 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): def __init__(self, mbox, mailid, request, dformat="DISPLAYMODE", addrfull=False, links=0): mformat = parameters.get_user(request.user, "DISPLAYMODE") dformat = parameters.get_user(request.user, dformat) self.headers = [] self.attachments = {} self.imapc = get_imapconnector(request) msg = self.imapc.fetchmail(mbox, mailid) self.mbox = mbox self.mailid = mailid headers = msg['BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)]'] fallback_fmt = "html" if dformat == "plain" else "plain" self.bs = BodyStructure(msg['BODYSTRUCTURE']) data = None mformat = dformat if self.bs.contents.has_key(dformat) else fallback_fmt pnum = self.bs.contents[mformat]['pnum'] data = self.imapc._cmd("FETCH", mailid, "(BODY.PEEK[%s])" % pnum) content = self._decode_content(self.bs.contents[mformat]['encoding'], data[int(mailid)]['BODY[%s]' % pnum]) charset = self._find_content_charset(mformat) if charset is not None: content = content.decode(charset) self._find_attachments() self._fetch_inlines() self.body = \ getattr(self, "viewmail_%s" % mformat)(content, links=links) msg = email.message_from_string(headers) fields = ["Subject", "From", "To", "Reply-To", "Cc", "Date"] for f in fields: label = f if not f in msg.keys(): f = f.upper() if not f in msg.keys(): continue try: key = re.sub("-", "_", f).lower() value = getattr(IMAPheader, "parse_%s" % key)(msg[f], full=addrfull) self.headers += [{"name" : label, "value" : value}] except AttributeError: self.headers += [{"name" : label, "value" : msg[f]}] try: label = re.sub("-", "_", label) setattr(self, label, value) except: pass def _find_content_charset(self, subtype): for pos, elem in enumerate(self.bs.contents[subtype]["params"]): if elem == "charset": return self.bs.contents[subtype]["params"][pos + 1] return None def _decode_content(self, encoding, content): encoding = encoding.lower() if encoding == "base64": import base64 return base64.b64decode(content) elif encoding == "quoted-printable": import quopri return quopri.decodestring(content) return content def _find_attachments(self): for att in self.bs.attachments: attname = "part_%s" % att["pnum"] params = None key = None if att.has_key("params") and att["params"] != "NIL": params = att["params"] key = "name" if key is None and \ att.has_key("disposition") and len(att["disposition"]) > 1: params = att["disposition"][1] key = "filename" if key and params: for pos, value in enumerate(params): if value == key: attname = u2u_decode.u2u_decode(params[pos + 1]).strip("\r\t\n") break self.attachments[att["pnum"]] = attname def _fetch_inlines(self): for cid, params in self.bs.inlines.iteritems(): if re.search("\.\.", cid): continue fname = "static/tmp/%s_%s" % (self.mailid, cid) path = os.path.join(settings.MODOBOA_DIR, fname) params["fname"] = "/%s" % fname if os.path.exists(path): continue content = self.imapc.fetchpart(self.mailid, self.mbox, params["pnum"]) fp = open(path, "wb") fp.write(self._decode_content(params["encoding"], content)) fp.close() def map_cid(self, url): import re m = re.match(".*cid:(.+)", url) if m: if self.bs.inlines.has_key(m.group(1)): return self.bs.inlines[m.group(1)]["fname"] return url def render_headers(self, **kwargs): from django.template.loader import render_to_string 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 ReplyModifier(ImapEmail): def __init__(self, msg, user, form, all=False, **kwargs): ImapEmail.__init__(self, msg, user, dformat="EDITOR", **kwargs) self.textheader = "%s %s" % (self.From, _("wrote:")) getattr(self, "_modify_%s" % self.dformat)() form.fields["from_"].initial = user.username if not "Reply-To" in msg.keys(): form.fields["to"].initial = self.From else: form.fields["to"].initial = self.Reply_To if all: form.fields["cc"].initial = "" toparse = msg["To"].split(",") if "Cc" in msg.keys(): toparse += msg["Cc"].split(",") for addr in toparse: tmp = EmailAddress(addr) if tmp.address and tmp.address == 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): self.body = re.sub("", "", self.body) lines = self.body.split('\n') body = "" for l in lines: if body != "": body += "\n" body += ">%s" % l self.body = body def _modify_html(self): pass class ForwardModifier(ImapEmail): def __init__(self, msg, user, form, **kwargs): ImapEmail.__init__(self, msg, user, dformat="EDITOR", **kwargs) self._header(msg) form.fields["from_"].initial = user.username form.fields["subject"].initial = "Fwd: %s" % self.Subject def __getfunc(self, name): return getattr(self, "%s_%s" % (name, self.dformat)) def _header(self, msg): self.textheader = self.__getfunc("_header_begin")() + "\n" self.textheader += \ self.__getfunc("_header_line")(_("Subject"), self.Subject) + "\n" self.textheader += \ self.__getfunc("_header_line")(_("Date"), msg["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 -----
""" % _("Original message") def _header_line_plain(self, key, value): return "%s: %s" % (key, value) def _header_line_html(self, key, value): return " " % (key, value) def _header_end_plain(self): return "\n" def _header_end_html(self): return "
%s%s
" 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", "
", content) self._sig = """
---
%s""" % content def __repr__(self): return self._sig def find_images_in_body(body): """Looks for images inside a HTML body 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 off the MIME message if we want recipients to display them correctly. :param body: the HTML body to parse """ 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) dirname = os.path.dirname(o.path).replace(settings.MEDIA_URL, "") fname = os.path.basename(o.path) path = os.path.join(settings.MEDIA_ROOT, dirname, fname) if not os.path.exists(path): continue cid = "%s@modoboa" % os.path.splitext(fname)[0] tag.set("src", "cid:%s" % cid) fp = open(path, "rb") p = MIMEImage(fp.read()) fp.close() p["Content-ID"] = "<%s>" % cid ct = p["Content-Type"] p.replace_header("Content-Type", '%s; name="%s"' \ % (ct, os.path.basename(fname))) p["Content-Disposition"] = "inline" parts.append(p) return lxml.html.tostring(html), parts def set_compose_session(request): """Initialize a new "compose" session. It is used to keep track of attachments defined with a new message. Each new message will be associated with a unique ID (in order to avoid conflicts between users). :param request: a Request object. :return: the new unique ID. """ import uuid randid = str(uuid.uuid4()).replace("-", "") request.session["compose_mail"] = {"id" : randid, "attachments" : []} return randid def save_attachment(f): """Save a new attachment to the filesystem. The attachment is not saved using its own name to the filesystem. To avoid conflicts, a random name is generated and used instead. :param f: an uploaded file object (see Django's documentation) :return: the new random name """ from tempfile import NamedTemporaryFile dstdir = os.path.join(settings.MEDIA_ROOT, "tmp") try: fp = NamedTemporaryFile(dir=dstdir, delete=False) except Exception, e: raise WebmailError(str(e)) for chunk in f.chunks(): fp.write(chunk) fp.close() return fp.name def clean_attachments(attlist): """Remove all attachments from the filesystem :param attlist: a list of 2-uple. Each element must contain the following information : (random name, real name). """ for att in attlist: fullpath = os.path.join(settings.MEDIA_ROOT, "tmp", att["tmpname"]) try: os.remove(fullpath) except OSError, e: pass def html2plaintext(content): """HTML to plain text translation :param content: some HTML content """ 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 get_current_url(request): if not request.session.has_key("folder"): return "" res = "%s?page=%s" % (request.session["folder"], request.session["page"]) for p in ["criteria", "pattern", "order"]: if p in request.session.keys(): res += "&%s=%s" % (p, request.session[p]) return res def create_mail_attachment(attdef): """Create the MIME part corresponding to the given attachment. Mandatory keys: 'fname', 'tmpname', 'content-type' :param attdef: a dictionary containing the attachment definition :return: a MIMEBase object """ from email import Encoders from email.mime.base import MIMEBase maintype, subtype = attdef["content-type"].split("/") res = MIMEBase(maintype, subtype) fp = open(os.path.join(settings.MEDIA_ROOT, "tmp", attdef["tmpname"]), "rb") res.set_payload(fp.read()) fp.close() Encoders.encode_base64(res) res.add_header("Content-Disposition", "attachment; filename='%s'" % attdef["fname"]) return res def send_mail(request, origmsg=None, 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 origmsg: the eventual original message (in case of a reply) :param posturl: the url to post the message form to :return: a 2-uple (True|False, HttpResponse) """ from email.mime.multipart import MIMEMultipart from forms import ComposeMailForm from modoboa.lib.webutils import getctx, ajax_simple_response from modoboa.admin.models import Mailbox from modoboa.auth.lib import get_password form = ComposeMailForm(request.POST) editormode = parameters.get_user(request.user, "EDITOR") if form.is_valid(): from email.mime.text import MIMEText from email.utils import make_msgid, formatdate import smtplib body = request.POST["id_body"] charset = "utf-8" if editormode == "html": msg = MIMEMultipart(_subtype="related") submsg = MIMEMultipart(_subtype="alternative") textbody = html2plaintext(body) submsg.attach(MIMEText(textbody.encode(charset), _subtype="plain", _charset=charset)) body, images = find_images_in_body(body) submsg.attach(MIMEText(body.encode(charset), _subtype=editormode, _charset=charset)) msg.attach(submsg) for img in images: msg.attach(img) else: text = MIMEText(body.encode(charset), _subtype=editormode, _charset=charset) if len(request.session["compose_mail"]["attachments"]): msg = MIMEMultipart() msg.attach(text) else: msg = text for attdef in request.session["compose_mail"]["attachments"]: msg.attach(create_mail_attachment(attdef)) msg["Subject"] = form.cleaned_data["subject"] address, domain = split_mailbox(form.cleaned_data["from_"]) try: mb = Mailbox.objects.get(address=address, domain__name=domain) msg["From"] = "%s <%s>" % (mb.name, form.cleaned_data["from_"]) except Mailbox.DoesNotExist: msg["From"] = form.cleaned_data["from_"] msg["To"] = form.cleaned_data["to"] msg["Message-ID"] = make_msgid() msg["User-Agent"] = "Modoboa" msg["Date"] = formatdate(time.time(), True) if origmsg and origmsg.has_key("Message-ID"): msg["References"] = msg["In-Reply-To"] = origmsg["Message-ID"] rcpts = msg['To'].split(',') if form.cleaned_data["cc"] != "": msg["Cc"] = form.cleaned_data["cc"] rcpts += msg["Cc"].split(",") 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, text: raise WebmailError(str(text)) if parameters.get_admin("SMTP_AUTHENTICATION") == "yes": try: s.login(request.user.username, get_password(request)) except smtplib.SMTPAuthenticationError, e: raise WebmailError(str(e)) s.sendmail(msg['From'], rcpts, msg.as_string()) s.quit() sentfolder = parameters.get_user(request.user, "SENT_FOLDER") IMAPconnector(user=request.user.username, password=request.session["password"]).push_mail(sentfolder, msg) clean_attachments(request.session["compose_mail"]["attachments"]) del request.session["compose_mail"] return True, ajax_simple_response(getctx("ok", url=get_current_url(request))) listing = render_to_string("webmail/compose.html", {"form" : form, "body" : request.POST["id_body"].strip(), "posturl" : posturl}) return False, ajax_simple_response( getctx("ko", level=2, listing=listing, editor=editormode) ) class AttachmentUploadHandler(FileUploadHandler): """ Simple upload handler to limit the size of the attachments users can upload. """ def __init__(self, request=None): super(AttachmentUploadHandler, self).__init__(request) self.total_upload = 0 self.toobig = False self.maxsize = size2integer(parameters.get_admin("MAX_ATTACHMENT_SIZE")) def receive_data_chunk(self, raw_data, start): self.total_upload += len(raw_data) if self.total_upload >= self.maxsize: self.toobig = True raise SkipFile() return raw_data def file_complete(self, file_size): return None