mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-18 05:51:26 +02:00
Merge branch 'master' into feature/radicale
Conflicts: modoboa/extensions/admin/forms/account.py
This commit is contained in:
@@ -41,7 +41,7 @@ Then, you need to register this application using the provided
|
||||
API. Just copy/paste the following example into the :file:`__init__.py` file
|
||||
of the future extension::
|
||||
|
||||
from modoboa.extensions import ModoExtension, exts_pool
|
||||
from modoboa.core.extensions import ModoExtension, exts_pool
|
||||
|
||||
class MyExtension(ModoExtension):
|
||||
name = "myext"
|
||||
|
||||
@@ -58,16 +58,16 @@ Replace values between ``<>`` by yours.
|
||||
Edit the :file:`/etc/postfix/main.cf` file and copy the following
|
||||
lines inside::
|
||||
|
||||
relay_domains = <driver>:/etc/postfix/maps/sql-relaydomains.cf
|
||||
relay_domains = <driver>:/etc/postfix/sql-relaydomains.cf
|
||||
transport_maps =
|
||||
<driver>:/etc/postfix/maps/sql-relaydomains-transport.cf
|
||||
<driver>:/etc/postfix/maps/sql-relaydomain-aliases-transport.cf
|
||||
<driver>:/etc/postfix/sql-relaydomains-transport.cf
|
||||
<driver>:/etc/postfix/sql-relaydomain-aliases-transport.cf
|
||||
|
||||
smtpd_recipient_restrictions =
|
||||
permit_mynetworks
|
||||
reject_unauth_destination
|
||||
check_recipient_access
|
||||
<driver>:/etc/postfix/maps/sql-relay-recipient-verification.cf
|
||||
<driver>:/etc/postfix/sql-relay-recipient-verification.cf
|
||||
|
||||
Replace ``<driver>`` by the name of the database you use.
|
||||
|
||||
@@ -332,11 +332,11 @@ configuration files as follows:
|
||||
|
||||
``/etc/postfix/main.cf``::
|
||||
|
||||
transport_maps = mysql:/etc/postfix/maps/sql-autoreplies-transport.cf
|
||||
virtual_alias_maps = <driver>:/etc/postfix/maps/sql-aliases.cf
|
||||
<driver>:/etc/postfix/mapfiles/sql-domain-aliases-mailboxes.cf,
|
||||
<driver>:/etc/postfix/maps/sql-autoreplies.cf,
|
||||
<driver>:/etc/postfix/mapfiles/sql-catchall-aliases.cf
|
||||
transport_maps = <driver>:/etc/postfix/sql-autoreplies-transport.cf
|
||||
virtual_alias_maps = <driver>:/etc/postfix/sql-aliases.cf
|
||||
<driver>:/etc/postfix/sql-domain-aliases-mailboxes.cf,
|
||||
<driver>:/etc/postfix/sql-autoreplies.cf,
|
||||
<driver>:/etc/postfix/sql-catchall-aliases.cf
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -345,7 +345,7 @@ configuration files as follows:
|
||||
``/etc/postfix/master.cf``::
|
||||
|
||||
autoreply unix - n n - - pipe
|
||||
flags= user=vmail:<group> argv=<modoboa_site>/manage.py autoreply $sender $mailbox
|
||||
flags= user=vmail:<group> argv=python <modoboa_site>/manage.py autoreply $sender $mailbox
|
||||
|
||||
Replace ``<driver>`` by the name of the database you
|
||||
use. ``<modoboa_site>`` is the path of your Modoboa instance.
|
||||
|
||||
@@ -28,19 +28,19 @@ Then, edit the ``inbox`` namespace and add the following lines::
|
||||
inbox = yes
|
||||
|
||||
mailbox Drafts {
|
||||
auto = create
|
||||
auto = subscribe
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox Junk {
|
||||
auto = create
|
||||
auto = subscribe
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox Sent {
|
||||
auto = create
|
||||
auto = subscribe
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox Trash {
|
||||
auto = create
|
||||
auto = subscribe
|
||||
special_use = \Trash
|
||||
}
|
||||
|
||||
|
||||
@@ -322,7 +322,7 @@ class UserSettings(parameters.UserParametersForm):
|
||||
choices=[("cs", "čeština"), ("de", "deutsch"),
|
||||
("en", "english"), ("es", "español"),
|
||||
("fr", "français"), ("it", "italiano"),
|
||||
("pt", "português"), ("ru", "русский"),
|
||||
("sv", "svenska")],
|
||||
("nl", "nederlands"), ("pt", "português"),
|
||||
("ru", "русский"), ("sv", "svenska")],
|
||||
help_text=ugettext_lazy("Prefered language to display pages")
|
||||
)
|
||||
|
||||
@@ -61,8 +61,11 @@ dologin = never_cache(dologin)
|
||||
|
||||
|
||||
def dologout(request):
|
||||
events.raiseEvent("UserLogout", request)
|
||||
logger = logging.getLogger("modoboa.auth")
|
||||
logger.info(_("User '%s' logged out" % request.user.username))
|
||||
logout(request)
|
||||
"""Logout the current user.
|
||||
"""
|
||||
if not request.user.is_anonymous():
|
||||
events.raiseEvent("UserLogout", request)
|
||||
logger = logging.getLogger("modoboa.auth")
|
||||
logger.info(_("User '%s' logged out" % request.user.username))
|
||||
logout(request)
|
||||
return HttpResponseRedirect(reverse(dologin))
|
||||
|
||||
@@ -153,7 +153,7 @@ def import_account_mailbox(user, account, row):
|
||||
:param list rom: list of fields (strings)
|
||||
"""
|
||||
account.email = row[0].strip()
|
||||
if account.email != "":
|
||||
if account.email:
|
||||
account.save()
|
||||
mailbox, domname = split_mailbox(account.email)
|
||||
try:
|
||||
@@ -169,8 +169,16 @@ def import_account_mailbox(user, account, row):
|
||||
except Mailbox.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
raise Conflict(_("Mailbox %s already exists" % account.email))
|
||||
quota = int(row[1].strip())
|
||||
raise Conflict(_("Mailbox %s already exists" % account.email))
|
||||
if len(row) == 1:
|
||||
quota = None
|
||||
else:
|
||||
try:
|
||||
quota = int(row[1].strip())
|
||||
except ValueError:
|
||||
raise BadRequest(
|
||||
_("Account import failed (%s): wrong quota value" % account.username)
|
||||
)
|
||||
use_domain_quota = True if not quota else False
|
||||
mb = Mailbox(address=mailbox, domain=domain,
|
||||
user=account, use_domain_quota=use_domain_quota)
|
||||
|
||||
@@ -60,18 +60,35 @@ class AccountFormGeneral(forms.ModelForm):
|
||||
if 'instance' in kwargs else get_account_roles(user)
|
||||
|
||||
if "instance" in kwargs:
|
||||
if args \
|
||||
and (args[0].get("password1", "") == ""
|
||||
and args[0].get("password2", "") == ""):
|
||||
self.fields["password1"].required = False
|
||||
self.fields["password2"].required = False
|
||||
account = kwargs["instance"]
|
||||
domain_disabled = account.mailbox_set.count() and \
|
||||
not account.mailbox_set.all()[0].domain.enabled
|
||||
if domain_disabled:
|
||||
self.fields["is_active"].widget.attrs['disabled'] = "disabled"
|
||||
if args:
|
||||
if args[0].get("password1", "") == "" \
|
||||
and args[0].get("password2", "") == "":
|
||||
self.fields["password1"].required = False
|
||||
self.fields["password2"].required = False
|
||||
if domain_disabled:
|
||||
del self.fields["is_active"]
|
||||
self.fields["role"].initial = account.group
|
||||
if not account.is_local \
|
||||
and parameters.get_admin("LDAP_AUTH_METHOD", app="core") == "directbind":
|
||||
del self.fields["password1"]
|
||||
del self.fields["password2"]
|
||||
|
||||
def domain_is_disabled(self):
|
||||
"""Little shortcut to get the domain's state.
|
||||
|
||||
We need this information inside a template and the form is the
|
||||
only object available...
|
||||
|
||||
"""
|
||||
if not self.instance.mailbox_set.count():
|
||||
return False
|
||||
return self.instance.mailbox_set.all()[0].domain.enabled == False
|
||||
|
||||
def clean_role(self):
|
||||
if self.user.group == "DomainAdmins":
|
||||
if self.instance == self.user:
|
||||
@@ -327,8 +344,15 @@ class AccountForm(TabForms):
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
self.user = request.user
|
||||
self.forms = [
|
||||
<<<<<<< HEAD
|
||||
dict(id="general", title=_("General"), cls=AccountFormGeneral,
|
||||
new_args=[self.user], mandatory=True),
|
||||
=======
|
||||
dict(id="general", title=_("General"),
|
||||
formtpl="admin/account_general_form.html",
|
||||
cls=AccountFormGeneral,
|
||||
new_args=[user], mandatory=True),
|
||||
>>>>>>> master
|
||||
dict(id="mail", title=_("Mail"), formtpl="admin/mailform.html",
|
||||
cls=AccountFormMail),
|
||||
dict(id="perms", title=_("Permissions"), formtpl="admin/permsform.html",
|
||||
|
||||
@@ -37,10 +37,15 @@ class AliasForm(forms.ModelForm, DynamicForm):
|
||||
self.fields.keyOrder = ['email', 'recipients', 'enabled']
|
||||
|
||||
if len(args) and isinstance(args[0], QueryDict):
|
||||
if "instance" in kwargs:
|
||||
if not kwargs["instance"].domain.enabled:
|
||||
del self.fields["enabled"]
|
||||
self._load_from_qdict(args[0], "recipients", forms.EmailField)
|
||||
elif "instance" in kwargs:
|
||||
dlist = kwargs["instance"]
|
||||
self.fields["email"].initial = dlist.full_address
|
||||
if not dlist.domain.enabled:
|
||||
self.fields["enabled"].widget.attrs["disabled"] = "disabled"
|
||||
cpt = 1
|
||||
for al in dlist.aliases.all():
|
||||
name = "recipients_%d" % cpt
|
||||
|
||||
@@ -170,6 +170,8 @@ class Alias(AdminObject):
|
||||
ext_rcpts = []
|
||||
for rcpt in row[3:]:
|
||||
rcpt = rcpt.strip()
|
||||
if not rcpt:
|
||||
continue
|
||||
localpart, domname = split_mailbox(rcpt)
|
||||
try:
|
||||
Domain.objects.get(name=domname)
|
||||
@@ -177,14 +179,16 @@ class Alias(AdminObject):
|
||||
ext_rcpts += [rcpt]
|
||||
continue
|
||||
try:
|
||||
target = Alias.objects.get(domain__name=domname, address=localpart)
|
||||
target = Alias.objects.get(
|
||||
domain__name=domname, address=localpart
|
||||
)
|
||||
if target.full_address == self.full_address:
|
||||
target = None
|
||||
except Alias.DoesNotExist:
|
||||
target = None
|
||||
if target is None:
|
||||
try:
|
||||
target = Mailbox.objects.get(address=localpart,
|
||||
target = Mailbox.objects.get(address=localpart,
|
||||
domain__name=domname)
|
||||
except Mailbox.DoesNotExist:
|
||||
raise BadRequest(_("Local recipient %s not found" % rcpt))
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{% load lib_tags %}
|
||||
{% render_field form.role %}
|
||||
{% render_field form.username %}
|
||||
{% render_field form.first_name %}
|
||||
{% render_field form.last_name %}
|
||||
{% render_field form.password1 %}
|
||||
{% render_field form.password2 %}
|
||||
{% if form.domain_is_disabled %}
|
||||
{% include "common/generic_field.html" with field=form.is_active inline_message=_("Domain is disabled") extra_classes="info" %}
|
||||
{% else %}
|
||||
{% render_field form.is_active %}
|
||||
{% endif %}
|
||||
@@ -2,5 +2,9 @@
|
||||
{% block formbody %}
|
||||
{% render_field form.email %}
|
||||
{% render_fields_group form "recipients" %}
|
||||
{% if form.instance.domain.enabled %}
|
||||
{% render_field form.enabled %}
|
||||
{% else %}
|
||||
{% include "common/generic_field.html" with field=form.enabled inline_message=_("Domain is disabled") extra_classes="info" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for ident in identities %}
|
||||
<tr id="{{ ident.id }}" class="{% if not ident.enabled %}muted{% endif %}" name="{{ forloop.counter }}">
|
||||
<tr id="{{ ident.id }}" class="{% disable_identity ident %}" name="{{ forloop.counter }}">
|
||||
<td name="identity">{% identity_modify_link ident %}</td>
|
||||
<td name="name_or_rcpt">{{ ident.name_or_rcpt }}</td>
|
||||
<td name="tags">{% render_tags ident.tags %}</td>
|
||||
|
||||
@@ -256,3 +256,18 @@ def get_extra_admin_content(user, target, currentpage):
|
||||
"ExtraAdminContent", user, target, currentpage
|
||||
)
|
||||
return "".join(res)
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def disable_identity(identity):
|
||||
"""Disable an identity.
|
||||
|
||||
Finding this information depends on the identity type.
|
||||
"""
|
||||
if identity.__class__.__name__ == "User":
|
||||
if identity.is_active and identity.mailbox_set.count() \
|
||||
and identity.mailbox_set.all()[0].domain.enabled:
|
||||
return ""
|
||||
elif identity.enabled and identity.domain.enabled:
|
||||
return ""
|
||||
return "muted"
|
||||
|
||||
@@ -104,6 +104,16 @@ dlist; dlist@test.com; True; user1@test.com; user@extdomain.com
|
||||
self.assertTrue(admin.is_owner(dlist))
|
||||
|
||||
def test_import_invalid_quota(self):
|
||||
f = ContentFile(b"""
|
||||
account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com; ; test.com
|
||||
""", name="identities.csv")
|
||||
resp = self.clt.post(
|
||||
reverse("modoboa.extensions.admin.views.import.import_identities"),
|
||||
{"sourcefile": f, "crypt_password": True}
|
||||
)
|
||||
self.assertIn('wrong quota value', resp.content)
|
||||
|
||||
def test_import_quota_too_big(self):
|
||||
self.clt.logout()
|
||||
self.clt.login(username="admin@test.com", password="toto")
|
||||
f = ContentFile(b"""
|
||||
@@ -115,6 +125,20 @@ account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com; 20
|
||||
)
|
||||
self.assertIn('Quota is greater than the allowed', resp.content)
|
||||
|
||||
def test_import_missing_quota(self):
|
||||
f = ContentFile(b"""
|
||||
account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com
|
||||
""", name="identities.csv")
|
||||
resp = self.clt.post(
|
||||
reverse("modoboa.extensions.admin.views.import.import_identities"),
|
||||
{"sourcefile": f, "crypt_password": True}
|
||||
)
|
||||
account = User.objects.get(username="user1@test.com")
|
||||
self.assertEqual(
|
||||
account.mailbox_set.all()[0].quota,
|
||||
account.mailbox_set.all()[0].domain.quota
|
||||
)
|
||||
|
||||
def test_import_duplicate(self):
|
||||
f = ContentFile(b"""
|
||||
account; admin@test.com; toto; Admin; ; True; DomainAdmins; admin@test.com; 0; test.com
|
||||
@@ -146,3 +170,17 @@ account; sa@test.com; toto; Super; Admin; True; SuperAdmins; superadmin@test.com
|
||||
)
|
||||
with self.assertRaises(User.DoesNotExist):
|
||||
User.objects.get(username="sa@test.com")
|
||||
|
||||
def test_import_alias_with_empty_values(self):
|
||||
f = ContentFile(b"""
|
||||
alias;user.alias@test.com;True;user@test.com;;;;;;;;;;;;;;;;
|
||||
""", name="identities.csv")
|
||||
self.clt.post(
|
||||
reverse("modoboa.extensions.admin.views.import.import_identities"),
|
||||
{"sourcefile": f, "crypt_password": True,
|
||||
"continue_if_exists": True}
|
||||
)
|
||||
alias = Alias.objects.get(
|
||||
domain__name="test.com", address="user.alias"
|
||||
)
|
||||
self.assertEqual(alias.type, "alias")
|
||||
|
||||
@@ -50,7 +50,7 @@ class Command(BaseCommand):
|
||||
flags += ['R']
|
||||
|
||||
self.__vprint("Deleting marked messages...")
|
||||
ids = Msgrcpt.objects.filter(rs__in=flags).values("mail_id")
|
||||
ids = Msgrcpt.objects.filter(rs__in=flags).values("mail_id").distinct()
|
||||
for msg in Msgs.objects.filter(mail_id__in=ids):
|
||||
if not msg.msgrcpt_set.exclude(rs__in=flags).count():
|
||||
msg.delete()
|
||||
|
||||
@@ -65,7 +65,7 @@ class Msgs(models.Model):
|
||||
class Msgrcpt(models.Model):
|
||||
partition_tag = models.IntegerField(null=True, blank=True)
|
||||
mail = models.ForeignKey(Msgs, primary_key=True)
|
||||
rid = models.ForeignKey(Maddr, db_column='rid', primary_key=True)
|
||||
rid = models.ForeignKey(Maddr, db_column='rid')
|
||||
rseqnum = models.IntegerField(default=0)
|
||||
is_local = models.CharField(max_length=3)
|
||||
content = models.CharField(max_length=3)
|
||||
|
||||
@@ -9,7 +9,7 @@ from modoboa.lib.webutils import static_url
|
||||
from modoboa.lib.email_listing import MBconnector, EmailListing
|
||||
from modoboa.lib.emailutils import Email
|
||||
from modoboa.lib.dbutils import db_type
|
||||
from modoboa.extensions.admin.models import Domain, Alias
|
||||
from modoboa.extensions.admin.models import Domain
|
||||
from .models import Quarantine, Msgrcpt
|
||||
|
||||
|
||||
@@ -46,9 +46,10 @@ class Qtable(tables.Table):
|
||||
"date", label=ugettext_lazy("Date"), sort_order="date",
|
||||
cssclass="openable"
|
||||
)
|
||||
|
||||
|
||||
cols_order = [
|
||||
'selection', 'type', 'rstatus', 'score', 'to', 'from_', 'subject', 'time'
|
||||
'selection', 'type', 'rstatus', 'score', 'to', 'from_', 'subject',
|
||||
'time'
|
||||
]
|
||||
|
||||
def parse_type(self, value):
|
||||
@@ -101,7 +102,7 @@ class SQLconnector(MBconnector):
|
||||
order = self.order_translation_table[order]
|
||||
self.messages = self.messages.order_by(sign + order)
|
||||
|
||||
self.count = len(self.messages)
|
||||
self.count = self.messages.count()
|
||||
return self.count
|
||||
|
||||
def fetch(self, start=None, stop=None, **kwargs):
|
||||
@@ -115,7 +116,9 @@ class SQLconnector(MBconnector):
|
||||
"type": qm["mail__msgrcpt__content"],
|
||||
"score": qm["mail__msgrcpt__bspam_level"]}
|
||||
rs = qm["mail__msgrcpt__rs"]
|
||||
if rs == '':
|
||||
if rs == 'D':
|
||||
continue
|
||||
elif rs == '':
|
||||
m["class"] = "unseen"
|
||||
elif rs == 'R':
|
||||
m["img_rstatus"] = static_url("pics/release.png")
|
||||
@@ -138,6 +141,18 @@ class SQLWrapper(object):
|
||||
See ``PgWrapper`` for the real mess...
|
||||
"""
|
||||
|
||||
def _exec(self, query, args):
|
||||
"""Execute a raw SQL query.
|
||||
|
||||
:param string query: query to execute
|
||||
:param list args: a list of arguments to replace in :kw:`query`
|
||||
"""
|
||||
from django.db import connections, transaction
|
||||
|
||||
cursor = connections['amavis'].cursor()
|
||||
cursor.execute(query, args)
|
||||
transaction.commit_unless_managed(using='amavis')
|
||||
|
||||
def get_mails(self, request, rcptfilter=None):
|
||||
"""Retrieve all messages visible by a user.
|
||||
|
||||
@@ -163,6 +178,19 @@ class SQLWrapper(object):
|
||||
def get_recipient_message(self, address, mailid):
|
||||
return Msgrcpt.objects.get(mail=mailid, rid__email=address)
|
||||
|
||||
def set_msgrcpt_status(self, address, mailid, status):
|
||||
"""Change the status (rs field) of a message recipient.
|
||||
|
||||
:param string status: status
|
||||
"""
|
||||
from modoboa.extensions.amavis.models import Maddr
|
||||
|
||||
addr = Maddr.objects.get(email=address)
|
||||
self._exec(
|
||||
"UPDATE msgrcpt SET rs=%s WHERE mail_id=%s AND rid=%s",
|
||||
[status, mailid, addr.id]
|
||||
)
|
||||
|
||||
def get_recipient_messages(self, address, mailids):
|
||||
return Msgrcpt.objects.filter(mail__in=mailids, rid__email=address)
|
||||
|
||||
@@ -278,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 != "":
|
||||
@@ -300,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", {
|
||||
|
||||
@@ -57,7 +57,6 @@ def _listing(request):
|
||||
else:
|
||||
raise BadRequest("unsupported search criteria %s" % c)
|
||||
flt = nfilter if flt is None else flt | nfilter
|
||||
|
||||
msgtype = navparams.get('msgtype', None)
|
||||
if msgtype is not None:
|
||||
nfilter = Q(mail__msgrcpt__content=msgtype)
|
||||
@@ -93,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
|
||||
@@ -107,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
|
||||
@@ -140,9 +129,7 @@ def viewmail(request, mail_id):
|
||||
if request.user.mailbox_set.count():
|
||||
mb = Mailbox.objects.get(user=request.user)
|
||||
if rcpt in mb.alias_addresses:
|
||||
msgrcpt = get_wrapper().get_recipient_message(rcpt, mail_id)
|
||||
msgrcpt.rs = 'V'
|
||||
msgrcpt.save()
|
||||
get_wrapper().set_msgrcpt_status(rcpt, mail_id, 'V')
|
||||
|
||||
content = Template("""
|
||||
<iframe src="{{ url }}" id="mailcontent"></iframe>
|
||||
@@ -178,9 +165,7 @@ def delete_selfservice(request, mail_id):
|
||||
if rcpt is None:
|
||||
raise BadRequest(_("Invalid request"))
|
||||
try:
|
||||
msgrcpt = get_wrapper().get_recipient_message(rcpt, mail_id)
|
||||
msgrcpt.rs = 'D'
|
||||
msgrcpt.save()
|
||||
get_wrapper().set_msgrcpt_status(rcpt, mail_id, 'D')
|
||||
except Msgrcpt.DoesNotExist:
|
||||
raise BadRequest(_("Invalid request"))
|
||||
return render_to_json_response(_("Message deleted"))
|
||||
@@ -201,9 +186,7 @@ def delete(request, mail_id):
|
||||
if mb is not None and r != mb.full_address \
|
||||
and not r in mb.alias_addresses:
|
||||
continue
|
||||
msgrcpt = wrapper.get_recipient_message(r, i)
|
||||
msgrcpt.rs = 'D'
|
||||
msgrcpt.save()
|
||||
wrapper.set_msgrcpt_status(r, i, 'D')
|
||||
message = ungettext("%(count)d message deleted successfully",
|
||||
"%(count)d messages deleted successfully",
|
||||
len(mail_id)) % {"count": len(mail_id)}
|
||||
@@ -218,24 +201,24 @@ def release_selfservice(request, mail_id):
|
||||
secret_id = request.GET.get("secret_id", None)
|
||||
if rcpt is None or secret_id is None:
|
||||
raise BadRequest(_("Invalid request"))
|
||||
wrapper = get_wrapper()
|
||||
try:
|
||||
msgrcpt = get_wrapper().get_recipient_message(rcpt, mail_id)
|
||||
msgrcpt = wrapper.get_recipient_message(rcpt, mail_id)
|
||||
except Msgrcpt.DoesNotExist:
|
||||
raise BadRequest(_("Invalid request"))
|
||||
if secret_id != msgrcpt.mail.secret_id:
|
||||
raise BadRequest(_("Invalid request"))
|
||||
if parameters.get_admin("USER_CAN_RELEASE") == "no":
|
||||
msgrcpt.rs = 'p'
|
||||
wrapper.set_msgrcpt_status(rcpt, mail_id, 'p')
|
||||
msg = _("Request sent")
|
||||
else:
|
||||
amr = AMrelease()
|
||||
result = amr.sendreq(mail_id, secret_id, rcpt)
|
||||
if result:
|
||||
rcpt.rs = 'R'
|
||||
wrapper.set_msgrcpt_status(rcpt, mail_id, 'R')
|
||||
msg = _("Message released")
|
||||
else:
|
||||
raise BadRequest(result)
|
||||
msgrcpt.save()
|
||||
return render_to_json_response(msg)
|
||||
|
||||
|
||||
@@ -257,14 +240,10 @@ def release(request, mail_id):
|
||||
continue
|
||||
msgrcpts += [wrapper.get_recipient_message(r, i)]
|
||||
if mb is not None and parameters.get_admin("USER_CAN_RELEASE") == "no":
|
||||
# FIXME : can't use this syntax because extra SQL (using
|
||||
# .extra() for postgres) is not propagated (the 'tables'
|
||||
# parameter is lost somewhere...)
|
||||
#
|
||||
# msgrcpts.update(rs='p')
|
||||
for msgrcpt in msgrcpts:
|
||||
msgrcpt.rs = 'p'
|
||||
msgrcpt.save()
|
||||
wrapper.set_msgrcpt_status(
|
||||
msgrcpt.rid.email, msgrcpt.mail.mail_id, 'p'
|
||||
)
|
||||
message = ungettext("%(count)d request sent",
|
||||
"%(count)d requests sent",
|
||||
len(mail_id)) % {"count": len(mail_id)}
|
||||
@@ -276,10 +255,11 @@ def release(request, mail_id):
|
||||
amr = AMrelease()
|
||||
error = None
|
||||
for rcpt in msgrcpts:
|
||||
result = amr.sendreq(rcpt.mail.mail_id, rcpt.mail.secret_id, rcpt.rid.email)
|
||||
result = amr.sendreq(
|
||||
rcpt.mail.mail_id, rcpt.mail.secret_id, rcpt.rid.email
|
||||
)
|
||||
if result:
|
||||
rcpt.rs = 'R'
|
||||
rcpt.save()
|
||||
wrapper.set_msgrcpt_status(rcpt.rid.email, rcpt.mail.mail_id, 'R')
|
||||
else:
|
||||
error = result
|
||||
break
|
||||
|
||||
@@ -56,7 +56,7 @@ class Command(BaseCommand):
|
||||
try:
|
||||
mbox = Mailbox.objects.get(address=address, domain__name=domain)
|
||||
except Mailbox.DoesNotExist:
|
||||
print "Unknown recipient %s" % (mbox)
|
||||
print "Unknown recipient %s" % (fulladdress)
|
||||
continue
|
||||
try:
|
||||
armessage = ARmessage.objects.get(mbox=mbox.id, enabled=True)
|
||||
|
||||
@@ -97,6 +97,13 @@ class FilterForm(forms.Form):
|
||||
for a in actions:
|
||||
getattr(self, "_build_%s_field" % a[0])(request, a[1])
|
||||
|
||||
def clean_name(self):
|
||||
"""Check that name does not contain strange chars.
|
||||
"""
|
||||
if '#' in self.cleaned_data["name"]:
|
||||
raise forms.ValidationError(_("Wrong filter name"))
|
||||
return self.cleaned_data["name"]
|
||||
|
||||
def _build_header_field(self, name, op, value):
|
||||
targets = []
|
||||
ops = []
|
||||
@@ -173,7 +180,7 @@ class FilterForm(forms.Form):
|
||||
return ret
|
||||
|
||||
def userfolders(self, request):
|
||||
from modoboa.extensions.webmail.imaputils import get_imapconnector
|
||||
from modoboa.extensions.webmail.lib import get_imapconnector
|
||||
|
||||
mbc = get_imapconnector(request)
|
||||
ret = mbc.getmboxes(request.user, unseen_messages=False)
|
||||
|
||||
@@ -19,13 +19,13 @@ from .forms import (
|
||||
FilterForm, build_filter_form_from_qdict, build_filter_form_from_filter,
|
||||
FiltersSetForm
|
||||
)
|
||||
from templatetags.sfilters_tags import fset_menu
|
||||
from .templatetags.sfilters_tags import fset_menu
|
||||
|
||||
|
||||
@login_required
|
||||
@needs_mailbox()
|
||||
def index(request, tplname="sievefilters/index.html"):
|
||||
from modoboa.extensions.webmail.imaputils import get_imapconnector
|
||||
from modoboa.extensions.webmail.lib import get_imapconnector
|
||||
|
||||
try:
|
||||
sc = SieveClient(user=request.user.username,
|
||||
|
||||
@@ -21,9 +21,3 @@ class ParametersForm(AdminParametersForm):
|
||||
initial="/tmp/modoboa",
|
||||
help_text=ugettext_lazy("Path to directory where RRD files are stored")
|
||||
)
|
||||
|
||||
img_rootdir = forms.CharField(
|
||||
label=ugettext_lazy("Directory to store PNG files"),
|
||||
initial="/tmp/modoboa",
|
||||
help_text=ugettext_lazy("Path to directory where PNG files are stored")
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ import sys
|
||||
import os
|
||||
import time
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
from django.conf import settings
|
||||
from modoboa.lib import parameters
|
||||
|
||||
periods = [{"name": "day", "label": ugettext_lazy("Day")},
|
||||
@@ -35,7 +36,7 @@ def str2Time(y, M, d, h="00", m="00", s="00"):
|
||||
class Grapher(object):
|
||||
def __init__(self):
|
||||
self.rrd_rootdir = parameters.get_admin("RRD_ROOTDIR")
|
||||
self.img_rootdir = parameters.get_admin("IMG_ROOTDIR")
|
||||
self.img_rootdir = os.path.join(settings.MEDIA_ROOT, "stats")
|
||||
|
||||
def process(self, target, suffix, start, end, graph_tpl):
|
||||
import rrdtool
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -1,767 +0,0 @@
|
||||
# coding: utf-8
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
import email
|
||||
import lxml
|
||||
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="<input type='checkbox' name='toggleselect' id='toggleselect' />"
|
||||
)
|
||||
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 -----<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):
|
||||
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", "<br/>", content)
|
||||
self._sig = """<br/>
|
||||
---<br/>
|
||||
%s""" % content
|
||||
|
||||
def __repr__(self):
|
||||
return self._sig
|
||||
|
||||
|
||||
def decode_payload(encoding, payload):
|
||||
"""Decode the payload according to the given encoding
|
||||
|
||||
Supported encodings: base64, quoted-printable.
|
||||
|
||||
:param encoding: the encoding's name
|
||||
:param payload: the value to decode
|
||||
:return: a string
|
||||
"""
|
||||
encoding = encoding.lower()
|
||||
if encoding == "base64":
|
||||
import base64
|
||||
return base64.b64decode(payload)
|
||||
elif encoding == "quoted-printable":
|
||||
import quopri
|
||||
return quopri.decodestring(payload)
|
||||
return payload
|
||||
|
||||
|
||||
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)
|
||||
fname = os.path.basename(o.path)
|
||||
path = os.path.join(settings.MEDIA_ROOT, "webmail", 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, "webmail")
|
||||
try:
|
||||
fp = NamedTemporaryFile(dir=dstdir, delete=False)
|
||||
except Exception as e:
|
||||
raise InternalError(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, "webmail", att["tmpname"])
|
||||
try:
|
||||
os.remove(fullpath)
|
||||
except OSError:
|
||||
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 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, "webmail", attdef["tmpname"]), "rb")
|
||||
res.set_payload(fp.read())
|
||||
fp.close()
|
||||
Encoders.encode_base64(res)
|
||||
res['Content-Disposition'] = build_header(attdef['fname'].decode('utf-8'))
|
||||
return res
|
||||
|
||||
|
||||
def send_mail(request, 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)
|
||||
"""
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from .forms import ComposeMailForm
|
||||
from modoboa.lib.webutils import _render_to_string
|
||||
from modoboa.lib.cryptutils import get_password
|
||||
|
||||
form = ComposeMailForm(request.POST)
|
||||
editormode = parameters.get_user(request.user, "EDITOR")
|
||||
if form.is_valid():
|
||||
from email.mime.text import MIMEText
|
||||
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))
|
||||
|
||||
set_email_headers(
|
||||
msg, form.cleaned_data["subject"], request.user.encoded_address,
|
||||
form.cleaned_data['to']
|
||||
)
|
||||
if "origmsgid" in form.cleaned_data:
|
||||
msg["References"] = msg["In-Reply-To"] = form.cleaned_data["origmsgid"]
|
||||
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, e:
|
||||
raise WebmailInternalError(str(e))
|
||||
try:
|
||||
s.sendmail(request.user.email, rcpts, msg.as_string())
|
||||
s.quit()
|
||||
except smtplib.SMTPException, e:
|
||||
raise WebmailInternalError(str(e))
|
||||
|
||||
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, {}
|
||||
|
||||
listing = _render_to_string(request, "webmail/compose.html",
|
||||
{"form": form, "noerrors": True,
|
||||
"body": request.POST["id_body"].strip(),
|
||||
"posturl": posturl})
|
||||
return False, dict(status="ko", 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
|
||||
|
||||
|
||||
class WebmailNavigationParameters(NavigationParameters):
|
||||
"""
|
||||
Specific NavigationParameters subclass for the webmail.
|
||||
"""
|
||||
def __init__(self, request, defmailbox=None):
|
||||
super(WebmailNavigationParameters, self).__init__(
|
||||
request, 'webmail_navparams'
|
||||
)
|
||||
if defmailbox is not None:
|
||||
self.parameters += [('mbox', defmailbox, False)]
|
||||
@@ -0,0 +1,35 @@
|
||||
from .attachments import (
|
||||
create_mail_attachment, save_attachment, clean_attachments,
|
||||
set_compose_session, AttachmentUploadHandler
|
||||
)
|
||||
from .imapemail import ImapEmail, ReplyModifier, ForwardModifier
|
||||
from .imaputils import (
|
||||
BodyStructure, IMAPconnector, get_imapconnector, separate_mailbox
|
||||
)
|
||||
from .listing import ImapListing
|
||||
from .signature import EmailSignature
|
||||
from .utils import decode_payload, WebmailNavigationParameters
|
||||
from .sendmail import send_mail
|
||||
from .fetch_parser import parse_fetch_response
|
||||
|
||||
|
||||
__all__ = [
|
||||
'AttachmentUploadHandler',
|
||||
'BodyStructure',
|
||||
'EmailSignature',
|
||||
'ForwardModifier',
|
||||
'IMAPconnector',
|
||||
'ImapEmail',
|
||||
'ImapListing',
|
||||
'ReplyModifier',
|
||||
'WebmailNavigationParameters',
|
||||
'clean_attachments',
|
||||
'create_mail_attachment',
|
||||
'decode_payload',
|
||||
'get_imapconnector',
|
||||
'parse_fetch_response',
|
||||
'save_attachment',
|
||||
'send_mail',
|
||||
'separate_mailbox',
|
||||
'set_compose_session',
|
||||
]
|
||||
@@ -0,0 +1,114 @@
|
||||
import os
|
||||
from rfc6266 import build_header
|
||||
from django.conf import settings
|
||||
from django.core.files.uploadhandler import FileUploadHandler, SkipFile
|
||||
from modoboa.lib import parameters
|
||||
from modoboa.lib.exceptions import InternalError
|
||||
from modoboa.lib.webutils import size2integer
|
||||
|
||||
|
||||
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, "webmail")
|
||||
try:
|
||||
fp = NamedTemporaryFile(dir=dstdir, delete=False)
|
||||
except Exception as e:
|
||||
raise InternalError(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, "webmail", att["tmpname"])
|
||||
try:
|
||||
os.remove(fullpath)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def create_mail_attachment(attdef, payload=None):
|
||||
"""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
|
||||
|
||||
if "content-type" in attdef:
|
||||
maintype, subtype = attdef["content-type"].split("/")
|
||||
elif "Content-Type" in attdef:
|
||||
maintype, subtype = attdef["Content-Type"].split("/")
|
||||
else:
|
||||
return None
|
||||
res = MIMEBase(maintype, subtype)
|
||||
if payload is None:
|
||||
with open(os.path.join(
|
||||
settings.MEDIA_ROOT, "webmail", attdef["tmpname"]), "rb") as fp:
|
||||
res.set_payload(fp.read())
|
||||
else:
|
||||
res.set_payload(payload)
|
||||
Encoders.encode_base64(res)
|
||||
if type(attdef['fname']) is str:
|
||||
attdef['fname'] = attdef['fname'].decode('utf-8')
|
||||
res['Content-Disposition'] = build_header(attdef['fname'])
|
||||
return res
|
||||
|
||||
|
||||
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
|
||||
@@ -0,0 +1,307 @@
|
||||
"""
|
||||
Set of classes to manipulate/display emails inside the webmail.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import email
|
||||
|
||||
import chardet
|
||||
from rfc6266 import parse_headers
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from modoboa.lib import parameters, u2u_decode
|
||||
from modoboa.lib.emailutils import Email, EmailAddress
|
||||
from .imaputils import (
|
||||
get_imapconnector, BodyStructure
|
||||
)
|
||||
from .utils import decode_payload
|
||||
|
||||
|
||||
class ImapEmail(Email):
|
||||
"""
|
||||
A class to represent an email fetched from an IMAP server.
|
||||
"""
|
||||
headernames = [
|
||||
('From', True),
|
||||
('To', True),
|
||||
('Cc', True),
|
||||
('Date', True),
|
||||
('Subject', True),
|
||||
]
|
||||
|
||||
def __init__(self, request, addrfull, *args, **kwargs):
|
||||
super(ImapEmail, self).__init__(*args, **kwargs)
|
||||
self.request = request
|
||||
self.addrfull = addrfull
|
||||
self.imapc = get_imapconnector(request)
|
||||
self.mbox, self.mailid = self.mailid.split(":")
|
||||
|
||||
headers = self.msg['BODY[HEADER.FIELDS (%s)]' % self.headers_as_text]
|
||||
msg = email.message_from_string(headers)
|
||||
for hdr in self.headernames:
|
||||
label = hdr[0]
|
||||
hdrvalue = self.get_header(msg, label)
|
||||
if not hdrvalue:
|
||||
continue
|
||||
if hdr[1]:
|
||||
self.headers += [{"name": label, "value": hdrvalue}]
|
||||
label = re.sub("-", "_", label)
|
||||
setattr(self, label, hdrvalue)
|
||||
|
||||
def get_header(self, msg, hdrname):
|
||||
"""Look for a particular header.
|
||||
|
||||
We also try to decode the default value.
|
||||
"""
|
||||
from . import imapheader
|
||||
|
||||
hdrvalue = super(ImapEmail, self).get_header(msg, hdrname)
|
||||
if not hdrvalue:
|
||||
return ""
|
||||
try:
|
||||
key = re.sub("-", "_", hdrname).lower()
|
||||
hdrvalue = getattr(imapheader, "parse_%s" % key)(
|
||||
hdrvalue, full=self.addrfull
|
||||
)
|
||||
except AttributeError:
|
||||
pass
|
||||
return hdrvalue
|
||||
|
||||
@property
|
||||
def msg(self):
|
||||
"""
|
||||
"""
|
||||
if self._msg is None:
|
||||
self._msg = self.imapc.fetchmail(
|
||||
self.mbox, self.mailid, readonly=False,
|
||||
headers=self.headers_as_list
|
||||
)
|
||||
self.bs = BodyStructure(self._msg['BODYSTRUCTURE'])
|
||||
self._find_attachments()
|
||||
if not self.dformat in ["plain", "html"]:
|
||||
self.dformat = parameters.get_user(
|
||||
self.request.user, self.dformat
|
||||
)
|
||||
fallback_fmt = "html" if self.dformat == "plain" else "plain"
|
||||
self.mformat = self.dformat \
|
||||
if self.dformat in self.bs.contents else fallback_fmt
|
||||
return self._msg
|
||||
|
||||
@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)
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
"""Load email's body.
|
||||
|
||||
This operation has to be made "on demand" because it requires
|
||||
a communication with the IMAP server.
|
||||
|
||||
"""
|
||||
if self._body is None and self.bs.contents:
|
||||
bodyc = u''
|
||||
for part in self.bs.contents[self.mformat]:
|
||||
pnum = part['pnum']
|
||||
data = self.imapc._cmd(
|
||||
"FETCH", self.mailid, "(BODY.PEEK[%s])" % pnum
|
||||
)
|
||||
content = decode_payload(
|
||||
part['encoding'], data[int(self.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" % self.mformat)(
|
||||
bodyc, links=self.links
|
||||
)
|
||||
return self._body
|
||||
|
||||
@body.setter
|
||||
def body(self, value):
|
||||
self._body = value
|
||||
|
||||
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(r"\.\.", 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"]
|
||||
)
|
||||
with open(path, "wb") as fpo:
|
||||
fpo.write(decode_payload(params["encoding"], content))
|
||||
|
||||
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
|
||||
|
||||
def fetch_attachment(self, pnum):
|
||||
"""Fetch an attachment from the IMAP server.
|
||||
"""
|
||||
return self.imapc.fetchpart(self.mailid, self.mbox, pnum)
|
||||
|
||||
|
||||
class Modifier(ImapEmail):
|
||||
def __init__(self, form, *args, **kwargs):
|
||||
kwargs["dformat"] = "EDITOR"
|
||||
super(Modifier, self).__init__(*args, **kwargs)
|
||||
self.form = form
|
||||
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, *args, **kwargs):
|
||||
super(ReplyModifier, self).__init__(*args, **kwargs)
|
||||
|
||||
self.textheader = "%s %s" % (self.From, _("wrote:"))
|
||||
if hasattr(self, "Message_ID"):
|
||||
self.form.fields["origmsgid"].initial = self.Message_ID
|
||||
if not hasattr(self, "Reply_To"):
|
||||
self.form.fields["to"].initial = self.From
|
||||
else:
|
||||
self.form.fields["to"].initial = self.Reply_To
|
||||
if self.request.GET.get("all", "0") == "1": # reply-all
|
||||
self.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 == self.request.user.username:
|
||||
continue
|
||||
if self.form.fields["cc"].initial != "":
|
||||
self.form.fields["cc"].initial += ", "
|
||||
self.form.fields["cc"].initial += tmp.fulladdress
|
||||
m = re.match(r"re\s*:\s*.+", self.Subject.lower())
|
||||
if m:
|
||||
self.form.fields["subject"].initial = self.Subject
|
||||
else:
|
||||
self.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, *args, **kwargs):
|
||||
super(ForwardModifier, self).__init__(*args, **kwargs)
|
||||
self._header()
|
||||
self.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)
|
||||
self.textheader += \
|
||||
self.__getfunc("_header_line")(_("Date"), self.Date)
|
||||
for hdr in ["From", "To", "Reply-To"]:
|
||||
try:
|
||||
key = re.sub("-", "_", hdr)
|
||||
value = getattr(self, key)
|
||||
self.textheader += \
|
||||
self.__getfunc("_header_line")(_(hdr), value)
|
||||
except AttributeError:
|
||||
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\n" % (key, value)
|
||||
|
||||
def _header_line_html(self, key, value):
|
||||
return "<p>%s: %s</p>" % (key, value)
|
||||
|
||||
def _header_end_plain(self):
|
||||
return "\n"
|
||||
|
||||
def _header_end_html(self):
|
||||
return ""
|
||||
@@ -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)
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
@@ -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="<input type='checkbox' name='toggleselect' id='toggleselect' />"
|
||||
)
|
||||
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)
|
||||
@@ -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, {}
|
||||
@@ -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", "<br/>", content)
|
||||
self._sig = """<br/>
|
||||
---<br/>
|
||||
%s""" % content
|
||||
|
||||
def __repr__(self):
|
||||
return self._sig
|
||||
@@ -0,0 +1,48 @@
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy
|
||||
|
||||
from modoboa.lib import tables
|
||||
from . import imapheader
|
||||
|
||||
|
||||
class WMtable(tables.Table):
|
||||
"""
|
||||
The webmail listing.
|
||||
"""
|
||||
tableid = "emails"
|
||||
styles = "table-condensed"
|
||||
idkey = "imapid"
|
||||
drag = tables.ImgColumn(
|
||||
"drag", cssclass="draggable left", width="1%",
|
||||
defvalue="%spics/grippy.png" % settings.STATIC_URL,
|
||||
header="<input type='checkbox' name='toggleselect' id='toggleselect' />"
|
||||
)
|
||||
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
|
||||
@@ -0,0 +1,35 @@
|
||||
"""
|
||||
Misc. utilities.
|
||||
"""
|
||||
from modoboa.lib.webutils import NavigationParameters
|
||||
|
||||
|
||||
def decode_payload(encoding, payload):
|
||||
"""Decode the payload according to the given encoding
|
||||
|
||||
Supported encodings: base64, quoted-printable.
|
||||
|
||||
:param encoding: the encoding's name
|
||||
:param payload: the value to decode
|
||||
:return: a string
|
||||
"""
|
||||
encoding = encoding.lower()
|
||||
if encoding == "base64":
|
||||
import base64
|
||||
return base64.b64decode(payload)
|
||||
elif encoding == "quoted-printable":
|
||||
import quopri
|
||||
return quopri.decodestring(payload)
|
||||
return payload
|
||||
|
||||
|
||||
class WebmailNavigationParameters(NavigationParameters):
|
||||
"""
|
||||
Specific NavigationParameters subclass for the webmail.
|
||||
"""
|
||||
def __init__(self, request, defmailbox=None):
|
||||
super(WebmailNavigationParameters, self).__init__(
|
||||
request, 'webmail_navparams'
|
||||
)
|
||||
if defmailbox is not None:
|
||||
self.parameters += [('mbox', defmailbox, False)]
|
||||
@@ -753,7 +753,7 @@ Webmail.prototype = {
|
||||
}, this)).fail($.proxy(function(jqxhr) {
|
||||
var data = $.parseJSON(jqxhr.responseText);
|
||||
if (data != undefined) {
|
||||
$("body").notify("error", data);
|
||||
this.compose_callback(data);
|
||||
}
|
||||
$("a[name=sendmail]").attr("disabled", null);
|
||||
}, this));
|
||||
|
||||
@@ -23,11 +23,11 @@
|
||||
|
||||
<div id="body_container">
|
||||
{% if body %}
|
||||
<textarea id="id_body" name="id_body" class="editor">{% if bodyheader %}{{ bodyheader }}{% endif %}
|
||||
<textarea id="id_body" name="body" class="editor">{% if bodyheader %}{{ bodyheader }}{% endif %}
|
||||
{{ body }}
|
||||
</textarea>
|
||||
{% else %}
|
||||
<textarea id="id_body" name="id_body" class="editor"></textarea>
|
||||
<textarea id="id_body" name="body" class="editor"></textarea>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.utils.translation import ugettext as _
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.conf import settings
|
||||
from modoboa.extensions import webmail
|
||||
from modoboa.extensions.webmail.imaputils import separate_mailbox
|
||||
from modoboa.extensions.webmail.lib import separate_mailbox
|
||||
from modoboa.lib import parameters
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@@ -17,13 +17,15 @@ from modoboa.lib.webutils import (
|
||||
)
|
||||
from modoboa.extensions.admin.lib import needs_mailbox
|
||||
from .exceptions import UnknownAction
|
||||
from .forms import FolderForm, AttachmentForm, ComposeMailForm
|
||||
from .imaputils import get_imapconnector, IMAPconnector, separate_mailbox
|
||||
from .forms import (
|
||||
FolderForm, AttachmentForm, ComposeMailForm, ForwardMailForm
|
||||
)
|
||||
from .lib import (
|
||||
decode_payload, AttachmentUploadHandler,
|
||||
save_attachment, ImapListing, EmailSignature,
|
||||
clean_attachments, set_compose_session, send_mail,
|
||||
ImapEmail, WebmailNavigationParameters, ReplyModifier, ForwardModifier
|
||||
ImapEmail, WebmailNavigationParameters, ReplyModifier, ForwardModifier,
|
||||
get_imapconnector, IMAPconnector, separate_mailbox
|
||||
)
|
||||
from templatetags import webmail_tags
|
||||
|
||||
@@ -382,43 +384,62 @@ def render_compose(request, form, posturl, email=None, insert_signature=False):
|
||||
def compose(request):
|
||||
url = "?action=compose"
|
||||
if request.method == "POST":
|
||||
status, resp = send_mail(request, posturl=url)
|
||||
form = ComposeMailForm(request.POST)
|
||||
status, resp = send_mail(request, form, posturl=url)
|
||||
return resp
|
||||
|
||||
form = ComposeMailForm()
|
||||
return render_compose(request, form, url, insert_signature=True)
|
||||
|
||||
|
||||
def compose_and_send(request, action, callback=None):
|
||||
def get_mail_info(request):
|
||||
"""Retrieve a mailbox and an email ID from a request.
|
||||
"""
|
||||
mbox = request.GET.get("mbox", None)
|
||||
mailid = request.GET.get("mailid", None)
|
||||
if mbox is None or mailid is None:
|
||||
raise BadRequest(_("Invalid request"))
|
||||
url = "?action=%s&mbox=%s&mailid=%s" % (action, mbox, mailid)
|
||||
if request.method == "POST":
|
||||
status, resp = send_mail(request, url)
|
||||
if status and callback:
|
||||
callback(mbox, mailid)
|
||||
return resp
|
||||
return mbox, mailid
|
||||
|
||||
|
||||
def new_compose_form(request, action, mbox, mailid):
|
||||
"""Return a new composition form.
|
||||
|
||||
Valid for reply and forward actions only.
|
||||
"""
|
||||
form = ComposeMailForm()
|
||||
modclass = globals()["%sModifier" % action.capitalize()]
|
||||
email = modclass(mbox, mailid, request, form, addrfull=True, links="1")
|
||||
email = modclass(form, request, True, "%s:%s" % (mbox, mailid), links="1")
|
||||
url = "?action=%s&mbox=%s&mailid=%s" % (action, mbox, mailid)
|
||||
return render_compose(request, form, url, email)
|
||||
|
||||
|
||||
def reply(request):
|
||||
def msg_replied(mbox, mailid):
|
||||
get_imapconnector(request).msg_answered(mbox, mailid)
|
||||
|
||||
return compose_and_send(request, "reply", msg_replied)
|
||||
"""Reply to email.
|
||||
"""
|
||||
mbox, mailid = get_mail_info(request)
|
||||
if request.method == "POST":
|
||||
url = "?action=reply&mbox=%s&mailid=%s" % (mbox, mailid)
|
||||
form = ComposeMailForm(request.POST)
|
||||
status, resp = send_mail(request, form, url)
|
||||
if status:
|
||||
get_imapconnector(request).msg_answered(mbox, mailid)
|
||||
return resp
|
||||
return new_compose_form(request, "reply", mbox, mailid)
|
||||
|
||||
|
||||
def forward(request):
|
||||
def msg_forwarded(mbox, mailid):
|
||||
get_imapconnector(request).msg_forwarded(mbox, mailid)
|
||||
|
||||
return compose_and_send(request, "forward", msg_forwarded)
|
||||
"""Forward email.
|
||||
"""
|
||||
mbox, mailid = get_mail_info(request)
|
||||
if request.method == "POST":
|
||||
url = "?action=forward&mbox=%s&mailid=%s" % (mbox, mailid)
|
||||
form = ForwardMailForm(request.POST)
|
||||
status, resp = send_mail(request, form, url)
|
||||
if status:
|
||||
get_imapconnector(request).msg_forwarded(mbox, mailid)
|
||||
return resp
|
||||
return new_compose_form(request, "forward", mbox, mailid)
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -428,7 +449,10 @@ def getmailcontent(request):
|
||||
mailid = request.GET.get("mailid", None)
|
||||
if mbox is None or mailid is None:
|
||||
raise BadRequest(_("Invalid request"))
|
||||
email = ImapEmail(mbox, mailid, request, links=int(request.GET["links"]))
|
||||
email = ImapEmail(
|
||||
request, False, "%s:%s" % (mbox, mailid), dformat="DISPLAYMODE",
|
||||
links=int(request.GET["links"])
|
||||
)
|
||||
return render(request, "common/viewmail.html", {
|
||||
"headers": email.render_headers(folder=mbox, mail_id=mailid),
|
||||
"folder": mbox, "imapid": mailid,
|
||||
|
||||
+41
-15
@@ -7,7 +7,7 @@ from email.mime.text import MIMEText
|
||||
from email.utils import make_msgid, formatdate, parseaddr
|
||||
from django.template.loader import render_to_string
|
||||
from django.conf import settings
|
||||
import u2u_decode
|
||||
from modoboa.lib import u2u_decode
|
||||
|
||||
|
||||
class EmailAddress(object):
|
||||
@@ -22,26 +22,49 @@ class EmailAddress(object):
|
||||
|
||||
|
||||
class Email(object):
|
||||
def __init__(self, msg, mformat="plain", dformat="plain", links=0):
|
||||
def __init__(self, mailid, mformat="plain", dformat="plain", links=0):
|
||||
self.attached_map = {}
|
||||
self.contents = {"html": "", "plain": ""}
|
||||
self.headers = []
|
||||
self.attachments = {}
|
||||
self.mailid = mailid
|
||||
self.mformat = mformat
|
||||
self.dformat = dformat
|
||||
self.links = links
|
||||
|
||||
self.__parse(msg)
|
||||
self._msg = None
|
||||
self._body = None
|
||||
|
||||
if not mformat in self.contents 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)
|
||||
@property
|
||||
def msg(self):
|
||||
"""Return an email.message object.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def __parse_default(self, msg, level):
|
||||
@property
|
||||
def body(self):
|
||||
"""Return email's body.
|
||||
"""
|
||||
if self._body is None:
|
||||
self._body = getattr(self, "viewmail_%s" % self.mformat) \
|
||||
(self.contents[self.mformat], links=self.links)
|
||||
return self._body
|
||||
|
||||
def get_header(self, msg, hdrname):
|
||||
"""Look for a particular header.
|
||||
|
||||
:param string hdrname: header name
|
||||
:return: header avalue
|
||||
"""
|
||||
for name in [hdrname, hdrname.upper()]:
|
||||
if name in msg:
|
||||
return msg[name]
|
||||
return ""
|
||||
|
||||
def _parse_default(self, msg, level):
|
||||
"""Default parser
|
||||
|
||||
All parts handled by this parser will be consireded as
|
||||
@@ -63,7 +86,7 @@ class Email(object):
|
||||
inside a navigator.
|
||||
"""
|
||||
if msg.get_content_subtype() not in ["plain", "html"]:
|
||||
self.__parse_default(msg, level)
|
||||
self._parse_default(msg, level)
|
||||
target = "plain"
|
||||
else:
|
||||
target = msg.get_content_subtype()
|
||||
@@ -96,9 +119,9 @@ class Email(object):
|
||||
self.attached_map[cid] = re.match("^http:", fname) and fname \
|
||||
or self.__save_image(fname, msg)
|
||||
return
|
||||
self.__parse_default(msg, level)
|
||||
self._parse_default(msg, level)
|
||||
|
||||
def __parse(self, msg, level=None):
|
||||
def _parse(self, msg, level=None):
|
||||
"""Recursive email parser
|
||||
|
||||
A message structure can be complex. To correctly handle
|
||||
@@ -116,7 +139,7 @@ class Email(object):
|
||||
for part in msg.get_payload():
|
||||
nlevel = level is None and ("%d" % cpt) \
|
||||
or "%s.%d" % (level, cpt)
|
||||
self.__parse(part, nlevel)
|
||||
self._parse(part, nlevel)
|
||||
cpt += 1
|
||||
return
|
||||
|
||||
@@ -125,7 +148,7 @@ class Email(object):
|
||||
try:
|
||||
getattr(self, "_parse_%s" % msg.get_content_maintype())(msg, level)
|
||||
except AttributeError:
|
||||
self.__parse_default(msg, level)
|
||||
self._parse_default(msg, level)
|
||||
|
||||
def __save_image(self, fname, part):
|
||||
"""Save an inline image on the filesystem.
|
||||
@@ -138,7 +161,7 @@ class Email(object):
|
||||
:param fname: the image associated filename
|
||||
:param part: the email part that contains the image payload
|
||||
"""
|
||||
if re.search("\.\.", fname):
|
||||
if re.search(r"\.\.", fname):
|
||||
return None
|
||||
path = "/static/tmp/" + fname
|
||||
fp = open(settings.MODOBOA_DIR + path, "wb")
|
||||
@@ -250,11 +273,14 @@ def set_email_headers(msg, subject, sender, rcpt):
|
||||
|
||||
Subject, From, To, Message-ID, User-Agent, Date
|
||||
"""
|
||||
import pkg_resources
|
||||
|
||||
msg["Subject"] = Header(subject, 'utf8')
|
||||
msg["From"] = sender
|
||||
msg["To"] = prepare_addresses(rcpt)
|
||||
msg["Message-ID"] = make_msgid()
|
||||
msg["User-Agent"] = "Modoboa"
|
||||
msg["User-Agent"] = "Modoboa %s" % \
|
||||
(pkg_resources.get_distribution("modoboa").version)
|
||||
msg["Date"] = formatdate(time.time(), True)
|
||||
|
||||
|
||||
|
||||
@@ -39,14 +39,6 @@
|
||||
"value": "vmail"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 12,
|
||||
"model": "lib.parameter",
|
||||
"fields": {
|
||||
"name": "stats.IMG_ROOTDIR",
|
||||
"value": "/srv/www/modoboa/media/stats"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 13,
|
||||
"model": "lib.parameter",
|
||||
|
||||
@@ -27,7 +27,10 @@ def decode_mime(m):
|
||||
"""substitute matching encoded_word with unicode equiv.
|
||||
"""
|
||||
h = decode_header(clean_spaces(m))
|
||||
u = unicode(make_header(h))
|
||||
try:
|
||||
u = unicode(make_header(h))
|
||||
except UnicodeDecodeError:
|
||||
return m.group(0)
|
||||
return u
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,142 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
# TuxBrother <vrijwilligerswerk@wij.rs>, 2014
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Modoboa\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2013-12-15 10:16+0100\n"
|
||||
"PO-Revision-Date: 2014-04-16 12:58+0000\n"
|
||||
"Last-Translator: TuxBrother <vrijwilligerswerk@wij.rs>\n"
|
||||
"Language-Team: Dutch (Netherlands) (http://www.transifex.com/projects/p/modoboa/language/nl_NL/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: nl_NL\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: extensions/admin/static/admin/js/admin.js:32
|
||||
msgid "Search"
|
||||
msgstr "Zoeken"
|
||||
|
||||
#: extensions/admin/static/admin/js/admin.js:170
|
||||
msgid "Do not delete domain directory"
|
||||
msgstr "De map van dit domein niet verwijderen (Zal de data ervan behouden)"
|
||||
|
||||
#: extensions/admin/static/admin/js/admin.js:173
|
||||
msgid "This operation will remove ALL data associated to this domain."
|
||||
msgstr "Deze actie zal ALLE data verwijderen die bij dit domein hoort. Wilt u doorgaan?"
|
||||
|
||||
#: extensions/admin/static/admin/js/admin.js:174
|
||||
msgid ""
|
||||
"This operation will remove all data associated to this domain, excepting "
|
||||
"accounts."
|
||||
msgstr "Deze actie zal alle data die bij dit domein hoort verwijderen, behalve de gebruikers."
|
||||
|
||||
#: extensions/admin/static/admin/js/admin.js:222
|
||||
msgid "No domain administrator defined"
|
||||
msgstr "Geen administrator toegewezen"
|
||||
|
||||
#: extensions/admin/static/admin/js/admin.js:291
|
||||
msgid "Do not delete mailbox directory"
|
||||
msgstr "Verwijder de map van deze mailbox niet (zal de data ervan behouden)"
|
||||
|
||||
#: extensions/amavis/static/amavis/js/quarantine.js:180
|
||||
msgid "Release this selection?"
|
||||
msgstr "Wilt u deze selectie loslaten?"
|
||||
|
||||
#: extensions/amavis/static/amavis/js/quarantine.js:184
|
||||
msgid "Delete this selection?"
|
||||
msgstr "Selectie verwijderen?"
|
||||
|
||||
#: extensions/amavis/static/amavis/js/quarantine.js:203
|
||||
#: extensions/amavis/static/amavis/js/selfservice.js:17
|
||||
msgid "Release this message?"
|
||||
msgstr "Dit bericht loslaten?"
|
||||
|
||||
#: extensions/amavis/static/amavis/js/quarantine.js:207
|
||||
#: extensions/amavis/static/amavis/js/selfservice.js:21
|
||||
msgid "Delete this message?"
|
||||
msgstr "Dit bericht verwijderen?"
|
||||
|
||||
#: extensions/amavis/static/amavis/js/quarantine.js:224
|
||||
msgid "View full headers"
|
||||
msgstr "Volledige headers weergeven"
|
||||
|
||||
#: extensions/amavis/static/amavis/js/quarantine.js:229
|
||||
msgid "Hide full headers"
|
||||
msgstr "Volledige headers verbergen"
|
||||
|
||||
#: extensions/postfix_relay_domains/static/postfix_relay_domains/js/relay_domains.js:49
|
||||
msgid "Services updated"
|
||||
msgstr "Services upgedate"
|
||||
|
||||
#: extensions/sievefilters/static/sievefilters/js/lib.js:14
|
||||
msgid "Remove this filter?"
|
||||
msgstr "Dit filter verwijderen?"
|
||||
|
||||
#: extensions/sievefilters/static/sievefilters/js/lib.js:107
|
||||
msgid "Save changes?"
|
||||
msgstr "Wijzigingen opslaan?"
|
||||
|
||||
#: extensions/sievefilters/static/sievefilters/js/lib.js:129
|
||||
msgid "Remove filters set?"
|
||||
msgstr "Deze filterset verwijderen?"
|
||||
|
||||
#: extensions/sievefilters/static/sievefilters/js/lib.js:146
|
||||
msgid "Activate this filters set?"
|
||||
msgstr "Wilt u deze filterset activeren?"
|
||||
|
||||
#: extensions/sievefilters/static/sievefilters/js/lib.js:154
|
||||
#: extensions/sievefilters/static/sievefilters/js/lib.js:203
|
||||
msgid "active"
|
||||
msgstr "activeren"
|
||||
|
||||
#: extensions/stats/static/stats/js/stats.js:37
|
||||
msgid "Search a domain"
|
||||
msgstr "Zoek een domein"
|
||||
|
||||
#: extensions/webmail/static/webmail/js/webmail.js:491
|
||||
msgid "Remove the selected mailbox?"
|
||||
msgstr "Geselecteerde mailbox verwijderen?"
|
||||
|
||||
#: extensions/webmail/static/webmail/js/webmail.js:504
|
||||
msgid "Mailbox removed"
|
||||
msgstr "Mailbox verwijderd"
|
||||
|
||||
#: extensions/webmail/static/webmail/js/webmail.js:746
|
||||
msgid "Message sent"
|
||||
msgstr "Bericht verstuurd"
|
||||
|
||||
#: extensions/webmail/static/webmail/js/webmail.js:997
|
||||
#, c-format
|
||||
msgid "%s file"
|
||||
msgid_plural "%s files"
|
||||
msgstr[0] "%s bestanden"
|
||||
msgstr[1] "%s bestanden"
|
||||
|
||||
#: extensions/webmail/static/webmail/js/webmail.js:1031
|
||||
#, c-format
|
||||
msgid "Moving %s message"
|
||||
msgid_plural "Moving %s messages"
|
||||
msgstr[0] "%s Berichten verplaatsen"
|
||||
msgstr[1] "%s Berichten verplaatsen"
|
||||
|
||||
#: static/js/confirmation.js:50
|
||||
msgid "Warning"
|
||||
msgstr "Waarschuwing"
|
||||
|
||||
#: static/js/confirmation.js:59
|
||||
msgid "Cancel"
|
||||
msgstr "Annuleren"
|
||||
|
||||
#: static/js/global.js:314
|
||||
msgid "Internal error"
|
||||
msgstr "Interne fout"
|
||||
|
||||
#: static/js/searchbar.js:20
|
||||
msgid "Search..."
|
||||
msgstr "Zoeken..."
|
||||
@@ -1,9 +1,9 @@
|
||||
{% load i18n core_tags lib_tags %}
|
||||
<div class="control-group {% if field.errors %}error{% endif %}"{% visirule field %}{% if hidden %}style="display: none"{% endif %}>
|
||||
<div class="control-group {% if field.errors %}error{% endif %}{{ extra_classes }}"{% visirule field %}{% if hidden %}style="display: none"{% endif %}>
|
||||
{% if field.label %}<label class="control-label" for="id_{{ field.html_name }}">{{ field.label }}</label>{% endif %}{% if help_display_mode == "tooltip" and field.help_text %}<a href="#" class="help pull-left" data-original-title="{% trans 'Help:' %} {{ field.label }}" data-content="{{ field.help_text }}" rel="popover">?</a>{% endif %}
|
||||
<div class="controls spaced">
|
||||
{% if appended_text %}<div class="input-append">{% endif %}
|
||||
{{ field }}{% if appended_text %}<span class="add-on">{{ appended_text }}</span></div>{% endif %}{% if help_display_mode == "inline" and field.help_text %}<span class="help-inline">{{ field.help_text }}</span>{% endif %}
|
||||
{{ field }}{% if appended_text %}<span class="add-on">{{ appended_text }}</span></div>{% endif %}{% if help_display_mode == "inline" and field.help_text %}<span class="help-inline">{{ field.help_text }}</span>{% endif %}{% if inline_message %}<span class="help-inline">{{ inline_message }}</span>{% endif %}
|
||||
{% if activator %}<label class="checkbox inline" for="id_{{ field.name }}_act">
|
||||
<input type="checkbox" class="activator" id="id_{{ field.name }}_act" name="{{ field.name }}_act" {% if activator_value %}checked='checked'{% else %}{% if deactivate_if_empty and field.value == None %}checked='checked'{% endif %}{% endif %}/>{{ activator_label }}
|
||||
</label>{% endif %}
|
||||
|
||||
Reference in New Issue
Block a user