fix: return receipts are now displayed correctly.

new: parameters module refactored. The API is more simple now, there is no need to provide a module name anymore, this last is automatically guessed. Also, a new 'label' field can be provide when declaring a new parameter. It will be used instead of the name.

new: email parsing refactored. My first attempt was really ugly. I did not really studied the various RFC describing how messages are formatted or what IMAP fetch queries can do. Now, I hope this new code will face better unknown types. At least, It will be simpler to update.
This commit is contained in:
tonio
2010-09-17 09:23:35 +02:00
parent 9b04aefbe0
commit c01d6de7d8
27 changed files with 398 additions and 257 deletions
+6 -6
View File
@@ -3,17 +3,17 @@
from modoboa.lib import parameters
from django.utils.translation import ugettext as _
parameters.register_admin("admin","STORAGE_PATH", type="string", deflt="/var/vmail",
parameters.register_admin("STORAGE_PATH", type="string", deflt="/var/vmail",
help=_("Path to the root directory where messages are stored"))
parameters.register_admin("admin", "VIRTUAL_UID", type="string", deflt="vmail",
parameters.register_admin("VIRTUAL_UID", type="string", deflt="vmail",
help=_("UID of the virtual user which owns domains/mailboxes/messages on the filesystem"))
parameters.register_admin("admin", "VIRTUAL_GID", type="string", deflt="vmail",
parameters.register_admin("VIRTUAL_GID", type="string", deflt="vmail",
help=_("GID of the virtual user which owns domains/mailboxes/messages on the filesystem"))
parameters.register_admin("admin", "MAILBOX_TYPE", type="list", deflt="maildir",
parameters.register_admin("MAILBOX_TYPE", type="list", deflt="maildir",
values=[("maildir", "maildir"), ("mbox", "mbox")],
help=_("Mailboxes storage format"))
parameters.register_admin("admin", "MAILDIR_ROOT", type="string", deflt=".maildir",
parameters.register_admin("MAILDIR_ROOT", type="string", deflt=".maildir",
help=_("Sub-directory (inside the mailbox) where messages are stored when using the maildir format"))
parameters.register_admin("admin", "PASSWORD_SCHEME", type="list", deflt="crypt",
parameters.register_admin("PASSWORD_SCHEME", type="list", deflt="crypt",
values=[("crypt", "crypt"), ("md5", "md5"), ("clear", "clear")],
help=_("Scheme used to crypt mailbox passwords"))
+8 -8
View File
@@ -19,17 +19,17 @@ class Domain(models.Model):
)
def create_dir(self):
path = "%s/%s" % (parameters.get_admin("admin", "STORAGE_PATH"), self.name)
path = "%s/%s" % (parameters.get_admin("STORAGE_PATH"), self.name)
return exec_as_vuser("mkdir -p %s" % path)
def rename_dir(self, newname):
stpath = parameters.get_admin("admin", "STORAGE_PATH")
stpath = parameters.get_admin("STORAGE_PATH")
return exec_as_vuser("mv %s/%s %s/%s" \
% (stpath, self.name, stpath, newname))
def delete_dir(self):
return exec_as_vuser("rm -r %s/%s" \
% (parameters.get_admin("admin", "STORAGE_PATH"),
% (parameters.get_admin("STORAGE_PATH"),
self.name))
def __str__(self):
@@ -57,14 +57,14 @@ class Mailbox(models.Model):
def __init__(self, *args, **kwargs):
super(Mailbox, self).__init__(*args, **kwargs)
self.mbtype = parameters.get_admin("admin", "MAILBOX_TYPE")
self.mdirroot = parameters.get_admin("admin", "MAILDIR_ROOT")
self.mbtype = parameters.get_admin("MAILBOX_TYPE")
self.mdirroot = parameters.get_admin("MAILDIR_ROOT")
def __str__(self):
return "%s" % (self.address)
def create_dir(self, domain):
path = "%s/%s/%s" % (parameters.get_admin("admin", "STORAGE_PATH"),
path = "%s/%s/%s" % (parameters.get_admin("STORAGE_PATH"),
domain.name, self.address)
if os.path.exists(path):
return True
@@ -86,7 +86,7 @@ class Mailbox(models.Model):
return True
def rename_dir(self, domain, newaddress):
path = "%s/%s" % (parameters.get_admin("admin", "STORAGE_PATH"), domain)
path = "%s/%s" % (parameters.get_admin("STORAGE_PATH"), domain)
code = exec_as_vuser("mv %s/%s %s/%s" \
% (path, self.address, path, newaddress))
if code:
@@ -95,7 +95,7 @@ class Mailbox(models.Model):
def delete_dir(self):
return exec_as_vuser("rm -r %s/%s/%s" \
% (parameters.get_admin("admin", "STORAGE_PATH"),
% (parameters.get_admin("STORAGE_PATH"),
self.domain.name, self.address))
class Alias(models.Model):
+3 -2
View File
@@ -97,10 +97,11 @@ def loadadminextmenu(user):
@register.simple_tag
def param(app, definition):
result = """<div class='row'>
<label>%s</label>""" % definition["name"]
<label>%s</label>""" % (definition.has_key("label") \
and definition["label"] or definition["name"])
name = "%s.%s" % (app, definition["name"])
value = definition.has_key("value") \
and definition["value"] or definition["default"]
and definition["value"] or definition["deflt"]
if definition["type"] in ["string", "int"]:
result += """
+5 -5
View File
@@ -182,8 +182,8 @@ def newmailbox(request, dom_id=None):
mb.password = crypt_password(request.POST["password1"])
mb.uid = pwd.getpwnam(parameters.get_admin("admin", "VIRTUAL_UID")).pw_uid
mb.gid = pwd.getpwnam(parameters.get_admin("admin", "VIRTUAL_GID")).pw_gid
mb.uid = pwd.getpwnam(parameters.get_admin("VIRTUAL_UID")).pw_uid
mb.gid = pwd.getpwnam(parameters.get_admin("VIRTUAL_GID")).pw_gid
mb.domain = domain
mb.quota = request.POST["quota"]
if not mb.quota:
@@ -422,9 +422,9 @@ def viewparameters(request):
for p in sorted(parameters._params[app]['A']):
param_def = parameters._params[app]['A'][p]
newdef = {"name" : p,
"value" : parameters.get_admin(app, p),
"value" : parameters.get_admin(p, app=app),
"help" : param_def["help"],
"default" : param_def["default"],
"default" : param_def["deflt"],
"type" : param_def["type"]}
if "values" in param_def.keys():
newdef["values"] = param_def["values"]
@@ -442,6 +442,6 @@ def saveparameters(request):
if pname == "update":
continue
app, name = pname.split('.')
parameters.save_admin(app, name, v)
parameters.save_admin(name, v, app=app)
ctx = getctx("ok")
return HttpResponse(simplejson.dumps(ctx), mimetype="application/json")
+1 -1
View File
@@ -8,7 +8,7 @@ if __name__ == "__main__":
main.init()
conn = db.getconnection("amavis_quarantine")
max_messages_age = int(parameters.get_admin("amavis_quarantine", "MAX_MESSAGES_AGE"))
max_messages_age = int(parameters.get_admin("MAX_MESSAGES_AGE"))
print "Deleting messages older than %d days..." % max_messages_age
db.execute(conn, """
+4 -4
View File
@@ -6,14 +6,14 @@ from modoboa.lib import parameters
class AMrelease(object):
def __init__(self):
mode = parameters.get_admin("amavis_quarantine", "AM_PDP_MODE")
mode = parameters.get_admin("AM_PDP_MODE")
if mode == "inet":
host = parameters.get_admin("amavis_quarantine", 'AM_PDP_HOST')
port = parameters.get_admin("amavis_quarantine", 'AM_PDP_PORT')
host = parameters.get_admin('AM_PDP_HOST')
port = parameters.get_admin('AM_PDP_PORT')
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect((host, int(port)))
else:
path = parameters.get_admin("amavis_quarantine", 'AM_PDP_SOCKET')
path = parameters.get_admin('AM_PDP_SOCKET')
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.sock.connect(path)
+5 -5
View File
@@ -16,20 +16,20 @@ def infos():
def init():
events.register("UserMenuDisplay", menu)
parameters.register_admin("amavis_quarantine", "MAX_MESSAGES_AGE", type="int",
parameters.register_admin("MAX_MESSAGES_AGE", type="int",
deflt=14,
help=_("Quarantine messages maximum age (in days) before deletion"))
parameters.register_admin("amavis_quarantine", "AM_PDP_MODE", type="list",
parameters.register_admin("AM_PDP_MODE", type="list",
deflt="unix",
values=[("inet", "inet"), ("unix", "unix")],
help=_("Mode used to access the PDP server"))
parameters.register_admin("amavis_quarantine", "AM_PDP_HOST", type="string",
parameters.register_admin("AM_PDP_HOST", type="string",
deflt="localhost",
help=_("PDP server address (if inet mode)"))
parameters.register_admin("amavis_quarantine", "AM_PDP_PORT", type="int",
parameters.register_admin("AM_PDP_PORT", type="int",
deflt=9998,
help=_("PDP server port (if inet mode)"))
parameters.register_admin("amavis_quarantine", "AM_PDP_SOCKET", type="string",
parameters.register_admin("AM_PDP_SOCKET", type="string",
deflt="/var/amavis/amavisd.sock",
help=_("Path to the PDP server socket (if unix mode)"))
+1 -2
View File
@@ -89,8 +89,7 @@ WHERE quarantine.mail_id='%s'
mail = SQLemail(msg, mode, links)
return _render(request, "common/viewmail.html", {
"headers" : mail.render_headers(),
"mailbody" : mail.body,
"pre" : mail.pre
"mailbody" : mail.body
})
@login_required
+1 -1
View File
@@ -28,7 +28,7 @@ def init():
events.register("ModifyMailbox", onModifyMailbox)
events.register("UserMenuDisplay", menu)
parameters.register_admin("postfix_autoreply", "AUTOREPLIES_TIMEOUT",
parameters.register_admin("AUTOREPLIES_TIMEOUT",
type="int", deflt=86400,
help=_("Timeout in seconds between two auto-replies to the same recipient"))
@@ -31,7 +31,8 @@ if __name__ == "__main__":
try:
lastar = ARhistoric.objects.get(armessage=armessage.id, sender=sender)
main.init()
timeout = parameters.get_admin("postfix_autoreply", "AUTOREPLIES_TIMEOUT")
timeout = parameters.get_admin("AUTOREPLIES_TIMEOUT",
app="postfix_autoreply")
delta = datetime.timedelta(seconds=int(timeout))
if lastar.last_sent + delta > datetime.datetime.now():
sys.exit(0)
+2 -2
View File
@@ -73,8 +73,8 @@ def str2Time(y, M, d, h ="0", m ="0", s="0"):
class Grapher(object):
def __init__(self):
self.rrd_rootdir = parameters.get_admin("stats", "RRD_ROOTDIR")
self.img_rootdir = parameters.get_admin("stats", "IMG_ROOTDIR")
self.rrd_rootdir = parameters.get_admin("RRD_ROOTDIR")
self.img_rootdir = parameters.get_admin("IMG_ROOTDIR")
def process(self, target, suffix, start, end, tpl=traffic_avg_template):
rrdfile = "%s/%s.rrd" % (self.rrd_rootdir, target)
+3 -3
View File
@@ -21,13 +21,13 @@ def infos():
def init():
events.register("AdminMenuDisplay", menu)
parameters.register_admin("stats", "LOGFILE", type="string",
parameters.register_admin("LOGFILE", type="string",
deflt="/var/log/mail.log",
help=_("Path to log file used to collect statistics"))
parameters.register_admin("stats", "RRD_ROOTDIR", type="string",
parameters.register_admin("RRD_ROOTDIR", type="string",
deflt="/tmp/modoboa",
help=_("Path to directory where RRD files are stored"))
parameters.register_admin("stats", "IMG_ROOTDIR", type="string",
parameters.register_admin("IMG_ROOTDIR", type="string",
deflt="/tmp/modoboa",
help=_("Path to directory where PNG files are stored"))
+2 -2
View File
@@ -261,8 +261,8 @@ if __name__ == "__main__":
main.init()
log_file = parameters.get_admin("stats", "LOGFILE")
rrd_rootdir = parameters.get_admin("stats", "RRD_ROOTDIR")
log_file = parameters.get_admin("LOGFILE")
rrd_rootdir = parameters.get_admin("RRD_ROOTDIR")
parser = OptionParser()
parser.add_option("-l","--logfile", default=log_file,
+1 -1
View File
@@ -5,7 +5,7 @@ from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from django.utils import simplejson
from django.template.loader import render_to_string
from modoboa.lib import parameters, _render, getctx
from modoboa.lib import _render, getctx
from modoboa.admin.views import good_domain
from modoboa.admin.models import Domain, Mailbox
from django.contrib.auth.decorators \
+136 -21
View File
@@ -154,8 +154,8 @@ class IMAPconnector(object):
def __init__(self, user=None, password=None):
self.criterions = []
self.address = parameters.get_admin("webmail", "IMAP_SERVER")
self.port = int(parameters.get_admin("webmail", "IMAP_PORT"))
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)
@@ -177,7 +177,7 @@ class IMAPconnector(object):
def login(self, user, passwd):
import imaplib
try:
secured = parameters.get_admin("webmail", "IMAP_SECURED")
secured = parameters.get_admin("IMAP_SECURED")
if secured == "yes":
self.m = imaplib.IMAP4_SSL(self.address, self.port)
else:
@@ -278,10 +278,8 @@ class IMAPconnector(object):
md_folders = [{"name" : "INBOX", "class" : "inbox"},
{"name" : 'Drafts'},
{"name" : 'Junk'},
{"name" : parameters.get_user(user, "webmail",
"SENT_FOLDER")},
{"name" : parameters.get_user(user, "webmail",
"TRASH_FOLDER"),
{"name" : parameters.get_user(user, "SENT_FOLDER")},
{"name" : parameters.get_user(user, "TRASH_FOLDER"),
"class" : "trash"}]
md_folders += self._listfolders(md_folders=md_folders)
for fd in md_folders:
@@ -341,6 +339,15 @@ class IMAPconnector(object):
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):
if not start and not stop:
return []
@@ -360,6 +367,7 @@ class IMAPconnector(object):
if not folder:
folder = "INBOX"
tmpdict = {}
for response_part in data:
if isinstance(response_part, tuple):
imapid = response_part[0].split()[0]
@@ -392,7 +400,7 @@ class ImapListing(EmailListing):
self.mbc.criterions = []
EmailListing.__init__(self, **kwargs)
self.extravars["refreshrate"] = \
int(parameters.get_user(user, "webmail", "REFRESH_INTERVAL")) * 1000
int(parameters.get_user(user, "REFRESH_INTERVAL")) * 1000
def parse_search_parameters(self, criterion, pattern):
def or_criterion(old, c):
@@ -422,8 +430,11 @@ class ImapListing(EmailListing):
class ImapEmail(Email):
def __init__(self, msg, addrfull=False, *args, **kwargs):
Email.__init__(self, msg, *args, **kwargs)
def __init__(self, msg, user, dformat="DISPLAYMODE", addrfull=False,
*args, **kwargs):
mformat = parameters.get_user(user, "DISPLAYMODE")
dformat = parameters.get_user(user, dformat)
Email.__init__(self, msg, mformat, dformat, *args, **kwargs)
fields = ["Subject", "From", "To", "Reply-To", "Cc", "Date"]
for f in fields:
@@ -447,19 +458,124 @@ class ImapEmail(Email):
def render_headers(self, **kwargs):
from django.template.loader import render_to_string
attachments = []
for part in self.attachments:
decoded = decode_header(part.get_filename())
if decoded[0][1] is None:
attachments += [decoded[0][0]]
else:
attachments += [unicode(decoded[0][0], decoded[0][1])]
return render_to_string("webmail/headers.html", {
"headers" : self.headers,
"folder" : kwargs["folder"], "mail_id" : kwargs["mail_id"],
"attachments" : attachments != [] and attachments or None
"attachments" : self.attachments != {} and self.attachments or None
})
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 == 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):
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 -----<br/>
<table border='0'>""" % _("Original message")
def _header_line_plain(self, key, value):
return "%s: %s" % (key, value)
def _header_line_html(self, key, value):
return " <tr><td>%s</td><td>%s</td></tr>" % (key, value)
def _header_end_plain(self):
return "\n"
def _header_end_html(self):
return "</table>"
class EmailSignature(object):
"""User signature
:param user: User object
"""
def __init__(self, user):
dformat = parameters.get_user(user, "EDITOR")
content = parameters.get_user(user, "SIGNATURE")
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", "<br/>", content)
self._sig = """<br/>
---<br/>
%s""" % content
def __repr__(self):
return self._sig
def find_images_in_body(body):
"""Looks for images inside a HTML body
@@ -497,7 +613,7 @@ def encrypt(clear):
from Crypto.Cipher import AES
import base64
obj = AES.new(parameters.get_admin("webmail", "SECRET_KEY"),
obj = AES.new(parameters.get_admin("SECRET_KEY"),
AES.MODE_ECB)
if len(clear) % AES.block_size:
clear += "\0" * (AES.block_size - len(clear) % AES.block_size)
@@ -509,8 +625,7 @@ def decrypt(ciph):
from Crypto.Cipher import AES
import base64
obj = AES.new(parameters.get_admin("webmail", "SECRET_KEY"),
AES.MODE_ECB)
obj = AES.new(parameters.get_admin("SECRET_KEY"), AES.MODE_ECB)
ciph = base64.b64decode(ciph)
clear = obj.decrypt(ciph)
return clear.rstrip('\0')
+22 -14
View File
@@ -9,40 +9,48 @@ def init():
events.register("UserLogin", userlogin)
events.register("UserLogout", userlogout)
parameters.register_admin("webmail", "SECRET_KEY", type="string",
deflt="abcdefghijklmnop",
parameters.register_admin("SECRET_KEY", type="string", deflt="abcdefghijklmnop",
help=_("Key used to encrypt/decrypt passwords"))
parameters.register_admin("webmail", "IMAP_SERVER", type="string",
parameters.register_admin("IMAP_SERVER", type="string",
deflt="127.0.0.1",
help=_("Address of your IMAP server"))
parameters.register_admin("webmail", "IMAP_SECURED", type="list_yesno",
parameters.register_admin("IMAP_SECURED", type="list_yesno",
deflt="no",
help=_("Use a secured connection to access IMAP server"))
parameters.register_admin("webmail", "IMAP_PORT", type="int", deflt="143",
parameters.register_admin("IMAP_PORT", type="int", deflt="143",
help=_("Listening port of your IMAP server"))
parameters.register_admin("webmail", "SMTP_SERVER", type="string",
parameters.register_admin("SMTP_SERVER", type="string",
deflt="127.0.0.1",
help=_("Address of your SMTP server"))
parameters.register_admin("webmail", "SMTP_PORT", type="int", deflt="25",
parameters.register_admin("SMTP_PORT", type="int", deflt="25",
help=_("Listening port of your SMTP server"))
parameters.register_admin("webmail", "SMTP_AUTHENTICATION", type="list_yesno",
parameters.register_admin("SMTP_AUTHENTICATION", type="list_yesno",
deflt="no",
help=_("Server needs authentication"))
parameters.register_admin("webmail", "SMTP_SECURED", type="list_yesno",
parameters.register_admin("SMTP_SECURED", type="list_yesno",
deflt="no",
help=_("Use a secured connection to access SMTP server"))
parameters.register_user("webmail", "REFRESH_INTERVAL", type="int", deflt=300,
parameters.register_user("REFRESH_INTERVAL", type="int", deflt=300,
label=_("Listing refresh rate"),
help=_("Automatic folder refresh rate (in seconds)"))
parameters.register_user("webmail", "TRASH_FOLDER", type="string", deflt="Trash",
parameters.register_user("TRASH_FOLDER", type="string", deflt="Trash",
label=_("Trash folder"),
help=_("Folder where deleted messages go"))
parameters.register_user("webmail", "SENT_FOLDER", type="string", deflt="Sent",
parameters.register_user("SENT_FOLDER", type="string", deflt="Sent",
label=_("Sent folder"),
help=_("Folder where copies of sent messages go"))
parameters.register_user("webmail", "SIGNATURE", type="text", default="",
parameters.register_user("SIGNATURE", type="text", deflt="",
label=_("Signature text"),
help=_("User defined email signature"))
parameters.register_user("webmail", "EDITOR", type="list", deflt="text",
parameters.register_user("EDITOR", type="list", deflt="plain",
label=_("Default editor"),
values=[("html", "html"), ("plain", "text")],
help=_("The default editor to use when composing a message"))
parameters.register_user("DISPLAYMODE", type="list", deflt="plain",
label=_("Default message display mode"),
values=[("html", "html"), ("plain", "text")],
help=_("The default mode used when displaying a message"))
def destroy():
events.unregister("UserMenuDisplay", menu)
+2 -2
View File
@@ -7,8 +7,8 @@
<th>{% trans "Attachments" %}</th>
<td>
{% url extensions.webmail.views.getattachment folder mail_id as test %}
{% for attachment in attachments %}
<a href="{{ test }}?fname={{ attachment }}">{{ attachment }}</a>
{% for key, fname in attachments.items %}
<a href="{{ test }}?fname={{ fname }}&partnumber={{ key }}">{{ fname }}</a>
{% endfor %}
</td>
</tr>
+1 -1
View File
@@ -87,7 +87,7 @@ def listing_menu(selection, folder, user):
]
}
]
if folder == parameters.get_user(user, "webmail", "TRASH_FOLDER"):
if folder == parameters.get_user(user, "TRASH_FOLDER"):
entries[2]["menu"] += [
{"name" : "fdaction",
"label" : _("Empty folder"),
+51 -87
View File
@@ -116,43 +116,48 @@ def getmailcontent(request, folder, mail_id):
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, mode="html", links="1")
email = ImapEmail(msg, request.user, links="1")
try:
pageid = request.session["page"]
except KeyError:
pageid = "1"
return _render(request, "common/viewmail.html", {
"headers" : email.render_headers(folder=folder, mail_id=mail_id),
"folder" : folder, "imapid" : mail_id, "mailbody" : email.body,
"pre" : email.pre
"folder" : folder, "imapid" : mail_id, "mailbody" : email.body
})
@login_required
@is_not_localadmin()
def getattachment(request, folder, mail_id):
msg = fetchmail(request, folder, mail_id, True)
for part in msg.walk():
fname = part.get_filename()
if fname is None:
continue
decoded = decode_header(fname)
if decoded[0][1] is None:
fname = unicode(decoded[0][0])
else:
fname = unicode(decoded[0][0], decoded[0][1])
if fname == request.GET["fname"]:
resp = HttpResponse(part.get_payload(decode=1))
for hdr in ["Content-Type", "Content-Transfer-Encoding"]:
if request.GET.has_key("partnumber"):
headers = {"Content-Type" : "text/plain",
"Content-Transfer-Encoding" : None}
icon = IMAPconnector(user=request.user.username,
password=request.session["password"])
part = icon.fetchpart(mail_id, folder, request.GET["partnumber"])
if part is not None:
if part.get_content_maintype() == "message":
payload = part.get_payload(0)
else:
payload = part.get_payload(decode=True)
resp = HttpResponse(payload)
for hdr, default in headers.iteritems():
if not part.has_key(hdr):
continue
if default is not None:
resp[hdr] = default
else:
continue
resp[hdr] = re.sub("\s", "", part[hdr])
# I would add this part into the previous loop if was able
# to use functions as default values... but I'm a bit lazy
# :p
if part.has_key("Content-Disposition"):
resp["Content-Disposition"] = \
re.sub("\s", "", part["Content-Disposition"])
else:
resp["Content-Disposition"] = \
"attachment; filename=%s" % fname
resp["Content-Length"] = len(resp.content)
"attachment; filename=%s" % request.GET["fname"]
resp["Content-Length"] = len(payload)
return resp
raise Http404
@@ -172,8 +177,7 @@ def move(request):
def delete(request, fdname, mail_id):
mbc = IMAPconnector(user=request.user.username,
password=request.session["password"])
mbc.move(mail_id, fdname, parameters.get_user(request.user, "webmail",
"TRASH_FOLDER"))
mbc.move(mail_id, fdname, parameters.get_user(request.user, "TRASH_FOLDER"))
ctx = getctx("ok", next=__get_current_url(request))
return HttpResponse(simplejson.dumps(ctx), mimetype="application/json")
@@ -193,7 +197,7 @@ def mark(request, name):
@login_required
@is_not_localadmin()
def empty(request, name):
if name == parameters.get_user(request.user, "webmail", "TRASH_FOLDER"):
if name == parameters.get_user(request.user, "TRASH_FOLDER"):
mbc = IMAPconnector(user=request.user.username,
password=request.session["password"])
mbc.empty(name)
@@ -207,22 +211,23 @@ def compact(request, name):
mbc.compact(name)
return folder(request, name, False)
def render_compose(request, form, posturl, bodyheader=None, body=None,
insert_signature=False):
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, "webmail", "EDITOR")
editor = parameters.get_user(request.user, "EDITOR")
if email is None:
body = ""
textheader = ""
else:
body = email.body
textheader = email.textheader
if insert_signature:
if body == None:
body = ""
signature = parameters.get_user(request.user, "webmail", "SIGNATURE")
if signature != "":
body += editor == "plain" and "\n---\n%s" % signature \
or "<br/>---<br/>%s" % re.sub("\n", "<br/>", signature)
signature = EmailSignature(request.user)
body += str(signature)
content = render_to_string("webmail/compose.html", {
"form" : form, "bodyheader" : bodyheader, "body" : body,
"posturl" : posturl
"form" : form, "bodyheader" : textheader,
"body" : body, "posturl" : posturl
})
mbc = IMAPconnector(user=request.user.username,
password=request.session["password"])
@@ -240,7 +245,7 @@ def send_mail(request, withctx=False, origmsg=None, posturl=None):
from email.utils import make_msgid, formatdate
import smtplib
subtype = parameters.get_user(request.user, "webmail", "EDITOR")
subtype = parameters.get_user(request.user, "EDITOR")
body = request.POST["id_body"]
if subtype == "html":
from email.mime.multipart import MIMEMultipart
@@ -267,17 +272,17 @@ def send_mail(request, withctx=False, origmsg=None, posturl=None):
rcpts += msg["Cc"].split(",")
error = None
try:
s = smtplib.SMTP(parameters.get_admin("webmail", "SMTP_SERVER"))
if parameters.get_admin("webmail", "SMTP_SECURED") == "yes":
s = smtplib.SMTP(parameters.get_admin("SMTP_SERVER"))
if parameters.get_admin("SMTP_SECURED") == "yes":
s.starttls()
except (smtplib.SMTPException, ssl.SSLError), text:
error = text
if error is None:
if parameters.get_admin("webmail", "SMTP_AUTHENTICATION") == "yes":
if parameters.get_admin("SMTP_AUTHENTICATION") == "yes":
s.login(request.user.username, decrypt(request.session["password"]))
s.sendmail(msg['From'], rcpts, msg.as_string())
s.quit()
sentfolder = parameters.get_user(request.user, "webmail", "SENT_FOLDER")
sentfolder = parameters.get_user(request.user, "SENT_FOLDER")
IMAPconnector(user=request.user.username,
password=request.session["password"]).push_mail(sentfolder, msg)
ctx = getctx("ok", url=__get_current_url(request))
@@ -315,39 +320,12 @@ def reply(request, folder, mail_id):
password=request.session["password"]).msg_answered(folder,
mail_id)
return r
email = ImapEmail(msg, True)
lines = email.body.split('\n')
body = ""
for l in lines:
if body != "":
body += "\n"
body += ">%s" % l
form = ComposeMailForm()
form.fields["from_"].initial = request.user.username
if not "Reply-To" in msg.keys():
form.fields["to"].initial = email.From
else:
form.fields["to"].initial = email.Reply_To
if request.GET.has_key("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 == 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*.+", email.Subject.lower())
if m:
form.fields["subject"].initial = email.Subject
else:
form.fields["subject"].initial = "Re: %s" % email.Subject
textheader = "%s %s" % (email.From, _("wrote:"))
form = ComposeMailForm()
email = ReplyModifier(msg, request.user, form, request.GET.has_key("full"),
addrfull=True)
return render_compose(request, form, reverse(reply, args=[folder, mail_id]),
textheader, body)
email)
@login_required
@is_not_localadmin()
@@ -360,23 +338,9 @@ def forward(request, folder, mail_id):
password=request.session["password"]).msgforwarded(folder,
mail_id)
return response
msg = fetchmail(request, folder, mail_id, True)
email = ImapEmail(msg, True)
textheader = "----- %s -----" % _("Original message")
textheader += "\n%s: %s" % (_("Subject"), email.Subject)
textheader += "\n%s: %s" % (_("Date"), msg["Date"])
for hdr in ["From", "To", "Reply-To"]:
try:
key = re.sub("-", "_", hdr)
value = getattr(email, key)
textheader += "\n%s: %s" % (_(hdr), value)
except:
pass
textheader += "\n"
form = ComposeMailForm()
form.fields["from_"].initial = request.user.username
form.fields["subject"].initial = "Fwd: %s" % email.Subject
email = ForwardModifier(msg, request.user, form, addrfull=True)
return render_compose(request, form, reverse(forward, args=[folder, mail_id]),
textheader, email.body)
email)
+5 -2
View File
@@ -26,7 +26,7 @@ def _render_error(request, errortpl="error", user_context={}):
def exec_as_vuser(cmd):
code, output = exec_cmd("sudo -u %s %s" \
% (parameters.get_admin("admin", "VIRTUAL_UID"), cmd))
% (parameters.get_admin("VIRTUAL_UID", app="admin"), cmd))
if code:
exec_cmd("echo '%s' >> /tmp/vmail.log" % output)
return False
@@ -57,7 +57,10 @@ def getctx(status, level=1, callback=None, **kwargs):
ctx[kw] = v
return ctx
def decode(s, encodings=('utf8', 'latin1', 'windows-1252', 'ascii')):
def decode(s, encodings=('utf8', 'latin1', 'windows-1252', 'ascii'), charset=None):
if charset is not None:
return s.decode(charset, 'ignore')
for encoding in encodings:
try:
return s.decode(encoding)
+2 -2
View File
@@ -27,7 +27,7 @@ class SimpleBackend:
return None
def _check_password(password, crypted):
scheme = parameters.get_admin("admin", "PASSWORD_SCHEME")
scheme = parameters.get_admin("PASSWORD_SCHEME", app="admin")
if scheme == "crypt":
return crypt.crypt(password, crypted) == crypted
if scheme == "md5":
@@ -35,7 +35,7 @@ def _check_password(password, crypted):
return password
def crypt_password(password):
scheme = parameters.get_admin("admin", "PASSWORD_SCHEME")
scheme = parameters.get_admin("PASSWORD_SCHEME", app="admin")
if scheme == "crypt":
salt = ''.join(Random().sample(string.letters + string.digits, 2))
return crypt.crypt(password, salt)
+98 -62
View File
@@ -140,67 +140,109 @@ class EmailListing:
return _render(request, self.tpl, context)
class Email(object):
def __init__(self, msg, mode="plain", links="0"):
def __init__(self, msg, mformat="plain", dformat="plain", links="0"):
self.attached_map = {}
contents = {"html" : "", "plain" : ""}
self.contents = {"html" : "", "plain" : ""}
self.headers = []
self.attachments = []
for part in msg.walk():
if part.get_content_maintype() == 'multipart':
continue
if part.has_key("Content-Disposition"):
if re.match("^attachment", part["Content-Disposition"]):
self.attachments += [part]
continue
if part.get_content_type() == "text/calendar":
contents["plain"] += part.get_payload(decode=True)
continue
if part.get_content_type() in ("text/html", "text/plain"):
contents[part.get_content_subtype()] += part.get_payload(decode=True)
if mode != "html" or links == "0":
continue
if part.get_content_maintype() == "application":
self.attachments += [part]
continue
self.attachments = {}
self.mformat = mformat
self.dformat = dformat
self.links = links
if part.get_content_maintype() == "image":
m = re.match("<(.+)>", part["Content-ID"])
cid = m is not None and m.group(1) or part["Content-ID"]
self.__parse(msg)
if part.has_key("Content-Location"):
fname = part["Content-Location"]
elif part.has_key("Content-Disposition"):
if not part["Content-Disposition"].startswith("inline"):
continue
params = part["Content-Disposition"].split(";")
fname = None
for p in params:
lst = p.strip().split("=")
if len(p) and p[0] == "filename":
fname = p[1]
break
if fname is None:
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() == "calendar":
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
elif part.has_key("Content-Type"):
m = re.match(".*name=[\"'](.+)[\"']", part["Content-Type"])
if m is None:
continue
fname = m.group(1)
else:
continue
else:
# I give up for now :p
return
self.attached_map[cid] = re.match("^http:", fname) and fname \
or self.__save_image(fname, part)
or self.__save_image(fname, msg)
return
self.__parse_default(msg, level)
def __parse(self, msg, level=None):
"""Recursive email parser
if not contents.has_key(mode) or contents[mode] == "":
if mode == "html":
mode = "plain"
else:
mode = "html"
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).
self.pre, self.body = \
getattr(self, "viewmail_%s" % mode)(contents[mode], links=links)
: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.
@@ -226,7 +268,6 @@ class Email(object):
m = re.match(".*cid:(.+)", url)
if m:
print self.attached_map
if self.attached_map.has_key(m.group(1)):
return self.attached_map[m.group(1)]
return url
@@ -236,14 +277,9 @@ class Email(object):
"headers" : self.headers,
})
def render(self, request):
return _render(request, 'common/getmailcontent.html', {
"body" : self.body, "pre" : self.pre
})
def viewmail_plain(self, content, **kwargs):
body = decode(content)
return (True, body)
return self.dformat == "html" and "<pre>%s</pre>" % content \
or content
def viewmail_html(self, content, **kwargs):
if content is None or content == "":
@@ -260,8 +296,8 @@ class Email(object):
else:
body = lxml.html.tostring(body)
body = re.sub("<(/?)body", lambda m: "<%sdiv" % m.group(1), body)
body = Template(decode(body)).render(Context({}))
return (False, body)
body = Template(body).render(Context({}))
return body
def parse_search_parameters(request):
if request.GET.has_key("pattern"):
+30 -12
View File
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
import string
import inspect
import re
from modoboa.lib.models import *
"""
@@ -31,25 +33,34 @@ def __is_defined(app, level, name):
or not name in _params[app][level].keys():
raise NotDefined(app, name)
def __register(app, level, name, type="string", deflt=None, help=None, **kwargs):
def __register(app, level, name, **kwargs):
"""Register a new parameter.
app corresponds to a core component (admin, main) or an extension.
"""
"""
if not app in _params.keys():
_params[app] = {}
for lvl in _levels.keys():
_params[app][lvl] = {}
if not level in _levels.keys():
return
_params[app][level][name] = {"type" : type, "default" : deflt, "help" : help}
_params[app][level][name] = kwargs
for k, v in kwargs.iteritems():
_params[app][level][name][k] = v
def register_admin(app, name, **kwargs):
def __guess_extension():
modname = inspect.getmodule(inspect.stack()[2][0]).__name__
m = re.match("(?:modoboa\.)?(?:extensions\.)?([^\.$]+)", modname)
if m:
return m.group(1)
return None
def register_admin(name, **kwargs):
app = kwargs.has_key("app") and kwargs["app"] or __guess_extension()
return __register(app, 'A', name, **kwargs)
def register_user(app, name, **kwargs):
def register_user(name, **kwargs):
app = kwargs.has_key("app") and kwargs["app"] or __guess_extension()
return __register(app, 'U', name, **kwargs)
def unregister_app(app):
@@ -58,9 +69,10 @@ def unregister_app(app):
del _params[app]
return True
def save_admin(app, name, value):
def save_admin(name, value, app=None):
if app is None:
app = __guess_extension()
__is_defined(app, 'A', name)
fullname = "%s.%s" % (app, name)
try:
p = Parameter.objects.get(name=fullname)
@@ -72,7 +84,9 @@ def save_admin(app, name, value):
p.save()
return True
def save_user(user, app, name, value):
def save_user(user, name, value, app=None):
if app is None:
app = __guess_extension()
__is_defined(app, 'U', name)
fullname = "%s.%s" % (app, name)
try:
@@ -86,18 +100,22 @@ def save_user(user, app, name, value):
p.save()
return True
def get_admin(app, name):
def get_admin(name, app=None):
if app is None:
app = __guess_extension()
__is_defined(app, "A", name)
try:
p = Parameter.objects.get(name="%s.%s" % (app, name))
except Parameter.DoesNotExist:
return _params[app]["A"][name]["default"]
return _params[app]["A"][name]["deflt"]
return p.value.decode("string_escape")
def get_user(user, app, name):
def get_user(user, name, app=None):
if app is None:
app = __guess_extension()
__is_defined(app, "U", name)
try:
p = UserParameter.objects.get(user=user, name="%s.%s" % (app, name))
except UserParameter.DoesNotExist:
return _params[app]["U"][name]["default"]
return _params[app]["U"][name]["deflt"]
return p.value.decode("string_escape")
-1
View File
@@ -1 +0,0 @@
{% if pre %}<pre>{% endif %}{{ body }}{% if pre %}</pre>{% endif %}
+1 -1
View File
@@ -49,6 +49,6 @@
<div id="table-container">
{{ headers }}
</div>
{% if pre %}<pre>{% endif %}{{ mailbody }}{% if pre %}</pre>{% endif %}
{{ mailbody|safe }}
</body>
</html>
+1 -1
View File
@@ -25,7 +25,7 @@ if settings.DEBUG:
urlpatterns += patterns(
'',
(r'^img/stats/(?P<path>.*)/$', 'django.views.static.serve',
{'document_root': parameters.get_admin("stats", "IMG_ROOTDIR")})
{'document_root': parameters.get_admin("IMG_ROOTDIR", app="stats")})
)
+5 -8
View File
@@ -1,3 +1,4 @@
import copy
from django.shortcuts import get_object_or_404, render_to_response
from django.template.loader import render_to_string
from django.template import RequestContext
@@ -55,13 +56,9 @@ def preferences(request):
tmp = {"name" : app, "params" : []}
for p in sorted(parameters._params[app]['U']):
param_def = parameters._params[app]['U'][p]
newdef = {"name" : p,
"value" : parameters.get_user(request.user, app, p),
"help" : param_def["help"],
"default" : param_def["default"],
"type" : param_def["type"]}
if "values" in param_def.keys():
newdef["values"] = param_def["values"]
newdef = copy.deepcopy(param_def)
newdef["name"] = p
newdef["value"] = parameters.get_user(request.user, p, app=app)
tmp["params"] += [newdef]
gparams += [tmp]
@@ -75,7 +72,7 @@ def savepreferences(request):
if pname == "update":
continue
app, name = pname.split('.')
parameters.save_user(request.user, app, name, v)
parameters.save_user(request.user, name, v, app=app)
ctx = getctx("ok")
return HttpResponse(simplejson.dumps(ctx), mimetype="application/json")