diff --git a/doc/conf.py b/doc/conf.py index 3a248f79b..e807a8b4b 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -50,7 +50,7 @@ copyright = u'2014, Antoine Nguyen' # The short X.Y version. version = '1.1' # The full version, including alpha/beta/rc tags. -release = '1.1.1' +release = '1.1.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/modoboa/core/app_settings.py b/modoboa/core/app_settings.py index 006509811..3b3a77df6 100644 --- a/modoboa/core/app_settings.py +++ b/modoboa/core/app_settings.py @@ -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") ) diff --git a/modoboa/core/views/auth.py b/modoboa/core/views/auth.py index 67eb3f973..878b1cf81 100644 --- a/modoboa/core/views/auth.py +++ b/modoboa/core/views/auth.py @@ -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)) diff --git a/modoboa/extensions/admin/__init__.py b/modoboa/extensions/admin/__init__.py index a0f29e83b..12baf0961 100644 --- a/modoboa/extensions/admin/__init__.py +++ b/modoboa/extensions/admin/__init__.py @@ -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) diff --git a/modoboa/extensions/admin/forms/account.py b/modoboa/extensions/admin/forms/account.py index 3c9f3d7c0..8429f20a4 100644 --- a/modoboa/extensions/admin/forms/account.py +++ b/modoboa/extensions/admin/forms/account.py @@ -6,7 +6,7 @@ from modoboa.lib.exceptions import PermDeniedException, Conflict, NotFound from modoboa.lib.permissions import get_account_roles from modoboa.lib.emailutils import split_mailbox from modoboa.lib.formutils import ( - DomainNameField, DynamicForm, TabForms, YesNoField + DomainNameField, DynamicForm, TabForms ) from modoboa.core.models import User from modoboa.extensions.admin.models import ( @@ -28,7 +28,7 @@ class AccountFormGeneral(forms.ModelForm): help_text=ugettext_lazy("What level of permission this user will have") ) password1 = forms.CharField( - label=_("Password"), widget=forms.PasswordInput + label=ugettext_lazy("Password"), widget=forms.PasswordInput ) password2 = forms.CharField( label=ugettext_lazy("Confirmation"), @@ -58,18 +58,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: @@ -323,7 +340,9 @@ class AccountForm(TabForms): def __init__(self, user, *args, **kwargs): self.user = user self.forms = [ - dict(id="general", title=_("General"), cls=AccountFormGeneral, + dict(id="general", title=_("General"), + formtpl="admin/account_general_form.html", + cls=AccountFormGeneral, new_args=[user], mandatory=True), dict(id="mail", title=_("Mail"), formtpl="admin/mailform.html", cls=AccountFormMail), diff --git a/modoboa/extensions/admin/forms/alias.py b/modoboa/extensions/admin/forms/alias.py index 555b98802..a16b14257 100644 --- a/modoboa/extensions/admin/forms/alias.py +++ b/modoboa/extensions/admin/forms/alias.py @@ -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 diff --git a/modoboa/extensions/admin/models/alias.py b/modoboa/extensions/admin/models/alias.py index 62c0d6f5b..64d587b9f 100644 --- a/modoboa/extensions/admin/models/alias.py +++ b/modoboa/extensions/admin/models/alias.py @@ -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)) diff --git a/modoboa/extensions/admin/templates/admin/account_general_form.html b/modoboa/extensions/admin/templates/admin/account_general_form.html new file mode 100644 index 000000000..a7980337b --- /dev/null +++ b/modoboa/extensions/admin/templates/admin/account_general_form.html @@ -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 %} diff --git a/modoboa/extensions/admin/templates/admin/aliasform.html b/modoboa/extensions/admin/templates/admin/aliasform.html index 6a13e26c9..b82adfbf9 100644 --- a/modoboa/extensions/admin/templates/admin/aliasform.html +++ b/modoboa/extensions/admin/templates/admin/aliasform.html @@ -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 %} diff --git a/modoboa/extensions/admin/templates/admin/identities_table.html b/modoboa/extensions/admin/templates/admin/identities_table.html index 0d5200c6c..31d9693bb 100644 --- a/modoboa/extensions/admin/templates/admin/identities_table.html +++ b/modoboa/extensions/admin/templates/admin/identities_table.html @@ -10,7 +10,7 @@ {% for ident in identities %} - + {% identity_modify_link ident %} {{ ident.name_or_rcpt }} {% render_tags ident.tags %} diff --git a/modoboa/extensions/admin/templatetags/admin_tags.py b/modoboa/extensions/admin/templatetags/admin_tags.py index 3f934ddce..40d723203 100644 --- a/modoboa/extensions/admin/templatetags/admin_tags.py +++ b/modoboa/extensions/admin/templatetags/admin_tags.py @@ -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" diff --git a/modoboa/extensions/admin/tests/import_.py b/modoboa/extensions/admin/tests/import_.py index 8f02fed99..8c1e6fc24 100644 --- a/modoboa/extensions/admin/tests/import_.py +++ b/modoboa/extensions/admin/tests/import_.py @@ -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") diff --git a/modoboa/extensions/amavis/sql_listing.py b/modoboa/extensions/amavis/sql_listing.py index b7842eddb..edc06fbb7 100644 --- a/modoboa/extensions/amavis/sql_listing.py +++ b/modoboa/extensions/amavis/sql_listing.py @@ -306,18 +306,16 @@ class SQLlisting(EmailListing): class SQLemail(Email): - def __init__(self, msg, *args, **kwargs): - super(SQLemail, self).__init__(msg, *args, **kwargs) + def __init__(self, *args, **kwargs): + super(SQLemail, self).__init__(*args, **kwargs) fields = ["From", "To", "Cc", "Date", "Subject"] for f in fields: label = f - self.headers += [{"name": label, "value": self.get_header(msg, f)}] - try: - label = re.sub("-", "_", label) - setattr(self, label, msg[f]) - except: - pass - qreason = self.get_header(msg, "X-Amavis-Alert") + self.headers += [ + {"name": label, "value": self.get_header(self.msg, f)} + ] + setattr(self, label, self.msg[f]) + qreason = self.get_header(self.msg, "X-Amavis-Alert") self.qtype = "" self.qreason = "" if qreason != "": @@ -328,11 +326,19 @@ class SQLemail(Email): self.qtype = "BAD HEADER SECTION" self.qreason = qreason[19:] - def get_header(self, msg, name): - for k in [name, name.upper()]: - if k in msg: - return msg[k] - return "" + @property + def msg(self): + """ + """ + import email + + if self._msg is None: + qmails = get_wrapper().get_mail_content(self.mailid) + self._msg = email.message_from_string( + "".join([qm.mail_text for qm in qmails]) + ) + self._parse(self._msg) + return self._msg def render_headers(self, **kwargs): return render_to_string("amavis/mailheaders.html", { diff --git a/modoboa/extensions/amavis/views.py b/modoboa/extensions/amavis/views.py index c4d7f51d1..d57b4af98 100644 --- a/modoboa/extensions/amavis/views.py +++ b/modoboa/extensions/amavis/views.py @@ -92,12 +92,7 @@ def index(request): def getmailcontent_selfservice(request, mail_id): - qmails = get_wrapper().get_mail_content(mail_id) - content = "" - for qm in qmails: - content += qm.mail_text - msg = email.message_from_string(content) - mail = SQLemail(msg, mformat="plain", links="0") + mail = SQLemail(mail_id, mformat="plain", links="0") return render(request, "common/viewmail.html", { "headers": mail.render_headers(), "mailbody": mail.body @@ -106,12 +101,7 @@ def getmailcontent_selfservice(request, mail_id): @selfservice(getmailcontent_selfservice) def getmailcontent(request, mail_id): - qmails = get_wrapper().get_mail_content(mail_id) - content = "" - for qm in qmails: - content += qm.mail_text - msg = email.message_from_string(content) - mail = SQLemail(msg, mformat="plain", links="0") + mail = SQLemail(mail_id, mformat="plain", links="0") return render(request, "common/viewmail.html", { "headers": mail.render_headers(), "mailbody": mail.body diff --git a/modoboa/extensions/postfix_autoreply/management/commands/autoreply.py b/modoboa/extensions/postfix_autoreply/management/commands/autoreply.py index 63fb9b655..3118f6cc9 100644 --- a/modoboa/extensions/postfix_autoreply/management/commands/autoreply.py +++ b/modoboa/extensions/postfix_autoreply/management/commands/autoreply.py @@ -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) diff --git a/modoboa/extensions/sievefilters/forms.py b/modoboa/extensions/sievefilters/forms.py index bcbebd7f4..11047a395 100644 --- a/modoboa/extensions/sievefilters/forms.py +++ b/modoboa/extensions/sievefilters/forms.py @@ -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) diff --git a/modoboa/extensions/sievefilters/views.py b/modoboa/extensions/sievefilters/views.py index 7c5435251..c24fe8d39 100644 --- a/modoboa/extensions/sievefilters/views.py +++ b/modoboa/extensions/sievefilters/views.py @@ -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, diff --git a/modoboa/extensions/webmail/forms.py b/modoboa/extensions/webmail/forms.py index aa6a0bb04..d543e2971 100644 --- a/modoboa/extensions/webmail/forms.py +++ b/modoboa/extensions/webmail/forms.py @@ -1,9 +1,84 @@ # coding: utf-8 +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText + +import lxml.html + from django import forms +from django.conf import settings from django.utils.translation import ugettext_lazy as _ +from modoboa.lib import parameters +from modoboa.lib.emailutils import set_email_headers +from .lib import ( + ImapEmail, create_mail_attachment +) + + +def html2plaintext(content): + """HTML to plain text translation. + + :param content: some HTML content + """ + if not content: + return "" + html = lxml.html.fromstring(content) + plaintext = "" + for ch in html.iter(): + p = None + if ch.text is not None: + p = ch.text.strip('\r\t\n') + if ch.tag == "img": + p = ch.get("alt") + if p is None: + continue + plaintext += p + "\n" + return plaintext + + +def make_body_images_inline(body): + """Looks for images inside the body and make them inline. + + Before sending a message in HTML format, it is necessary to find + all img tags contained in the body in order to rewrite them. For + example, icons provided by CKeditor are stored on the server + filesystem and not accessible from the outside. We must embark + them as parts of the MIME message if we want recipients to + display them correctly. + + :param body: the HTML body to parse + """ + import os + from email.mime.image import MIMEImage + from urlparse import urlparse + + html = lxml.html.fromstring(body) + parts = [] + for tag in html.iter("img"): + src = tag.get("src") + if src is None: + continue + o = urlparse(src) + path = os.path.join(settings.MODOBOA_DIR, o.path[1:]) + if not os.path.exists(path): + continue + fname = os.path.basename(path) + cid = "%s@modoboa" % os.path.splitext(fname)[0] + tag.set("src", "cid:%s" % cid) + with open(path, "rb") as fp: + part = MIMEImage(fp.read()) + part["Content-ID"] = "<%s>" % cid + part.replace_header( + "Content-Type", '%s; name="%s"' % (part["Content-Type"], fname) + ) + part["Content-Disposition"] = "inline" + parts.append(part) + return lxml.html.tostring(html), parts + class ComposeMailForm(forms.Form): + """Compose mail form. + """ to = forms.CharField(label=_("To")) cc = forms.CharField(label=_("Cc"), required=False) cci = forms.CharField(label=_("Cci"), required=False) @@ -13,6 +88,103 @@ class ComposeMailForm(forms.Form): origmsgid = forms.CharField( label="", widget=forms.HiddenInput(), required=False ) + body = forms.CharField(widget=forms.widgets.Textarea) + + def _html_msg(self): + """Create a multipart message. + + We attach two alternatives: + * text/html + * text/plain + """ + msg = MIMEMultipart(_subtype="related") + submsg = MIMEMultipart(_subtype="alternative") + body = self.cleaned_data["body"] + charset = "utf-8" + if body: + tbody = html2plaintext(body) + body, images = make_body_images_inline(body) + else: + tbody = "" + images = [] + submsg.attach( + MIMEText(tbody.encode(charset), _subtype="plain", _charset=charset) + ) + submsg.attach( + MIMEText(body.encode(charset), _subtype="html", _charset=charset) + ) + msg.attach(submsg) + for img in images: + msg.attach(img) + return msg + + def _plain_msg(self): + """Create a simple text message. + """ + charset = "utf-8" + text = MIMEText(self.cleaned_data["body"].encode(charset), + _subtype="plain", _charset=charset) + return text + + def _build_msg(self, request): + """Convert form's content to a MIME message. + """ + editormode = parameters.get_user(request.user, "EDITOR") + msg = getattr(self, "_%s_msg" % editormode)() + + if request.session["compose_mail"]["attachments"]: + wrapper = MIMEMultipart(_subtype="mixed") + wrapper.attach(msg) + for attdef in request.session["compose_mail"]["attachments"]: + wrapper.attach(create_mail_attachment(attdef)) + msg = wrapper + return msg + + def to_msg(self, request): + """Convert form's content to an object ready to send. + + We set headers at the end to be sure no one will override + them. + + """ + msg = self._build_msg(request) + set_email_headers( + msg, self.cleaned_data["subject"], request.user.encoded_address, + self.cleaned_data['to'] + ) + origmsgid = self.cleaned_data.get("origmsgid", None) + if origmsgid: + msg["References"] = msg["In-Reply-To"] = origmsgid + return msg + + +class ForwardMailForm(ComposeMailForm): + """Forward mail form. + """ + + def _build_msg(self, request): + """Convert form's content to a MIME message. + + We also add original attachments (if any) to the new message. + """ + from modoboa.extensions.webmail.lib import decode_payload + + mbox = request.GET.get("mbox", None) + mailid = request.GET.get("mailid", None) + msg = super(ForwardMailForm, self)._build_msg(request) + origmsg = ImapEmail(request, False, "%s:%s" % (mbox, mailid)) + if origmsg.attachments: + if not msg.is_multipart or not msg.get_content_subtype() == "mixed": + wrapper = MIMEMultipart(_subtype="mixed") + wrapper.attach(msg) + msg = wrapper + for attpart, fname in origmsg.attachments.items(): + attdef, payload = origmsg.fetch_attachment(attpart) + attdef["fname"] = fname + msg.attach(create_mail_attachment( + attdef, decode_payload(attdef["encoding"], payload) + )) + return msg class FolderForm(forms.Form): diff --git a/modoboa/extensions/webmail/lib.py b/modoboa/extensions/webmail/lib.py deleted file mode 100644 index 5770086d0..000000000 --- a/modoboa/extensions/webmail/lib.py +++ /dev/null @@ -1,767 +0,0 @@ -# coding: utf-8 -import os -import re -from datetime import datetime, timedelta -import email -import lxml.html -import chardet -from rfc6266 import build_header, parse_headers -from django.core.files.uploadhandler import FileUploadHandler, SkipFile -from django.utils.translation import ugettext as _, ugettext_lazy -from django.conf import settings -from modoboa.lib import u2u_decode, tables, parameters -from modoboa.lib.webutils import size2integer, NavigationParameters -from modoboa.lib.exceptions import InternalError -from modoboa.lib.email_listing import EmailListing -from modoboa.lib.emailutils import ( - EmailAddress, Email, prepare_addresses, set_email_headers -) -from modoboa.extensions.webmail.exceptions import WebmailInternalError -from modoboa.extensions.webmail.imaputils import ( - IMAPconnector, get_imapconnector, BodyStructure -) - - -class WMtable(tables.Table): - tableid = "emails" - styles = "table-condensed" - idkey = "imapid" - drag = tables.ImgColumn( - "drag", cssclass="draggable left", width="1%", - defvalue="%spics/grippy.png" % settings.STATIC_URL, - header="" - ) - selection = tables.SelectionColumn( - "selection", safe=True, width='1%', header=None, sortable=False - ) - flags = tables.ImgColumn("flags", width="3%") - withatts = tables.ImgColumn("withatts", width="2%") - subject = tables.Column( - "subject", label=ugettext_lazy("Subject"), width="50%", limit=60, - cssclass="openable" - ) - from_ = tables.Column( - "from", width="20%", label=ugettext_lazy("From"), limit=30, - cssclass="openable" - ) - date = tables.Column( - "date", width="15%", label=ugettext_lazy("Date"), cssclass="openable" - ) - - cols_order = ["drag", "selection", "withatts", "flags", "subject", "from_", "date"] - - def parse(self, header, value): - if value is None: - return "" - try: - value = getattr(IMAPheader, "parse_%s" % header)(value) - except AttributeError: - pass - return value - - -class IMAPheader(object): - @staticmethod - def to_unicode(value): - if value is None or type(value) is unicode: - return value - try: - value = value.decode('utf-8') - except UnicodeDecodeError: - pass - else: - return value - try: - res = chardet.detect(value) - except UnicodeDecodeError: - return value - if res["encoding"] == "ascii": - return value - return value.decode(res["encoding"]) - - @staticmethod - def parse_address(value, **kwargs): - addr = EmailAddress(value) - if "full" in kwargs.keys() and kwargs["full"]: - return IMAPheader.to_unicode(addr.fulladdress) - result = addr.name and addr.name or addr.fulladdress - return IMAPheader.to_unicode(result) - - @staticmethod - def parse_address_list(values, **kwargs): - lst = values.split(",") - result = "" - for addr in lst: - if result != "": - result += ", " - result += IMAPheader.parse_address(addr, **kwargs) - return result - - @staticmethod - def parse_from(value, **kwargs): - return IMAPheader.parse_address(value, **kwargs) - - @staticmethod - def parse_to(value, **kwargs): - return IMAPheader.parse_address_list(value, **kwargs) - - @staticmethod - def parse_cc(value, **kwargs): - return IMAPheader.parse_address_list(value, **kwargs) - - @staticmethod - def parse_reply_to(value, **kwargs): - return IMAPheader.parse_address_list(value, **kwargs) - - @staticmethod - def parse_date(value, **kwargs): - tmp = email.utils.parsedate_tz(value) - if not tmp: - return value - ndate = datetime(*(tmp)[:7]) - now = datetime.now() - try: - if now - ndate > timedelta(7): - return ndate.strftime("%d.%m.%Y %H:%M") - return ndate.strftime("%a %H:%M") - except ValueError: - return value - - @staticmethod - def parse_message_id(value, **kwargs): - return value.strip('\n') - - @staticmethod - def parse_subject(value, **kwargs): - try: - subject = u2u_decode.u2u_decode(value) - except UnicodeDecodeError: - subject = value - return IMAPheader.to_unicode(subject) - - -class ImapListing(EmailListing): - tpl = "webmail/index.html" - tbltype = WMtable - deflocation = "INBOX/" - defcallback = "wm_updatelisting" - reset_wm_url = False - - def __init__(self, user, password, **kwargs): - self.user = user - self.mbc = IMAPconnector(user=user.username, password=password) - if "pattern" in kwargs and kwargs["pattern"]: - self.parse_search_parameters( - kwargs["criteria"], kwargs["pattern"] - ) - else: - self.mbc.criterions = [] - - super(ImapListing, self).__init__(**kwargs) - self.extravars["refreshrate"] = \ - int(parameters.get_user(user, "REFRESH_INTERVAL")) * 1000 - - def parse_search_parameters(self, criterion, pattern): - - def or_criterion(old, c): - if old == "": - return c - return "OR (%s) (%s)" % (old, c) - - if criterion == u"both": - criterion = u"from_addr, subject" - criterions = "" - if type(pattern) is unicode: - pattern = pattern.encode("utf-8") - if type(criterion) is unicode: - criterion = criterion.encode("utf-8") - for c in criterion.split(','): - if c == "from_addr": - key = "FROM" - elif c == "subject": - key = "SUBJECT" - else: - continue - criterions = \ - or_criterion(criterions, '(%s "%s")' % (key, pattern)) - self.mbc.criterions = [criterions] - - @staticmethod - def computequota(mbc): - try: - return int(float(mbc.quota_actual) \ - / float(mbc.quota_limit) * 100) - except (AttributeError, TypeError): - return -1 - - def getquota(self): - return ImapListing.computequota(self.mbc) - - -class ImapEmail(Email): - headernames = [ - ('From', True), - ('To', True), - ('Cc', True), - ('Date', True), - ('Subject', True), - ] - - def __init__(self, mbox, mailid, request, dformat="DISPLAYMODE", addrfull=False, - links=0): - mformat = parameters.get_user(request.user, "DISPLAYMODE") - self.dformat = parameters.get_user(request.user, dformat) - - self.headers = [] - self.attachments = {} - self.imapc = get_imapconnector(request) - msg = self.imapc.fetchmail(mbox, mailid, readonly=False, - headers=self.headers_as_list) - self.mbox = mbox - self.mailid = mailid - headers = msg['BODY[HEADER.FIELDS (%s)]' % self.headers_as_text] - fallback_fmt = "html" if self.dformat == "plain" else "plain" - - self.bs = BodyStructure(msg['BODYSTRUCTURE']) - data = None - - mformat = self.dformat if self.dformat in self.bs.contents else fallback_fmt - - if len(self.bs.contents): - bodyc = u'' - for part in self.bs.contents[mformat]: - pnum = part['pnum'] - data = self.imapc._cmd("FETCH", mailid, "(BODY.PEEK[%s])" % pnum) - content = decode_payload(part['encoding'], - data[int(mailid)]['BODY[%s]' % pnum]) - charset = self._find_content_charset(part) - if charset is not None: - try: - content = content.decode(charset) - except (UnicodeDecodeError, LookupError): - result = chardet.detect(content) - content = content.decode(result['encoding']) - bodyc += content - - self._fetch_inlines() - self.body = \ - getattr(self, "viewmail_%s" % mformat)(bodyc, links=links) - else: - self.body = None - - self._find_attachments() - - msg = email.message_from_string(headers) - for hdr in self.headernames: - label = hdr[0] - name = hdr[0] - if not name in msg.keys(): - name = name.upper() - if not name in msg.keys(): - continue - try: - key = re.sub("-", "_", name).lower() - value = getattr(IMAPheader, "parse_%s" % key)(msg[name], full=addrfull) - except AttributeError: - value = msg[name] - if hdr[1]: - self.headers += [{"name": label, "value": value}] - try: - label = re.sub("-", "_", label) - setattr(self, label, value) - except: - pass - - @property - def headers_as_list(self): - return [hdr[0].upper() for hdr in self.headernames] - - @property - def headers_as_text(self): - return " ".join(self.headers_as_list) - - def _find_content_charset(self, part): - for pos, elem in enumerate(part["params"]): - if elem == "charset": - return part["params"][pos + 1] - return None - - def _find_attachments(self): - """Retrieve attachments from the parsed body structure. - - We try to find and decode a file name for each attachment. If - we failed, a generic name will be used (ie. part_1, part_2, ...). - """ - for att in self.bs.attachments: - attname = "part_%s" % att["pnum"] - if "params" in att and att["params"] != "NIL": - attname = u2u_decode.u2u_decode(att["params"][1]) \ - .strip("\r\t\n") - elif "disposition" in att and len(att["disposition"]) > 1: - for pos, value in enumerate(att["disposition"][1]): - if not value.startswith("filename"): - continue - header = "%s; %s=%s" \ - % (att['disposition'][0], - value, - att["disposition"][1][pos + 1].strip("\r\t\n")) - attname = parse_headers(header).filename_unsafe - if attname is None: - attname = u2u_decode.u2u_decode( - att["disposition"][1][pos + 1] - ).strip("\r\t\n") - break - self.attachments[att["pnum"]] = attname - - def _fetch_inlines(self): - for cid, params in self.bs.inlines.iteritems(): - if re.search("\.\.", cid): - continue - fname = "webmail/%s_%s" % (self.mailid, cid) - path = os.path.join(settings.MEDIA_ROOT, fname) - params["fname"] = os.path.join(settings.MEDIA_URL, fname) - if os.path.exists(path): - continue - - pdef, content = self.imapc.fetchpart(self.mailid, self.mbox, params["pnum"]) - fp = open(path, "wb") - fp.write(decode_payload(params["encoding"], content)) - fp.close() - - def map_cid(self, url): - m = re.match(".*cid:(.+)", url) - if m: - if m.group(1) in self.bs.inlines: - return self.bs.inlines[m.group(1)]["fname"] - return url - - def render_headers(self, **kwargs): - from django.template.loader import render_to_string - - res = render_to_string("webmail/headers.html", { - "headers": self.headers, - "folder": kwargs["folder"], "mail_id": kwargs["mail_id"], - "attachments": self.attachments != {} and self.attachments or None - }) - return res - - -class Modifier(ImapEmail): - def __init__(self, *args, **kwargs): - super(Modifier, self).__init__(*args, **kwargs) - getattr(self, "_modify_%s" % self.dformat)() - - def _modify_plain(self): - self.body = re.sub("", "", 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 -----
-""" % _("Original message") - - def _header_line_plain(self, key, value): - return "%s: %s" % (key, value) - - def _header_line_html(self, key, value): - return " " % (key, value) - - def _header_end_plain(self): - return "\n" - - def _header_end_html(self): - return "
%s%s
" - - -class EmailSignature(object): - """User signature - - :param user: User object - """ - def __init__(self, user): - self._sig = "" - dformat = parameters.get_user(user, "EDITOR") - content = parameters.get_user(user, "SIGNATURE") - if len(content): - getattr(self, "_format_sig_%s" % dformat)(content) - - def _format_sig_plain(self, content): - self._sig = """ ---- -%s""" % content - - def _format_sig_html(self, content): - content = re.sub("\n", "
", content) - self._sig = """
----
-%s""" % content - - def __repr__(self): - return self._sig - - -def 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)] diff --git a/modoboa/extensions/webmail/lib/__init__.py b/modoboa/extensions/webmail/lib/__init__.py new file mode 100644 index 000000000..bf1f4b89f --- /dev/null +++ b/modoboa/extensions/webmail/lib/__init__.py @@ -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', +] diff --git a/modoboa/extensions/webmail/lib/attachments.py b/modoboa/extensions/webmail/lib/attachments.py new file mode 100644 index 000000000..11ab08f93 --- /dev/null +++ b/modoboa/extensions/webmail/lib/attachments.py @@ -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 diff --git a/modoboa/extensions/webmail/fetch_parser.py b/modoboa/extensions/webmail/lib/fetch_parser.py similarity index 100% rename from modoboa/extensions/webmail/fetch_parser.py rename to modoboa/extensions/webmail/lib/fetch_parser.py diff --git a/modoboa/extensions/webmail/lib/imapemail.py b/modoboa/extensions/webmail/lib/imapemail.py new file mode 100644 index 000000000..e07f92f71 --- /dev/null +++ b/modoboa/extensions/webmail/lib/imapemail.py @@ -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("", "", 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 "

%s: %s

" % (key, value) + + def _header_end_plain(self): + return "\n" + + def _header_end_html(self): + return "" diff --git a/modoboa/extensions/webmail/lib/imapheader.py b/modoboa/extensions/webmail/lib/imapheader.py new file mode 100644 index 000000000..2106ef8e3 --- /dev/null +++ b/modoboa/extensions/webmail/lib/imapheader.py @@ -0,0 +1,114 @@ +""" +Set of functions used to parse and transform email headers. +""" +import chardet +from modoboa.lib.emailutils import EmailAddress + +__all__ = [ + 'parse_from', 'parse_to', 'parse_message_id', 'parse_date', + 'parse_reply_to', 'parse_cc', 'parse_subject' +] + + +def to_unicode(value): + """Try to convert a string to unicode. + """ + if value is None or type(value) is unicode: + return value + try: + value = value.decode('utf-8') + except UnicodeDecodeError: + pass + else: + return value + try: + res = chardet.detect(value) + except UnicodeDecodeError: + return value + if res["encoding"] == "ascii": + return value + return value.decode(res["encoding"]) + + +def parse_address(value, **kwargs): + """Parse an email address. + """ + addr = EmailAddress(value) + if "full" in kwargs.keys() and kwargs["full"]: + return to_unicode(addr.fulladdress) + result = addr.name and addr.name or addr.fulladdress + return to_unicode(result) + + +def parse_address_list(values, **kwargs): + """Parse a list of email addresses. + """ + lst = values.split(",") + result = "" + for addr in lst: + if result != "": + result += ", " + result += parse_address(addr, **kwargs) + return result + + +def parse_from(value, **kwargs): + """Parse a From: header. + """ + return parse_address(value, **kwargs) + + +def parse_to(value, **kwargs): + """Parse a To: header. + """ + return parse_address_list(value, **kwargs) + + +def parse_cc(value, **kwargs): + """Parse a Cc: header. + """ + return parse_address_list(value, **kwargs) + + +def parse_reply_to(value, **kwargs): + """Parse a Reply-To: header. + """ + return parse_address_list(value, **kwargs) + + +def parse_date(value, **kwargs): + """Parse a Date: header. + """ + import datetime + import email + + tmp = email.utils.parsedate_tz(value) + if not tmp: + return value + ndate = datetime.datetime(*(tmp)[:7]) + now = datetime.datetime.now() + try: + if now - ndate > datetime.timedelta(7): + return ndate.strftime("%d.%m.%Y %H:%M") + return ndate.strftime("%a %H:%M") + except ValueError: + return value + + +def parse_message_id(value, **kwargs): + """Parse a Message-ID: header. + """ + return value.strip('\n') + + +def parse_subject(value, **kwargs): + """Parse a Subject: header. + """ + from modoboa.lib import u2u_decode + + try: + subject = u2u_decode.u2u_decode(value) + except UnicodeDecodeError: + subject = value + return to_unicode(subject) + diff --git a/modoboa/extensions/webmail/imaputils.py b/modoboa/extensions/webmail/lib/imaputils.py similarity index 99% rename from modoboa/extensions/webmail/imaputils.py rename to modoboa/extensions/webmail/lib/imaputils.py index 67065363a..c32584b13 100644 --- a/modoboa/extensions/webmail/imaputils.py +++ b/modoboa/extensions/webmail/lib/imaputils.py @@ -14,8 +14,8 @@ from modoboa.lib import parameters, imap_utf7 from modoboa.lib.exceptions import InternalError from modoboa.lib.connections import ConnectionsManager from modoboa.lib.webutils import static_url -from exceptions import ImapError, WebmailInternalError -from fetch_parser import parse_fetch_response +from ..exceptions import ImapError, WebmailInternalError +from .fetch_parser import parse_fetch_response #imaplib.Debug = 4 diff --git a/modoboa/extensions/webmail/lib/listing.py b/modoboa/extensions/webmail/lib/listing.py new file mode 100644 index 000000000..6a49fa149 --- /dev/null +++ b/modoboa/extensions/webmail/lib/listing.py @@ -0,0 +1,107 @@ +from django.conf import settings +from django.utils.translation import ugettext_lazy + +from modoboa.lib import parameters, tables +from modoboa.lib.email_listing import EmailListing +from . import IMAPconnector, imapheader + + +class WMtable(tables.Table): + """ + Webmail listing table. + """ + tableid = "emails" + styles = "table-condensed" + idkey = "imapid" + drag = tables.ImgColumn( + "drag", cssclass="draggable left", width="1%", + defvalue="%spics/grippy.png" % settings.STATIC_URL, + header="" + ) + selection = tables.SelectionColumn( + "selection", safe=True, width='1%', header=None, sortable=False + ) + flags = tables.ImgColumn("flags", width="3%") + withatts = tables.ImgColumn("withatts", width="2%") + subject = tables.Column( + "subject", label=ugettext_lazy("Subject"), width="50%", limit=60, + cssclass="openable" + ) + from_ = tables.Column( + "from", width="20%", label=ugettext_lazy("From"), limit=30, + cssclass="openable" + ) + date = tables.Column( + "date", width="15%", label=ugettext_lazy("Date"), cssclass="openable" + ) + + cols_order = [ + "drag", "selection", "withatts", "flags", "subject", "from_", "date" + ] + + def parse(self, header, value): + if value is None: + return "" + try: + value = getattr(imapheader, "parse_%s" % header)(value) + except AttributeError: + pass + return value + + +class ImapListing(EmailListing): + tpl = "webmail/index.html" + tbltype = WMtable + deflocation = "INBOX/" + defcallback = "wm_updatelisting" + reset_wm_url = False + + def __init__(self, user, password, **kwargs): + self.user = user + self.mbc = IMAPconnector(user=user.username, password=password) + if "pattern" in kwargs and kwargs["pattern"]: + self.parse_search_parameters( + kwargs["criteria"], kwargs["pattern"] + ) + else: + self.mbc.criterions = [] + + super(ImapListing, self).__init__(**kwargs) + self.extravars["refreshrate"] = \ + int(parameters.get_user(user, "REFRESH_INTERVAL")) * 1000 + + def parse_search_parameters(self, criterion, pattern): + + def or_criterion(old, c): + if old == "": + return c + return "OR (%s) (%s)" % (old, c) + + if criterion == u"both": + criterion = u"from_addr, subject" + criterions = "" + if type(pattern) is unicode: + pattern = pattern.encode("utf-8") + if type(criterion) is unicode: + criterion = criterion.encode("utf-8") + for c in criterion.split(','): + if c == "from_addr": + key = "FROM" + elif c == "subject": + key = "SUBJECT" + else: + continue + criterions = \ + or_criterion(criterions, '(%s "%s")' % (key, pattern)) + self.mbc.criterions = [criterions] + + @staticmethod + def computequota(mbc): + try: + return int(float(mbc.quota_actual) \ + / float(mbc.quota_limit) * 100) + except (AttributeError, TypeError): + return -1 + + def getquota(self): + return ImapListing.computequota(self.mbc) diff --git a/modoboa/extensions/webmail/lib/sendmail.py b/modoboa/extensions/webmail/lib/sendmail.py new file mode 100644 index 000000000..ba654cc02 --- /dev/null +++ b/modoboa/extensions/webmail/lib/sendmail.py @@ -0,0 +1,65 @@ +import smtplib +from modoboa.lib import parameters +from modoboa.lib.emailutils import prepare_addresses +from modoboa.lib.webutils import _render_to_string +from modoboa.lib.cryptutils import get_password +from modoboa.extensions.webmail.exceptions import WebmailInternalError +from . import get_imapconnector, clean_attachments + + +def send_mail(request, form, posturl=None): + """Email verification and sending. + + If the form does not present any error, a new MIME message is + constructed. Then, a connection is established with the defined + SMTP server and the message is finally sent. + + :param request: a Request object + :param posturl: the url to post the message form to + :return: a 2-uple (True|False, HttpResponse) + """ + if not form.is_valid(): + editormode = parameters.get_user(request.user, "EDITOR") + listing = _render_to_string( + request, "webmail/compose.html", + {"form": form, "noerrors": True, + "body": form.cleaned_data.get("body", "").strip(), + "posturl": posturl} + ) + return False, dict(status="ko", listing=listing, editor=editormode) + + msg = form.to_msg(request) + rcpts = prepare_addresses(form.cleaned_data["to"], "envelope") + for hdr in ["cc", "cci"]: + if form.cleaned_data[hdr]: + msg[hdr.capitalize()] = prepare_addresses(form.cleaned_data[hdr]) + rcpts += prepare_addresses(form.cleaned_data[hdr], "envelope") + try: + secmode = parameters.get_admin("SMTP_SECURED_MODE") + if secmode == "ssl": + s = smtplib.SMTP_SSL(parameters.get_admin("SMTP_SERVER"), + int(parameters.get_admin("SMTP_PORT"))) + else: + s = smtplib.SMTP(parameters.get_admin("SMTP_SERVER"), + int(parameters.get_admin("SMTP_PORT"))) + if secmode == "starttls": + s.starttls() + except Exception as text: + raise WebmailInternalError(str(text)) + + if parameters.get_admin("SMTP_AUTHENTICATION") == "yes": + try: + s.login(request.user.username, get_password(request)) + except smtplib.SMTPException as err: + raise WebmailInternalError(str(err)) + try: + s.sendmail(request.user.email, rcpts, msg.as_string()) + s.quit() + except smtplib.SMTPException as err: + raise WebmailInternalError(str(err)) + + sentfolder = parameters.get_user(request.user, "SENT_FOLDER") + get_imapconnector(request).push_mail(sentfolder, msg) + clean_attachments(request.session["compose_mail"]["attachments"]) + del request.session["compose_mail"] + return True, {} diff --git a/modoboa/extensions/webmail/lib/signature.py b/modoboa/extensions/webmail/lib/signature.py new file mode 100644 index 000000000..359801a1f --- /dev/null +++ b/modoboa/extensions/webmail/lib/signature.py @@ -0,0 +1,29 @@ +import re +from modoboa.lib import parameters + + +class EmailSignature(object): + """User signature + + :param user: User object + """ + def __init__(self, user): + self._sig = "" + dformat = parameters.get_user(user, "EDITOR") + content = parameters.get_user(user, "SIGNATURE") + if len(content): + getattr(self, "_format_sig_%s" % dformat)(content) + + def _format_sig_plain(self, content): + self._sig = """ +--- +%s""" % content + + def _format_sig_html(self, content): + content = re.sub("\n", "
", content) + self._sig = """
+---
+%s""" % content + + def __repr__(self): + return self._sig diff --git a/modoboa/extensions/webmail/lib/tables.py b/modoboa/extensions/webmail/lib/tables.py new file mode 100644 index 000000000..4830c6799 --- /dev/null +++ b/modoboa/extensions/webmail/lib/tables.py @@ -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="" + ) + 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 diff --git a/modoboa/extensions/webmail/lib/utils.py b/modoboa/extensions/webmail/lib/utils.py new file mode 100644 index 000000000..fc2569ad9 --- /dev/null +++ b/modoboa/extensions/webmail/lib/utils.py @@ -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)] diff --git a/modoboa/extensions/webmail/static/webmail/js/webmail.js b/modoboa/extensions/webmail/static/webmail/js/webmail.js index 14ea120c7..fc7b468f4 100644 --- a/modoboa/extensions/webmail/static/webmail/js/webmail.js +++ b/modoboa/extensions/webmail/static/webmail/js/webmail.js @@ -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)); diff --git a/modoboa/extensions/webmail/templates/webmail/compose.html b/modoboa/extensions/webmail/templates/webmail/compose.html index a05a47d3b..9deee9786 100644 --- a/modoboa/extensions/webmail/templates/webmail/compose.html +++ b/modoboa/extensions/webmail/templates/webmail/compose.html @@ -23,11 +23,11 @@
{% if body %} - {% else %} - + {% endif %}
diff --git a/modoboa/extensions/webmail/templatetags/webmail_tags.py b/modoboa/extensions/webmail/templatetags/webmail_tags.py index 49a907af6..2c32b274a 100644 --- a/modoboa/extensions/webmail/templatetags/webmail_tags.py +++ b/modoboa/extensions/webmail/templatetags/webmail_tags.py @@ -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() diff --git a/modoboa/extensions/webmail/views.py b/modoboa/extensions/webmail/views.py index 730a1e4d3..5f5088ac3 100644 --- a/modoboa/extensions/webmail/views.py +++ b/modoboa/extensions/webmail/views.py @@ -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, diff --git a/modoboa/lib/emailutils.py b/modoboa/lib/emailutils.py index 0f298d9bd..63b6bab67 100644 --- a/modoboa/lib/emailutils.py +++ b/modoboa/lib/emailutils.py @@ -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) diff --git a/modoboa/lib/u2u_decode.py b/modoboa/lib/u2u_decode.py index 751f1022d..7ff7c8fee 100644 --- a/modoboa/lib/u2u_decode.py +++ b/modoboa/lib/u2u_decode.py @@ -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 diff --git a/modoboa/locale/nl_NL/LC_MESSAGES/django.po b/modoboa/locale/nl_NL/LC_MESSAGES/django.po new file mode 100644 index 000000000..9843651eb --- /dev/null +++ b/modoboa/locale/nl_NL/LC_MESSAGES/django.po @@ -0,0 +1,2841 @@ +# 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 , 2014 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-12-15 10:21+0100\n" +"PO-Revision-Date: 2014-04-16 12:58+0000\n" +"Last-Translator: TuxBrother \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" + +#: core/__init__.py:52 core/__init__.py:53 +#: extensions/admin/forms/account.py:326 extensions/admin/forms/domain.py:251 +#: extensions/admin/views/domain.py:95 extensions/admin/views/identity.py:127 +#: extensions/postfix_autoreply/app_settings.py:11 +#: extensions/postfix_relay_domains/forms.py:138 +#: extensions/sievefilters/app_settings.py:50 +#: extensions/stats/app_settings.py:11 extensions/webmail/app_settings.py:11 +msgid "General" +msgstr "Algemeen" + +#: core/app_settings.py:36 +msgid "Authentication" +msgstr "Authenticatie" + +#: core/app_settings.py:39 +msgid "Authentication type" +msgstr "Authenticatie type" + +#: core/app_settings.py:40 +msgid "Local" +msgstr "Lokaal" + +#: core/app_settings.py:43 +msgid "The backend used for authentication" +msgstr "De backend gebruikt voor authenticatie" + +#: core/app_settings.py:48 +msgid "Default password scheme" +msgstr "Standaard wachtwoord encryptiemethode" + +#: core/app_settings.py:51 +msgid "md5crypt (weak)" +msgstr "md5crypt (zwak)" + +#: core/app_settings.py:52 +msgid "sha256 (weak)" +msgstr "sha256 (zwak)" + +#: core/app_settings.py:53 +msgid "md5 (weak)" +msgstr "md5 (zwak)" + +#: core/app_settings.py:54 +msgid "crypt (weak)" +msgstr "crypt (zwak)" + +#: core/app_settings.py:55 +msgid "plain (weak)" +msgstr "plain (onversleuteld!)" + +#: core/app_settings.py:57 +msgid "Scheme used to crypt mailbox passwords" +msgstr "Schema dat gebruikt word om mailbox wachtwoorden te versleutelen" + +#: core/app_settings.py:61 +msgid "Rounds" +msgstr "Rondes" + +#: core/app_settings.py:64 +msgid "" +"Number of rounds to use (only used by sha256crypt and sha512crypt). Must be " +"between 1000 and 999999999, inclusive." +msgstr "Nummer van te gebruiken randomizeringsrondes (alleen bij sha256crypt en sha512crypt). Moet tussen de 1000 en 999999999 liggen." + +#: core/app_settings.py:70 +msgid "Secret key" +msgstr "Geheime sleutel" + +#: core/app_settings.py:72 +msgid "Key used to encrypt/decrypt passwords" +msgstr "Code die gebruikt word om wachtwoorden te versleutelen/ontsleutelen" + +#: core/app_settings.py:76 +msgid "LDAP settings" +msgstr "LDAP instellingen" + +#: core/app_settings.py:79 extensions/sievefilters/app_settings.py:22 +#: extensions/webmail/app_settings.py:22 extensions/webmail/app_settings.py:42 +msgid "Server address" +msgstr "Server adres" + +#: core/app_settings.py:81 +msgid "The IP address of the DNS name of the LDAP server" +msgstr "Het IP adres en de DNS naam van LDAP server" + +#: core/app_settings.py:85 extensions/sievefilters/app_settings.py:28 +#: extensions/webmail/app_settings.py:34 extensions/webmail/app_settings.py:58 +msgid "Server port" +msgstr "Server poort" + +#: core/app_settings.py:87 +msgid "The TCP port number used by the LDAP server" +msgstr "Het TCP poortnummer gebruikt door de LDAP server" + +#: core/app_settings.py:91 extensions/webmail/app_settings.py:28 +msgid "Use a secured connection" +msgstr "Gebruik een beveiligde verbinding" + +#: core/app_settings.py:93 +msgid "Use an SSL/TLS connection to access the LDAP server" +msgstr "Gebruik een SSL/TLS verbinding met de LDAP server" + +#: core/app_settings.py:97 +msgid "Authentication method" +msgstr "Authenticatie methode" + +#: core/app_settings.py:98 +msgid "Search and bind" +msgstr "Zoeken en koppelen" + +#: core/app_settings.py:99 +msgid "Direct bind" +msgstr "Directe koppeling" + +#: core/app_settings.py:101 +msgid "Choose the authentication method to use" +msgstr "Kies een authenticatie methode om te gebruiken" + +#: core/app_settings.py:106 +msgid "Bind DN" +msgstr "Koppeling DN" + +#: core/app_settings.py:108 +msgid "" +"The distinguished name to use when binding to the LDAP server. Leave empty " +"for an anonymous bind" +msgstr "De gedistingeerde naam om te gebruiken bij het koppeling met de LDAP server. Laat leeg voor een anonieme koppeling." + +#: core/app_settings.py:113 +msgid "Bind password" +msgstr "Koppel wachtwoord" + +#: core/app_settings.py:115 +msgid "The password to use when binding to the LDAP server (with 'Bind DN')" +msgstr "Het wachtwoord om te gebruiken tijdens het koppelen met de LDAP server (met 'koppel DN')" + +#: core/app_settings.py:121 +msgid "Users search base" +msgstr "Gebruikers zoekbasis" + +#: core/app_settings.py:123 +msgid "The distinguished name of the search base used to find users" +msgstr "De gedistingeerde naam van de zoekbasis om gebruikers te vinden" + +#: core/app_settings.py:128 +msgid "Search filter" +msgstr "Zoek filter" + +#: core/app_settings.py:130 +msgid "" +"An optional filter string (e.g. '(objectClass=person)'). In order to be " +"valid, it must be enclosed in parentheses." +msgstr "Een optionele filter string (zoals '(objectClass=person)'). Om geldig te kunnen zijn, moet het ingesloten zijn tussen haakjes." + +#: core/app_settings.py:135 +msgid "User DN template" +msgstr "Gebruiker DN template" + +#: core/app_settings.py:137 +#, python-format +msgid "" +"The template used to construct a user's DN. It should contain one " +"placeholder (ie. %(user)s)" +msgstr "Het template dat gebruikt word om een gebruiker zijn/haar DN op te bouwen. Het moet een placeholder bevatten (bijvoorbeeld. %(user)s)" + +#: core/app_settings.py:142 +msgid "Password attribute" +msgstr "Wachtwoord attribuut" + +#: core/app_settings.py:144 +msgid "The attribute used to store user passwords" +msgstr "Dit attribuut word gebruikt om wachtwoorden op te slaan" + +#: core/app_settings.py:148 +msgid "Active Directory" +msgstr "Active Directory" + +#: core/app_settings.py:150 +msgid "Tell if the LDAP server is an Active Directory one" +msgstr "De LDAP server is een Active Directory" + +#: core/app_settings.py:154 +msgid "Administrator groups" +msgstr "Administrator groepen" + +#: core/app_settings.py:157 +msgid "" +"Members of those LDAP Posix groups will be created as domain administrators." +" Use ';' characters to separate groups." +msgstr "Leden van deze LDAP POSIX groepen zullen aangemaakt worden als domein administrators. Gebruik ';' karakters om groepen te scheiden." + +#: core/app_settings.py:164 +msgid "Groups search base" +msgstr "Groepen zoek basis" + +#: core/app_settings.py:167 +msgid "The distinguished name of the search base used to find groups" +msgstr "De gedistingeerde naam van de zoekbasis om groepen te vinden" + +#: core/app_settings.py:172 +#: extensions/admin/templates/admin/editdomainform.html:7 +msgid "Miscellaneous" +msgstr "Diversen" + +#: core/app_settings.py:175 +msgid "Maximum log record age" +msgstr "Maximum bewaartijd van logs" + +#: core/app_settings.py:177 +msgid "The maximum age in days of a log record" +msgstr "De maximum bewaartijd in dagen van een log record" + +#: core/app_settings.py:181 +msgid "Items per page" +msgstr "Items per pagina" + +#: core/app_settings.py:183 +msgid "Number of displayed items per page" +msgstr "Hoeveelheid items per pagina" + +#: core/app_settings.py:187 +msgid "Default top redirection" +msgstr "Standaard top omleiding" + +#: core/app_settings.py:190 +msgid "The default redirection used when no application is specified" +msgstr "De standaard omleiding die gebruikt word als er geen andere applicatie is opgegeven" + +#: core/app_settings.py:218 +msgid "Key must be either 16, 24, or 32 bytes long" +msgstr "Sleutelwaarde moet 16, 24 of 32 bytes lang zijn" + +#: core/app_settings.py:227 extensions/postfix_autoreply/app_settings.py:53 +msgid "Invalid syntax" +msgstr "Ongeldige zinsbouw" + +#: core/app_settings.py:233 +msgid "Invalid rounds number" +msgstr "Ongeldig nummer van rondes" + +#: core/app_settings.py:256 +msgid "This field is required" +msgstr "Dit veld is benodigd" + +#: core/app_settings.py:317 extensions/amavis/app_settings.py:87 +#: extensions/amavis/templates/amavis/main_action_bar.html:29 +#: extensions/webmail/app_settings.py:73 +msgid "Display" +msgstr "Weergeven" + +#: core/app_settings.py:321 +msgid "Prefered language" +msgstr "Voorkeurs taal" + +#: core/app_settings.py:327 +msgid "Prefered language to display pages" +msgstr "Voorkeurstaal om pagina's weer te geven" + +#: core/forms.py:10 extensions/admin/forms/account.py:19 +#: extensions/admin/templates/admin/editdomainform.html:13 +msgid "Username" +msgstr "Gebruikersnaam" + +#: core/forms.py:14 extensions/admin/forms/account.py:31 +msgid "Password" +msgstr "Wachtwoord" + +#: core/forms.py:25 +msgid "Old password" +msgstr "Oud wachtwoord" + +#: core/forms.py:29 +msgid "New password" +msgstr "Nieuw wachtwoord" + +#: core/forms.py:33 extensions/admin/forms/account.py:34 +msgid "Confirmation" +msgstr "Bevestiging" + +#: core/forms.py:56 +msgid "Old password mismatchs" +msgstr "Oud wachtwoord komt niet overeen" + +#: core/forms.py:61 +msgid "Passwords mismatch" +msgstr "Wachtwoorden komt niet overeen" + +#: core/models.py:50 +msgid "password" +msgstr "wachtwoord" + +#: core/models.py:52 +msgid "last login" +msgstr "laatste login" + +#: core/models.py:81 +msgid "You can't delete your own account" +msgstr "U kunt uw eigen account niet verwijderen" + +#: core/models.py:117 +msgid "Failed to update password: LDAP module not installed" +msgstr "Mislukt om wachtwoord aan te passen: LDAP module niet geinstalleerd" + +#: core/models.py:170 +msgid "account" +msgstr "account" + +#: core/models.py:321 extensions/admin/models/domain.py:135 +#: extensions/admin/models/domain_alias.py:39 +#: extensions/postfix_relay_domains/models.py:134 +#: extensions/postfix_relay_domains/models.py:190 +msgid "Invalid line" +msgstr "Verkeerde regel" + +#: core/models.py:325 +msgid "You can't import an account with a role greater than yours" +msgstr "U kunt geen accounts importeren met hogere rechten dan die van uzelf." + +#: core/models.py:331 +#, python-format +msgid "The simple user '%s' must have a valid email address" +msgstr "De gebruiker '%s' moet een geldig email adres hebben" + +#: core/models.py:335 +#, python-format +msgid "username and email fields must not differ for '%s'" +msgstr "gebruikersnaam en email velden moeten niet verschillen van '%s'" + +#: core/models.py:404 extensions/admin/models/alias.py:33 +#: extensions/admin/models/domain.py:32 +#: extensions/admin/models/domain_alias.py:17 +#: extensions/postfix_autoreply/models.py:28 +#: extensions/postfix_relay_domains/models.py:85 +#: extensions/postfix_relay_domains/models.py:167 +msgid "enabled" +msgstr "actief" + +#: core/models.py:405 +msgid "Check to enable this extension" +msgstr "Aanvinken om deze extensie te activeren" + +#: core/models.py:475 +msgid "added" +msgstr "toegevoegd" + +#: core/models.py:478 +msgid "modified" +msgstr "veranderd" + +#: core/models.py:480 core/models.py:504 +#, python-format +msgid "%(object)s '%(name)s' %(action)s by user %(user)s" +msgstr "%(object)s '%(name)s' %(action)s door gebruiker %(user)s" + +#: core/models.py:506 +msgid "deleted" +msgstr "verwijderd" + +#: core/tables.py:8 core/templates/core/information.html:8 +#: extensions/admin/tables.py:9 extensions/admin/forms/domain.py:182 +#: extensions/admin/templates/admin/domains_table.html:5 +#: extensions/admin/templates/admin/editdomainform.html:13 +#: extensions/admin/templates/admin/identities_table.html:5 +#: extensions/sievefilters/forms.py:60 +#: extensions/sievefilters/templates/sievefilters/guieditor.html:6 +msgid "Name" +msgstr "Naam" + +#: core/tables.py:10 extensions/admin/tables.py:11 +msgid "Version" +msgstr "Versie" + +#: core/tables.py:12 extensions/admin/tables.py:13 +msgid "Description" +msgstr "Beschrijving" + +#: core/templates/core/extensions.html:2 core/templatetags/core_tags.py:48 +msgid "Extensions" +msgstr "Extensies" + +#: core/templates/core/extensions.html:2 +msgid "Choose the extension(s) to use" +msgstr "Kies de extensie(s) om te gebruiken" + +#: core/templates/core/extensions.html:6 core/templates/core/parameters.html:7 +#: core/templates/core/user_preferences.html:8 +msgid "Save" +msgstr "Opslaan" + +#: core/templates/core/information.html:2 core/templatetags/core_tags.py:53 +msgid "Information" +msgstr "Informatie" + +#: core/templates/core/information.html:2 +msgid "Various information about Modoboa" +msgstr "Informatie over Modoboa" + +#: core/templates/core/information.html:3 +msgid "is currently in use." +msgstr "is momenteel in gebruik." + +#: core/templates/core/information.html:5 +msgid "Active users" +msgstr "Actieve gebruikers" + +#: core/templates/core/information.html:9 +msgid "Group name" +msgstr "Groep naam" + +#: core/templates/core/information.html:10 +msgid "Last login" +msgstr "Laatste login" + +#: core/templates/core/logs.html:2 core/templatetags/core_tags.py:57 +msgid "Logs" +msgstr "Logs" + +#: core/templates/core/logs.html:2 +msgid "Previously on Modoboa..." +msgstr "Vorige gebeurtenissen op Modoboa..." + +#: core/templates/core/logs.html:6 extensions/amavis/sql_listing.py:46 +#: extensions/webmail/lib.py:48 extensions/webmail/lib.py:425 +msgid "Date" +msgstr "Datum" + +#: core/templates/core/logs.html:7 +msgid "Level" +msgstr "Niveau" + +#: core/templates/core/logs.html:8 +msgid "Logger" +msgstr "Logger" + +#: core/templates/core/logs.html:9 +msgid "Message" +msgstr "Bericht" + +#: core/templates/core/parameters.html:2 core/templatetags/core_tags.py:62 +msgid "Parameters" +msgstr "Parameters" + +#: core/templates/core/parameters.html:2 +msgid "Configure Modoboa" +msgstr "Configureer Modoboa" + +#: core/templates/core/settings_header.html:3 +msgid "Modoboa settings" +msgstr "Instellingen van Modoboa" + +#: core/templates/core/user_index.html:2 core/templatetags/core_tags.py:81 +msgid "Settings" +msgstr "Instellingen" + +#: core/templates/core/user_preferences.html:2 +#: core/templatetags/core_tags.py:108 +msgid "Preferences" +msgstr "Voorkeuren" + +#: core/templates/core/user_preferences.html:2 +msgid "Customize Modoboa" +msgstr "Modoboa aanpassen" + +#: core/templates/core/user_profile.html:2 core/templatetags/core_tags.py:104 +msgid "Profile" +msgstr "Profiel" + +#: core/templates/core/user_profile.html:2 +msgid "Update your personal information" +msgstr "Persoonlijke informatie aanpassen" + +#: core/templates/core/user_profile.html:8 +#: extensions/admin/templates/admin/forward.html:8 +#: extensions/admin/views/alias.py:107 extensions/admin/views/domain.py:165 +#: extensions/admin/views/identity.py:197 +#: extensions/postfix_autoreply/templates/postfix_autoreply/autoreply.html:9 +#: extensions/postfix_relay_domains/views.py:63 +#: extensions/sievefilters/views.py:163 extensions/webmail/views.py:170 +#: extensions/webmail/views.py:205 +msgid "Update" +msgstr "Toepassen" + +#: core/templatetags/core_tags.py:21 +msgid "Modoboa" +msgstr "Modoboa" + +#: core/templatetags/core_tags.py:90 +msgid "Logout" +msgstr "Uitloggen" + +#: core/views/admin.py:47 +msgid "Parameters saved" +msgstr "Parameters opgeslagen" + +#: core/views/admin.py:90 +msgid "Modifications applied." +msgstr "Wijzigingen toegepast." + +#: core/views/auth.py:30 +#, python-format +msgid "User '%s' successfully logged in" +msgstr "Gebruiker '%s' successvol aangemeld" + +#: core/views/auth.py:43 +msgid "Your username and password didn't match. Please try again." +msgstr "Uw gebruikersnaam en wachtwoord komen niet overeen. Probeer het alstublief opnieuw." + +#: core/views/auth.py:66 +#, python-format +msgid "User '%s' logged out" +msgstr "Gebruiker '%s' heeft afgemeld" + +#: core/views/user.py:35 +msgid "Profile updated" +msgstr "Profiel aangepast" + +#: core/views/user.py:58 +msgid "Preferences saved" +msgstr "Voorkeuren opgeslagen" + +#: demo/__init__.py:17 +msgid "Test messages" +msgstr "Testberichten" + +#: demo/__init__.py:22 +msgid "Send virus" +msgstr "Verstuur virus" + +#: demo/__init__.py:25 +msgid "Send spam" +msgstr "Verstuur spam" + +#: demo/views.py:18 demo/views.py:43 +msgid "Message sent" +msgstr "Bericht verstuurd" + +#: demo/templates/demo/login_announcement.html:2 +msgid "" +"\n" +"

Welcome to the online demo!

\n" +"

Warning: the demo's concent is resetted every two hours.

\n" +"

To try the demo, the following users are available (username/password):

\n" +"\n" +"

Please report any issue or give any suggestion to \n" +" the mailing list.

\n" +msgstr "\n

Welkom in de online demo!

\n

Waarschuwing: de content van de demo word elke 2 uur gewist.

\n

Om de demo te proberen, zijn de volgende gebruikers beschikbaar (gebruikersnaam/wachtwoord):

\n\n

Aarzel niet om problemen of suggesties door te geven aan \nde maillijst.

\n

Fouten gevonden in de Nederlandse vertaling? Geef uw opmerkingen of suggesties door aan: foss at waut punt nl

\n" + +#: extensions/admin/__init__.py:48 +msgid "Administration console" +msgstr "Administratie console" + +#: extensions/admin/__init__.py:50 +msgid "Web based console to manage domains, accounts and aliases" +msgstr "Web gebaseerde interface om domeinen, accounts en aliasen te beheren" + +#: extensions/admin/__init__.py:55 +msgid "Administration" +msgstr "Administratie" + +#: extensions/admin/__init__.py:79 +#: extensions/admin/templates/admin/domains.html:2 +#: extensions/limits/app_settings.py:19 extensions/limits/models.py:17 +msgid "Domains" +msgstr "Domeinen" + +#: extensions/admin/__init__.py:85 +#: extensions/admin/templates/admin/identities.html:2 +msgid "Identities" +msgstr "Identiteiten" + +#: extensions/admin/__init__.py:100 +#: extensions/admin/templates/admin/forward.html:2 +#: extensions/webmail/templatetags/webmail_tags.py:28 +msgid "Forward" +msgstr "Doorsturen" + +#: extensions/admin/__init__.py:163 +#, python-format +msgid "Account import failed (%s): domain does not exist" +msgstr "Accounts importeren mislukt (%s): domein bestaat niet" + +#: extensions/admin/app_settings.py:11 +#: extensions/admin/templates/admin/editdomainform.html:25 +#: extensions/limits/app_settings.py:31 extensions/limits/models.py:23 +#: extensions/webmail/app_settings.py:107 +#: extensions/webmail/templates/webmail/folders.html:4 +msgid "Mailboxes" +msgstr "Mailboxen" + +#: extensions/admin/app_settings.py:14 +msgid "Handle mailboxes on filesystem" +msgstr "Pas mailboxen op het bestandssysteem aan" + +#: extensions/admin/app_settings.py:16 +msgid "" +"Rename or remove mailboxes on the filesystem when they get renamed or " +"removed within Modoboa" +msgstr "Hernoem of verwijder mailboxen op het bestandssysteem wanner ze hernoemd of verwijderd worden in Modoboa." + +#: extensions/admin/app_settings.py:20 +msgid "Mailboxes ower" +msgstr "Eigenaar van mailboxen" + +#: extensions/admin/app_settings.py:22 +msgid "The UNIX account who owns mailboxes on the filesystem" +msgstr "De UNIX account die de rechten heeft tot de mailboxen op het bestandssysteem" + +#: extensions/admin/app_settings.py:26 +msgid "Default domain quota" +msgstr "Standaard domein quota" + +#: extensions/admin/app_settings.py:29 +msgid "" +"Default quota (in MB) applied to freshly created domains with no value " +"specified. A value of 0 means no quota." +msgstr "Standaard quota (in MB) om op nieuw aangemaakte domeinen zonder waarde toe te passen. Een waarde van 0 betekend geen quota." + +#: extensions/admin/app_settings.py:36 +msgid "Automatic account removal" +msgstr "Automatish een account verwijderen" + +#: extensions/admin/app_settings.py:38 +msgid "When a mailbox is removed, also remove the associated account" +msgstr "Als een mailbox word verwijderd, verwijder dan ook de bijbehorende account" + +#: extensions/admin/app_settings.py:69 extensions/admin/forms/account.py:178 +#: extensions/admin/forms/domain.py:56 +msgid "Must be a positive integer" +msgstr "Dit moet een positive integer zijn." + +#: extensions/admin/lib.py:28 +msgid "A mailbox is required" +msgstr "Een mailbox is benodigd" + +#: extensions/admin/forms/account.py:21 +msgid "" +"The user's name. Must be a valid e-mail address for simple users or " +"administrators with a mailbox." +msgstr "De naam van de gebruiker. Moet een geldig e-mail adres zijn voor gebruikers of administrators met een mailbox." + +#: extensions/admin/forms/account.py:26 +msgid "Role" +msgstr "Regel" + +#: extensions/admin/forms/account.py:27 extensions/admin/forms/account.py:55 +msgid "Choose" +msgstr "Kiezen" + +#: extensions/admin/forms/account.py:28 +msgid "What level of permission this user will have" +msgstr "Welk niveau van rechten deze gebruiker zal hebben" + +#: extensions/admin/forms/account.py:36 +msgid "Enter the same password as above, for verification." +msgstr "Herhaal wachtwoord (voor verificatie)" + +#: extensions/admin/forms/account.py:47 +msgid "Enabled" +msgstr "Actief" + +#: extensions/admin/forms/account.py:94 +msgid "The two password fields didn't match." +msgstr "De wachtwoorden komen niet overeen." + +#: extensions/admin/forms/account.py:100 +msgid "You can't disable your own account" +msgstr "U kunt uw eigen account niet uitschakelen" + +#: extensions/admin/forms/account.py:111 +msgid "E-mail" +msgstr "E-mail" + +#: extensions/admin/forms/account.py:113 extensions/admin/forms/domain.py:17 +msgid "Quota" +msgstr "Quota" + +#: extensions/admin/forms/account.py:115 +msgid "" +"Quota in MB for this mailbox. Define a custom value or use domain's default " +"one. Leave empty to define an unlimited value (not allowed for domain " +"administrators)." +msgstr "Quota in MB voor deze mailbox. Geef een aangepaste waarde of gebruik de stanaard waarde van het domein. Laat leeg om een ongelimiteerde waarde te definieren (niet toegestaan voor domein administrators)." + +#: extensions/admin/forms/account.py:123 extensions/admin/forms/domain.py:26 +#: extensions/admin/templates/admin/domains_table.html:6 +#: extensions/postfix_relay_domains/forms.py:12 +msgid "Alias(es)" +msgstr "Alias(sen)" + +#: extensions/admin/forms/account.py:126 +msgid "" +"Alias(es) of this mailbox. Indicate only one address per input, press ENTER " +"to add a new input. Use the '*' character to create a 'catchall' alias (ex: " +"*@domain.tld)." +msgstr "Alias(en) van deze mailbox. Geef slechts een adres per regel op, en toets ENTER om een nieuwe regel te krijgen. Gebruik een wildcard '*' om een 'catch-all' adres te maken (bijvoorbeeld: *@domein.tld)." + +#: extensions/admin/forms/account.py:187 extensions/admin/forms/account.py:217 +#: extensions/admin/forms/alias.py:65 +msgid "Domain does not exist" +msgstr "Domein bestaat niet" + +#: extensions/admin/forms/account.py:196 +#, python-format +msgid "Mailbox %s already exists" +msgstr "Mailbox %s bestaat al" + +#: extensions/admin/forms/account.py:284 +msgid "Domain(s)" +msgstr "Domein(en)" + +#: extensions/admin/forms/account.py:286 +msgid "Domain(s) that user administrates" +msgstr "Domein(en) die gebruiker mag beheren" + +#: extensions/admin/forms/account.py:328 +#: extensions/admin/views/identity.py:130 +msgid "Mail" +msgstr "Mail" + +#: extensions/admin/forms/account.py:330 +msgid "Permissions" +msgstr "Rechten" + +#: extensions/admin/forms/alias.py:16 +msgid "Email address" +msgstr "Email adres" + +#: extensions/admin/forms/alias.py:18 +msgid "" +"The distribution list address. Use the '*' character to create a 'catchall' " +"address (ex: *@domain.tld)." +msgstr "Het adres van de distributielijst . Gebruik een wildcard '*' om een 'catch-all' adres te maken (bijvoorbeeld: *@domein.tld)" + +#: extensions/admin/forms/alias.py:23 +msgid "Recipients" +msgstr "Ontvangers" + +#: extensions/admin/forms/alias.py:25 +msgid "" +"Mailbox(es) this alias will point to. Indicate only one address per input, " +"press ENTER to add a new input." +msgstr "Mailbox(en) waarnaar deze alias zal verwijzen. Geef slechts een adres per regel op, en toets ENTER om een nieuwe regel te krijgen." + +#: extensions/admin/forms/alias.py:68 +msgid "You don't have access to this domain" +msgstr "U heeft geen toegang tot dit domein" + +#: extensions/admin/forms/alias.py:90 +msgid "Invalid mailbox" +msgstr "Verkeerde mailbox" + +#: extensions/admin/forms/alias.py:108 extensions/admin/models/alias.py:182 +#, python-format +msgid "Local recipient %s not found" +msgstr "Lokale ontvanger %s niet gevonden" + +#: extensions/admin/forms/alias.py:112 extensions/admin/forms/alias.py:120 +#, python-format +msgid "Recipient %s already present" +msgstr "Ontvanger %s is al aanwezig" + +#: extensions/admin/forms/alias.py:126 +msgid "No recipient defined" +msgstr "Geen ontvanger opgegeven" + +#: extensions/admin/forms/domain.py:20 +msgid "" +"Default quota in MB applied to mailboxes. Leave empty to use the default " +"value." +msgstr "Standaard quota in MB toegepast op mailboxen. Laat leeg om de standaard waarde te gebruiken." + +#: extensions/admin/forms/domain.py:29 +msgid "" +"Alias(es) of this domain. Indicate only one name per input, press ENTER to " +"add a new input." +msgstr "Alias(sen) van dit domein. Geef slechts een adres per regel op, en toets ENTER om een nieuwe regel te krijgen." + +#: extensions/admin/forms/domain.py:75 +#: extensions/postfix_relay_domains/forms.py:50 +#: extensions/postfix_relay_domains/forms.py:70 +msgid "domain alias" +msgstr "domein alias" + +#: extensions/admin/forms/domain.py:82 extensions/admin/forms/domain.py:98 +#: extensions/postfix_relay_domains/forms.py:58 +#: extensions/postfix_relay_domains/forms.py:78 +#, python-format +msgid "A %s with this name already exists" +msgstr "Een %s met deze naam bestaat al" + +#: extensions/admin/forms/domain.py:92 +#: extensions/postfix_relay_domains/forms.py:49 +#: extensions/postfix_relay_domains/forms.py:71 +msgid "domain" +msgstr "domein" + +#: extensions/admin/forms/domain.py:176 +msgid "Create a domain administrator" +msgstr "Maak een domein administrator aan" + +#: extensions/admin/forms/domain.py:178 +msgid "Automatically create an administrator for this domain" +msgstr "Automatisch een administrator aanmaken voor dit domein" + +#: extensions/admin/forms/domain.py:185 +msgid "" +"The administrator's name. Don't include the domain's name here, it will be " +"automatically appended." +msgstr "De naam van de administrator. De domeinnaam word automatisch toegevoegd." + +#: extensions/admin/forms/domain.py:193 +msgid "Create aliases" +msgstr "Aliassen aanmaken" + +#: extensions/admin/forms/domain.py:195 +msgid "Automatically create standard aliases for this domain" +msgstr "Automatisch een standaard alias voor dit domein aanmaken" + +#: extensions/admin/forms/domain.py:211 +msgid "Invalid format" +msgstr "Verkeerd formaat" + +#: extensions/admin/forms/domain.py:225 +#, python-format +msgid "User '%s' already exists" +msgstr "Gebruiker '%s' bestaat al" + +#: extensions/admin/forms/export.py:7 +msgid "File name" +msgstr "Bestandsnaam" + +#: extensions/admin/forms/export.py:10 extensions/admin/forms/import_.py:8 +msgid "Separator" +msgstr "Scheidingsteken" + +#: extensions/admin/forms/forward.py:12 +msgid "Recipient(s)" +msgstr "Ontvanger(s)" + +#: extensions/admin/forms/forward.py:15 +msgid "Indicate one or more recipients separated by a ','" +msgstr "Geef een of meer ontvangers op, gescheiden door een komma" + +#: extensions/admin/forms/forward.py:18 +msgid "Keep local copies" +msgstr "Lokale kopie behouden" + +#: extensions/admin/forms/forward.py:20 +msgid "Forward messages and store copies into your local mailbox" +msgstr "Stuur berichten door en sla een kopie op in uw mailbox" + +#: extensions/admin/forms/forward.py:38 +msgid "" +"You can't define a forward to a local destination. Please ask your " +"administrator to create an alias instead." +msgstr "U kunt mail niet doorsturen naar een lokaal adres. Vraag uw systeembeheerder om een alias voor u aan te maken." + +#: extensions/admin/forms/import_.py:6 extensions/webmail/forms.py:26 +msgid "Select a file" +msgstr "Bestand selecteren" + +#: extensions/admin/forms/import_.py:11 +msgid "Continue on error" +msgstr "Doorgaan als er een fout optreed" + +#: extensions/admin/forms/import_.py:12 +msgid "Don't treat duplicated objects as error" +msgstr "Zie duplicaten niet als fout" + +#: extensions/admin/forms/import_.py:27 +msgid "Crypt passwords" +msgstr "Versleutel wachtwoorden" + +#: extensions/admin/forms/import_.py:28 +msgid "Check this option if passwords contained in your file are not crypted" +msgstr "Vink deze optie aan als wachtwoorden in uw bestand niet versleuteld zijn" + +#: extensions/admin/models/alias.py:16 extensions/admin/models/mailbox.py:51 +msgid "address" +msgstr "adres" + +#: extensions/admin/models/alias.py:18 +msgid "" +"The alias address (without the domain part). For a 'catch-all' address, just" +" enter an * character." +msgstr "Het alias adres (zonder het domein deel). Voor een 'catch-all' adres, vul een * in." + +#: extensions/admin/models/alias.py:24 +msgid "mailboxes" +msgstr "mailboxen" + +#: extensions/admin/models/alias.py:25 +msgid "The mailboxes this alias points to" +msgstr "De mailbox waarnaar deze alias verwijst" + +#: extensions/admin/models/alias.py:29 +msgid "The aliases this alias points to" +msgstr "Deze aliassen waarnaar deze alias verwijst" + +#: extensions/admin/models/alias.py:34 +#: extensions/admin/models/domain_alias.py:18 +#: extensions/postfix_relay_domains/models.py:168 +msgid "Check to activate this alias" +msgstr "Aanvinken om deze alias te activeren" + +#: extensions/admin/models/alias.py:79 +msgid "distribution list" +msgstr "distributie lijst" + +#: extensions/admin/models/alias.py:80 +msgid "forward" +msgstr "doorsturen" + +#: extensions/admin/models/alias.py:81 +msgid "alias" +msgstr "alias" + +#: extensions/admin/models/alias.py:150 +#, python-format +msgid "Invalid line: %s" +msgstr "Ongeldige regel: %s" + +#: extensions/admin/models/alias.py:155 +#, python-format +msgid "Domain '%s' does not exist" +msgstr "Domein '%s' bestaat niet" + +#: extensions/admin/models/domain.py:28 +#: extensions/admin/models/domain_alias.py:10 +#: extensions/postfix_relay_domains/models.py:58 +#: extensions/postfix_relay_domains/models.py:76 +#: extensions/postfix_relay_domains/models.py:159 +msgid "name" +msgstr "naam" + +#: extensions/admin/models/domain.py:29 +#: extensions/postfix_relay_domains/models.py:77 +msgid "The domain name" +msgstr "De domeinnaam" + +#: extensions/admin/models/domain.py:33 +#: extensions/postfix_relay_domains/models.py:86 +msgid "Check to activate this domain" +msgstr "Aanvinken om dit domein te activeren" + +#: extensions/admin/models/domain.py:61 +msgid "Domain" +msgstr "Domein" + +#: extensions/admin/models/domain.py:140 +#, python-format +msgid "Invalid quota value for domain '%s'" +msgstr "Verkeerde quota waarde voor domein '%s'" + +#: extensions/admin/models/domain_alias.py:11 +#: extensions/postfix_relay_domains/models.py:160 +msgid "The alias name" +msgstr "De alias naam" + +#: extensions/admin/models/domain_alias.py:13 +#: extensions/postfix_relay_domains/models.py:163 +msgid "target" +msgstr "doel" + +#: extensions/admin/models/domain_alias.py:14 +msgid "The domain this alias points to" +msgstr "De domein alias verwijst naar" + +#: extensions/admin/models/domain_alias.py:46 +#, python-format +msgid "A domain %s already exists" +msgstr "Een domein %s bestaat al" + +#: extensions/admin/models/domain_alias.py:51 +#, python-format +msgid "Unknown domain %s" +msgstr "Onbekend domein %s" + +#: extensions/admin/models/mailbox.py:52 +msgid "Mailbox address (without the @domain.tld part)" +msgstr "Mailbox adres (zonder het @domein.naam deel)" + +#: extensions/admin/models/mailbox.py:113 +#, python-format +msgid "Failed to retrieve mailbox location (%s)" +msgstr "Mislukt om de mailbox locatie te verkijgen (%s)" + +#: extensions/admin/models/mailbox.py:173 +#, python-format +msgid "Quota is greater than the allowed domain's limit (%dM)" +msgstr "Quota is groter dan de maximaal toegestane domein limiet (%dM)" + +#: extensions/admin/models/mailbox.py:178 +msgid "A quota is required" +msgstr "Een quota is vereist" + +#: extensions/admin/templates/admin/domain_general_form.html:3 +#: extensions/admin/templates/admin/mailform.html:3 +#: extensions/admin/templates/admin/newdomainform.html:3 +msgid "MB" +msgstr "MB" + +#: extensions/admin/templates/admin/domain_options_form.html:5 +msgid "You can't create a new domain administrator" +msgstr "U kunt geen nieuwe domein administrator aanmaken" + +#: extensions/admin/templates/admin/domains_table.html:7 +#: extensions/admin/templates/admin/identities_table.html:7 +msgid "Tags" +msgstr "Labels" + +#: extensions/admin/templates/admin/domains_table.html:8 +#: extensions/admin/templates/admin/identities_table.html:8 +#: extensions/admin/templates/admin/listing.html:17 +#: extensions/sievefilters/views.py:93 +#: extensions/sievefilters/templates/sievefilters/filter.html:17 +#: extensions/sievefilters/templates/sievefilters/guieditor.html:5 +#: extensions/webmail/templatetags/webmail_tags.py:101 +msgid "Actions" +msgstr "Acties" + +#: extensions/admin/templates/admin/editdomainform.html:4 +msgid "Administrators" +msgstr "Administrators" + +#: extensions/admin/templates/admin/editdomainform.html:19 +msgid "No domain administrator defined" +msgstr "Geen domein administrator toegewezen" + +#: extensions/admin/templates/admin/editdomainform.html:22 +msgid "Creation date" +msgstr "Datum van aanmaken" + +#: extensions/admin/templates/admin/editdomainform.html:23 +msgid "Last modification date" +msgstr "Laatst gewijzigd" + +#: extensions/admin/templates/admin/editdomainform.html:24 +#: extensions/limits/app_settings.py:25 extensions/limits/models.py:20 +msgid "Domain aliases" +msgstr "Domein aliassen" + +#: extensions/admin/templates/admin/editdomainform.html:26 +#: extensions/limits/app_settings.py:37 extensions/limits/models.py:25 +msgid "Mailbox aliases" +msgstr "Mailbox aliassen" + +#: extensions/admin/templates/admin/forward.html:2 +msgid "Automatically forward emails to another address" +msgstr "Automatisch mails doorsturen naar een ander adres" + +#: extensions/admin/templates/admin/identities_table.html:6 +msgid "Fullname/Recipient" +msgstr "Volledige naam/Ontvanger" + +#: extensions/admin/templates/admin/importform.html:5 +msgid "Help" +msgstr "Help" + +#: extensions/admin/templates/admin/importform.html:12 +msgid "Importing..." +msgstr "Importeren..." + +#: extensions/admin/templates/admin/listing.html:14 +#: extensions/stats/templates/stats/index.html:28 +msgid "Filter" +msgstr "Filter" + +#: extensions/admin/templates/admin/mailboxes_raw.html:3 +msgid "Choose a value" +msgstr "Kies een waarde" + +#: extensions/admin/templates/admin/mailform.html:3 +msgid "Use domain default value" +msgstr "Gebruik de standaard waarde van dit domein" + +#: extensions/admin/templates/admin/quotas.html:5 +msgid "Address" +msgstr "Adres" + +#: extensions/admin/templates/admin/quotas.html:6 +msgid "Value" +msgstr "Waarde" + +#: extensions/admin/templates/admin/quotas.html:7 +msgid "Limit" +msgstr "Limiet" + +#: extensions/admin/templates/admin/quotas.html:8 +#, python-format +msgid "Usage (%%)" +msgstr "Gebruik (%%)" + +#: extensions/admin/templates/admin/searchform.html:6 +msgid "Search" +msgstr "Zoeken" + +#: extensions/admin/templatetags/admin_tags.py:12 +msgid "enabled_m" +msgstr "geactiveerd_m" + +#: extensions/admin/templatetags/admin_tags.py:12 +msgid "enabled_f" +msgstr "geactiveerd_f" + +#: extensions/admin/templatetags/admin_tags.py:33 +msgid "Add domain" +msgstr "Domein toevoegen" + +#: extensions/admin/templatetags/admin_tags.py:42 +#: extensions/admin/templatetags/admin_tags.py:106 +#: extensions/admin/views/import.py:51 extensions/admin/views/import.py:166 +msgid "Import" +msgstr "Importeren" + +#: extensions/admin/templatetags/admin_tags.py:48 +#: extensions/admin/templatetags/admin_tags.py:112 +#: extensions/admin/views/export.py:38 extensions/admin/views/export.py:64 +msgid "Export" +msgstr "Exporteren" + +#: extensions/admin/templatetags/admin_tags.py:72 +msgid "List identities" +msgstr "Weergeef identiteiten" + +#: extensions/admin/templatetags/admin_tags.py:77 +msgid "List quotas" +msgstr "Weergeef quota's" + +#: extensions/admin/templatetags/admin_tags.py:82 +msgid "Add account" +msgstr "Account aanmaken" + +#: extensions/admin/templatetags/admin_tags.py:88 +msgid "Add alias" +msgstr "Alias toevoegen" + +#: extensions/admin/templatetags/admin_tags.py:94 +msgid "Add forward" +msgstr "Doorsuurregel toevoegen" + +#: extensions/admin/templatetags/admin_tags.py:100 +msgid "Add distribution list" +msgstr "Distributielijst toevoegen" + +#: extensions/admin/templatetags/admin_tags.py:133 +msgid "View the domain's identities" +msgstr "Weergeef de identiteiten van dit domein" + +#: extensions/admin/templatetags/admin_tags.py:143 +#: extensions/admin/templatetags/admin_tags.py:162 +#: extensions/admin/templatetags/admin_tags.py:169 +#: extensions/postfix_relay_domains/general_callbacks.py:84 +#, python-format +msgid "Delete %s?" +msgstr "Verwijder %s?" + +#: extensions/admin/templatetags/admin_tags.py:239 +msgid "Remove this permission" +msgstr "Deze bevoegdheid verwijderen" + +#: extensions/admin/views/alias.py:25 +msgid "Alias with this name already exists" +msgstr "Een alias met deze naam bestaat al" + +#: extensions/admin/views/alias.py:49 extensions/admin/views/domain.py:100 +#: extensions/admin/views/domain.py:129 extensions/admin/views/identity.py:131 +#: extensions/admin/views/identity.py:158 +#: extensions/postfix_relay_domains/views.py:32 +#: extensions/sievefilters/views.py:141 extensions/sievefilters/views.py:230 +#: extensions/webmail/views.py:151 +msgid "Create" +msgstr "Aanmaken" + +#: extensions/admin/views/alias.py:61 +msgid "New distribution list" +msgstr "Nieuwe distributie lijst" + +#: extensions/admin/views/alias.py:62 +msgid "Distribution list created" +msgstr "Distributielijst aangemaakt" + +#: extensions/admin/views/alias.py:71 +msgid "New alias" +msgstr "Nieuwe alias" + +#: extensions/admin/views/alias.py:72 +msgid "Alias created" +msgstr "Alias aangemaakt" + +#: extensions/admin/views/alias.py:81 +msgid "New forward" +msgstr "Nieuwe doorstuurregel" + +#: extensions/admin/views/alias.py:82 +msgid "Forward created" +msgstr "Doorstuurregel aangemaakt" + +#: extensions/admin/views/alias.py:95 +msgid "Distribution list modified" +msgstr "Distributielijst aangepast" + +#: extensions/admin/views/alias.py:97 +msgid "Forward modified" +msgstr "Doorstuurregel aangepast" + +#: extensions/admin/views/alias.py:99 +msgid "Alias modified" +msgstr "Alias aangepast" + +#: extensions/admin/views/domain.py:96 extensions/admin/views/identity.py:128 +msgid "Next" +msgstr "Volgende" + +#: extensions/admin/views/domain.py:99 +msgid "Options" +msgstr "Opties" + +#: extensions/admin/views/domain.py:101 extensions/admin/views/identity.py:132 +msgid "Previous" +msgstr "Vorige" + +#: extensions/admin/views/domain.py:123 +msgid "Domain created" +msgstr "Domein aangemaakt" + +#: extensions/admin/views/domain.py:128 +msgid "New domain" +msgstr "Nieuw domein" + +#: extensions/admin/views/domain.py:155 +msgid "Domain modified" +msgstr "Domein gewijzigd" + +#: extensions/admin/views/domain.py:189 +msgid "You can't delete your own domain" +msgstr "U kunt uw eigen domein niet verwijderen" + +#: extensions/admin/views/domain.py:192 +msgid "Domain deleted" +msgid_plural "Domains deleted" +msgstr[0] "Domeinen verwijderd" +msgstr[1] "Domeinen verwijderd" + +#: extensions/admin/views/export.py:37 +msgid "Export identities" +msgstr "Identiteiten exporteren" + +#: extensions/admin/views/export.py:63 +msgid "Export domains" +msgstr "Exporteer domeinen" + +#: extensions/admin/views/identity.py:103 +#: extensions/admin/views/identity.py:224 +#: extensions/admin/views/identity.py:229 extensions/amavis/views.py:179 +#: extensions/amavis/views.py:185 extensions/amavis/views.py:220 +#: extensions/amavis/views.py:224 extensions/amavis/views.py:226 +#: extensions/webmail/views.py:47 extensions/webmail/views.py:65 +#: extensions/webmail/views.py:79 extensions/webmail/views.py:97 +#: extensions/webmail/views.py:114 extensions/webmail/views.py:200 +#: extensions/webmail/views.py:223 extensions/webmail/views.py:396 +#: extensions/webmail/views.py:430 extensions/webmail/views.py:443 +#: extensions/webmail/views.py:472 extensions/webmail/views.py:512 +#: lib/formutils.py:44 lib/formutils.py:49 +msgid "Invalid request" +msgstr "Ongeldig verzoek" + +#: extensions/admin/views/identity.py:149 +msgid "Account created" +msgstr "Account aangemaakt" + +#: extensions/admin/views/identity.py:155 +msgid "New account" +msgstr "Nieuwe account" + +#: extensions/admin/views/identity.py:190 +msgid "Account updated" +msgstr "Account aangepast" + +#: extensions/admin/views/identity.py:214 +msgid "Account deleted" +msgid_plural "Accounts deleted" +msgstr[0] "Account verwijderd" +msgstr[1] "Accounts verwijderd" + +#: extensions/admin/views/import.py:39 +#, python-format +msgid "" +"Provide a CSV file where lines respect one of the following formats:\n" +"\n" +"

The first element of each line is mandatory and must be equal to one of the previous values.

\n" +"

You can use a different character as separator.

\n" +msgstr "Lever een CSV bestand en zorg dat de regels voldoen aan een van de volgende formaten:\n\n

Het eerste element van elke regel is verplicht en moet gelijk zijn aan een van de vorige waarden.

\n

U kunt een eigen scheidingsteken gebruiken.

\n

Let op! Zorg dat het 'line-encoding format' Unix is - anders gaat het importeren mis

\n\n" + +#: extensions/admin/views/import.py:50 +msgid "Import domains" +msgstr "Importeer domeinen" + +#: extensions/admin/views/import.py:129 +#, python-format +msgid "Object already exists: %s" +msgstr "Object bestaat al: %s" + +#: extensions/admin/views/import.py:133 +#, python-format +msgid "%d objects imported successfully" +msgstr "%d objecten successvol geimporteerd" + +#: extensions/admin/views/import.py:153 +msgid "" +"Provide a CSV file where lines respect one of the following formats:\n" +"\n" +"

The first element of each line is mandatory and must be equal to one of the previous values.

\n" +"\n" +"

You can use a different character as separator.

\n" +msgstr "Lever een CSV bestand en zorg dat de regels voldoen aan een van de volgende formaten:\n\n

Het eerste element van elke regel is verplicht en moet gelijk zijn aan een van de vorige waarden.

\n

U kunt een eigen scheidingsteken gebruiken.

\n

Let op! Zorg dat het 'line-encoding format' Unix is - anders gaat het importeren mis

\n" + +#: extensions/admin/views/import.py:165 +msgid "Import identities" +msgstr "Identiteiten importeren" + +#: extensions/admin/views/user.py:37 +msgid "Forward updated" +msgstr "Doorstuuradres aangepast" + +#: extensions/amavis/__init__.py:21 +msgid "Simple amavis management frontend" +msgstr "Gemakkelijke amavis beheertool" + +#: extensions/amavis/__init__.py:52 extensions/amavis/general_callbacks.py:17 +msgid "Quarantine" +msgstr "Quarantaine" + +#: extensions/amavis/app_settings.py:11 +msgid "Quarantine settings" +msgstr "Quarantaine instellingen" + +#: extensions/amavis/app_settings.py:14 +msgid "Maximum message age" +msgstr "Maximum bericht bewaartijd" + +#: extensions/amavis/app_settings.py:16 +msgid "Quarantine messages maximum age (in days) before deletion" +msgstr "Maximum bewaartijd quarantaine berichten (in dagen) voordat ze verwijderd worden" + +#: extensions/amavis/app_settings.py:19 +msgid "Messages releasing" +msgstr "Berichten worden vrijgegeven" + +#: extensions/amavis/app_settings.py:22 +msgid "Remove released messages" +msgstr "Verwijderde vrijgegeven berichten" + +#: extensions/amavis/app_settings.py:24 +msgid "Remove messages marked as released while cleaning up the database" +msgstr "Verwijder berichten gemarkeerd als vrijgegeven tijdens het opruimen van de database" + +#: extensions/amavis/app_settings.py:28 +msgid "Amavis connection mode" +msgstr "Modus om met Amavis te verbinden" + +#: extensions/amavis/app_settings.py:31 +msgid "Mode used to access the PDP server" +msgstr "Mode die gebruikt word om met de PDP server te verbinden" + +#: extensions/amavis/app_settings.py:36 +msgid "PDP server address" +msgstr "PDP server adres" + +#: extensions/amavis/app_settings.py:38 +msgid "PDP server address (if inet mode)" +msgstr "PDP server adres (indien inet mode gebruikt word)" + +#: extensions/amavis/app_settings.py:42 +msgid "PDP server port" +msgstr "PDP server poort" + +#: extensions/amavis/app_settings.py:44 +msgid "PDP server port (if inet mode)" +msgstr "PDP server poort (indien inet mode gebruikt word)" + +#: extensions/amavis/app_settings.py:48 +msgid "PDP server socket" +msgstr "PDP server socket" + +#: extensions/amavis/app_settings.py:50 +msgid "Path to the PDP server socket (if unix mode)" +msgstr "Locatie naar de PDP server socket (indien unix socket mode gebruikt word)" + +#: extensions/amavis/app_settings.py:54 +msgid "Check requests interval" +msgstr "Controleer tijdspanne tussen verzoeken" + +#: extensions/amavis/app_settings.py:56 +msgid "Interval between two release requests checks" +msgstr "Periode tussen twee vrijgeef verzoeken controles" + +#: extensions/amavis/app_settings.py:60 +msgid "Allow direct release" +msgstr "Sta toe om direct vrij te geven" + +#: extensions/amavis/app_settings.py:62 +msgid "Allow users to directly release their messages" +msgstr "Sta een gebruiker to om zelf berichten vrij te geven" + +#: extensions/amavis/app_settings.py:66 +msgid "Enable self-service mode" +msgstr "Activeer de 'zelfbedienings' mode" + +#: extensions/amavis/app_settings.py:68 +msgid "Activate the 'self-service' mode" +msgstr "Activeer de 'zelfbedienings' mode" + +#: extensions/amavis/app_settings.py:72 +msgid "Notifications sender" +msgstr "Afzender van notificaties" + +#: extensions/amavis/app_settings.py:74 +msgid "The e-mail address used to send notitications" +msgstr "E-mail adres dat gebruikt word om notificaties te versturen" + +#: extensions/amavis/app_settings.py:91 extensions/webmail/app_settings.py:91 +msgid "Number of displayed emails per page" +msgstr "Aantal weergegeven emails per pagina" + +#: extensions/amavis/app_settings.py:92 +msgid "Set the maximum number of messages displayed in a page" +msgstr "Stelt het maximum aantal weergegeven berichten per pagina in" + +#: extensions/amavis/general_callbacks.py:81 +msgid "pending requests" +msgstr "openstaande verzoeken" + +#: extensions/amavis/general_callbacks.py:124 +msgid "Pending requests" +msgstr "Openstaande verzoeken" + +#: extensions/amavis/general_callbacks.py:134 +msgid "Content filter" +msgstr "Content filter" + +#: extensions/amavis/lib.py:57 +#, python-format +msgid "Connection to amavis failed: %s" +msgstr "Verbinding met amavis mislukt: %s" + +#: extensions/amavis/models.py:93 +msgid "Virus filter" +msgstr "Virus filter" + +#: extensions/amavis/models.py:94 extensions/amavis/models.py:102 +#: extensions/amavis/models.py:110 extensions/sievefilters/views.py:290 +msgid "yes" +msgstr "ja" + +#: extensions/amavis/models.py:95 extensions/amavis/models.py:103 +#: extensions/amavis/models.py:111 extensions/sievefilters/views.py:294 +msgid "no" +msgstr "nee" + +#: extensions/amavis/models.py:96 extensions/amavis/models.py:104 +#: extensions/amavis/models.py:112 +#: extensions/amavis/templates/amavis/domain_content_filter.html:11 +msgid "default" +msgstr "standaard" + +#: extensions/amavis/models.py:98 +msgid "Bypass virus checks or not. Choose 'default' to use global settings." +msgstr "Optie om virusscans over te slaan. Kies 'default' om de systeeminstelling te gebruiken." + +#: extensions/amavis/models.py:101 +msgid "Spam filter" +msgstr "Spam filter" + +#: extensions/amavis/models.py:106 +msgid "Bypass spam checks or not. Choose 'default' to use global settings." +msgstr "Optie om spamfilter over te slaan. Kies 'default' om de systeeminstelling te gebruiken." + +#: extensions/amavis/models.py:109 +msgid "Banned filter" +msgstr "Zwarte lijst" + +#: extensions/amavis/models.py:114 +msgid "Bypass banned checks or not. Choose 'default' to use global settings." +msgstr "Optie om zwarte lijst over te slaan. Kies 'default' om de systeeminstelling te gebruiken." + +#: extensions/amavis/models.py:144 +msgid "Spam marker" +msgstr "Spam indicator" + +#: extensions/amavis/models.py:146 +msgid "" +"Modify spam subject using the specified text. Choose 'default' to use global" +" settings." +msgstr "Wijzig standaard spam voorvoegsel met de opgegeven text. Kies 'default' om de systeeminstelling te gebruiken. Geef niets op als u geen voorvoegsel wilt." + +#: extensions/amavis/sql_listing.py:31 +msgid "Score" +msgstr "Score" + +#: extensions/amavis/sql_listing.py:35 +#: extensions/amavis/templatetags/amavis_tags.py:75 +#: extensions/webmail/forms.py:7 +msgid "To" +msgstr "Naar" + +#: extensions/amavis/sql_listing.py:38 +#: extensions/postfix_autoreply/forms.py:11 extensions/webmail/lib.py:44 +msgid "From" +msgstr "Van" + +#: extensions/amavis/sql_listing.py:42 extensions/sievefilters/forms.py:77 +#: extensions/webmail/forms.py:11 extensions/webmail/lib.py:40 +#: extensions/webmail/lib.py:423 templates/common/email_searchbar.html:6 +msgid "Subject" +msgstr "Onderwerp" + +#: extensions/amavis/views.py:27 +msgid "Empty quarantine" +msgstr "Niets in quarantaine" + +#: extensions/amavis/views.py:186 +msgid "Message deleted" +msgstr "Bericht verwijderd" + +#: extensions/amavis/views.py:207 +#, python-format +msgid "%(count)d message deleted successfully" +msgid_plural "%(count)d messages deleted successfully" +msgstr[0] "%(count)d berichten successvol verwijderd" +msgstr[1] "%(count)d berichten successvol verwijderd" + +#: extensions/amavis/views.py:229 +msgid "Request sent" +msgstr "Verzoek ingediend" + +#: extensions/amavis/views.py:235 +msgid "Message released" +msgstr "Bericht vrijgegeven" + +#: extensions/amavis/views.py:268 +#, python-format +msgid "%(count)d request sent" +msgid_plural "%(count)d requests sent" +msgstr[0] "%(count)d verzoek ingediend" +msgstr[1] "%(count)d verzoeken ingediend" + +#: extensions/amavis/views.py:288 +#, python-format +msgid "%(count)d message released successfully" +msgid_plural "%(count)d messages released successfully" +msgstr[0] "%(count)d berichten successvol vrijgegeven" +msgstr[1] "%(count)d berichten successvol vrijgegeven" + +#: extensions/amavis/management/commands/amnotify.py:57 +msgid "[modoboa] Pending release requests" +msgstr "[modoboa] Openstaand quarantaine vrijgeef verzoek" + +#: extensions/amavis/templates/amavis/domain_content_filter.html:5 +msgid "Thresholds" +msgstr "Drempels" + +#: extensions/amavis/templates/amavis/domain_content_filter.html:7 +msgid "or more is spam" +msgstr "of meer is spam" + +#: extensions/amavis/templates/amavis/domain_content_filter.html:8 +msgid "or more throw spam message away" +msgstr "of meer weiger bericht" + +#: extensions/amavis/templates/amavis/index.html:2 +msgid "Quarantine management" +msgstr "Quarantaine beheer" + +#: extensions/amavis/templates/amavis/main_action_bar.html:3 +msgid "Select" +msgstr "Selecteren" + +#: extensions/amavis/templates/amavis/main_action_bar.html:9 +msgid "Spam" +msgstr "Ongewenst" + +#: extensions/amavis/templates/amavis/main_action_bar.html:10 +msgid "Virus" +msgstr "Virus" + +#: extensions/amavis/templates/amavis/main_action_bar.html:11 +msgid "Bad Header" +msgstr "Ongeldige Header" + +#: extensions/amavis/templates/amavis/main_action_bar.html:12 +msgid "Bad MIME" +msgstr "Verkeerde MIME" + +#: extensions/amavis/templates/amavis/main_action_bar.html:13 +msgid "Banned" +msgstr "Geband" + +#: extensions/amavis/templates/amavis/main_action_bar.html:18 +#: extensions/amavis/templatetags/amavis_tags.py:19 +#: extensions/amavis/templatetags/amavis_tags.py:54 +msgid "Release" +msgstr "Vrijgeven" + +#: extensions/amavis/templates/amavis/main_action_bar.html:23 +#: extensions/amavis/templatetags/amavis_tags.py:25 +#: extensions/amavis/templatetags/amavis_tags.py:61 +#: extensions/webmail/templatetags/webmail_tags.py:32 +msgid "Delete" +msgstr "Verwijderen" + +#: extensions/amavis/templates/amavis/main_action_bar.html:31 +#: extensions/sievefilters/forms.py:64 +msgid "All messages" +msgstr "Alle berichten" + +#: extensions/amavis/templates/amavis/main_action_bar.html:31 +msgid "All" +msgstr "Alles" + +#: extensions/amavis/templates/amavis/main_action_bar.html:32 +msgid "Spam only" +msgstr "Alleen spam" + +#: extensions/amavis/templates/amavis/main_action_bar.html:33 +msgid "Virus only" +msgstr "Alleen virussen" + +#: extensions/amavis/templates/amavis/main_action_bar.html:34 +msgid "Bad Header only" +msgstr "Alleen ongeldige headers" + +#: extensions/amavis/templates/amavis/main_action_bar.html:35 +msgid "Bad MIME only" +msgstr "Alleen ongeldige MIME's" + +#: extensions/amavis/templates/amavis/main_action_bar.html:36 +msgid "Banned only" +msgstr "Alleen geband" + +#: extensions/amavis/templates/amavis/notifications/pending_requests.html:1 +#, python-format +msgid "" +"\n" +"%(counter)s release request is pending for action.\n" +msgid_plural "" +"\n" +"%(counter)s release requests are pending for action.\n" +msgstr[0] "\n%(counter)s vrijgeef verzoek ingediend.\n" +msgstr[1] "\n%(counter)s vrijgeef verzoeken ingediend.\n" + +#: extensions/amavis/templates/amavis/notifications/pending_requests.html:6 +msgid "Sketch:" +msgstr "Voorbeeld:" + +#: extensions/amavis/templates/amavis/notifications/pending_requests.html:8 +msgid "From:" +msgstr "Van:" + +#: extensions/amavis/templates/amavis/notifications/pending_requests.html:9 +msgid "To:" +msgstr "Naar:" + +#: extensions/amavis/templates/amavis/notifications/pending_requests.html:10 +msgid "Date:" +msgstr "Datum:" + +#: extensions/amavis/templates/amavis/notifications/pending_requests.html:11 +msgid "Subject:" +msgstr "Onderwerp:" + +#: extensions/amavis/templates/amavis/notifications/pending_requests.html:12 +msgid "Act on this message:" +msgstr "Reageer op dit bericht:" + +#: extensions/amavis/templates/amavis/notifications/pending_requests.html:14 +#, python-format +msgid "Please visit %(listingurl)s for a full listing." +msgstr "Bezoek %(listingurl)s voor een volledige lijst." + +#: extensions/amavis/templatetags/amavis_tags.py:28 +msgid "View full headers" +msgstr "Volledige headers weergeven" + +#: extensions/limits/__init__.py:25 +msgid "" +"Per administrator resources to limit the number of objects they can create" +msgstr "Administrators limiteren in de hoeveelheid objecten die ze kunnen aanmaken" + +#: extensions/limits/__init__.py:56 +msgid "Limits" +msgstr "Limieten" + +#: extensions/limits/app_settings.py:10 +msgid "Default limits" +msgstr "Standaard limieten" + +#: extensions/limits/app_settings.py:13 extensions/limits/models.py:14 +msgid "Domain admins" +msgstr "Domein administrators" + +#: extensions/limits/app_settings.py:15 +msgid "" +"Maximum number of allowed domain administrators for a new administrator" +msgstr "Maximum aantal toegestane domein administrators voor een nieuwe administrator" + +#: extensions/limits/app_settings.py:21 +msgid "Maximum number of allowed domains for a new administrator" +msgstr "Maximum aantal toegestane domeinen voor een nieuwe administrator" + +#: extensions/limits/app_settings.py:27 +msgid "Maximum number of allowed domain aliases for a new administrator" +msgstr "Maximum aantal domein aliassen voor een nieuwe administrator" + +#: extensions/limits/app_settings.py:33 +msgid "Maximum number of allowed mailboxes for a new administrator" +msgstr "Maximum aantal toegestane mailboxen voor een nieuwe administrator" + +#: extensions/limits/app_settings.py:39 +msgid "Maximum number of allowed aliases for a new administrator" +msgstr "Maximum aantal aliassen voor een nieuwe administrator" + +#: extensions/limits/forms.py:28 extensions/limits/forms.py:35 +msgid "Invalid limit" +msgstr "Ongeldige limiet" + +#: extensions/limits/forms.py:57 +msgid "Your resources are not initialized yet" +msgstr "Uw capaciteiten zijn nog niet geintialiseerd" + +#: extensions/limits/forms.py:61 +msgid "You're not allowed to define unlimited values" +msgstr "U mag geen ongelimiteerde waardes opgeven" + +#: extensions/limits/general_callbacks.py:10 +msgid "Reseller" +msgstr "Verkoper" + +#: extensions/limits/general_callbacks.py:48 +msgid "Resources" +msgstr "Capaciteiten" + +#: extensions/limits/lib.py:19 +#, python-format +msgid "%s: limit reached" +msgstr "%s: limiet bereikt" + +#: extensions/limits/lib.py:29 +msgid "Not enough resources" +msgstr "Niet genoeg capaciteit" + +#: extensions/limits/models.py:15 +msgid "Maximum number of domain administrators this user can create" +msgstr "Maximum aantal domein administrators die deze gebruiker aan kan maken" + +#: extensions/limits/models.py:18 +msgid "Maximum number of domains this user can create" +msgstr "Maximum aantal domeinen die deze gebruiker aan kan maken" + +#: extensions/limits/models.py:21 +msgid "Maximum number of domain aliases this user can create" +msgstr "Maximum aantal domein-aliassen die deze gebruiker kan aanmaken" + +#: extensions/limits/models.py:24 +msgid "Maximum number of mailboxes this user can create" +msgstr "Maximum aantal mailboxen die deze gebruiker kan aanmaken" + +#: extensions/limits/models.py:26 +msgid "Maximum number of mailbox aliases this user can create" +msgstr "Maximum mailbox aliassen die deze gebruiker kan aanmken" + +#: extensions/limits/models.py:136 +msgid "undefined" +msgstr "ongedefinieerd" + +#: extensions/limits/models.py:138 +msgid "unlimited" +msgstr "ongelimiteerd" + +#: extensions/limits/templates/limits/poolusage.html:4 +msgid "Resources usage" +msgstr "Capaciteit gebruik" + +#: extensions/postfix_autoreply/__init__.py:26 +msgid "Auto-reply (vacation) functionality using Postfix" +msgstr "Auto-antwoord (vakantiebericht) functionaliteit dmv. Postfix" + +#: extensions/postfix_autoreply/__init__.py:47 +msgid "Automatic replies" +msgstr "Automatisch antwoorden" + +#: extensions/postfix_autoreply/app_settings.py:14 +msgid "Automatic reply timeout" +msgstr "Automatisch antwoorden timeout" + +#: extensions/postfix_autoreply/app_settings.py:16 +msgid "Timeout in seconds between two auto-replies to the same recipient" +msgstr "Timeout in seconden tussen twee auto-antwoorden naar dezelfde afzender" + +#: extensions/postfix_autoreply/app_settings.py:20 +msgid "Default subject" +msgstr "Standaard onderwerp" + +#: extensions/postfix_autoreply/app_settings.py:21 +msgid "I'm off" +msgstr "Ik ben niet aanwezig" + +#: extensions/postfix_autoreply/app_settings.py:23 +msgid "" +"Default subject used when an auto-reply message is created automatically" +msgstr "Standaard onderwerp gebruikt wanneer een automatisch antwoord automatisch is aangemaakt" + +#: extensions/postfix_autoreply/app_settings.py:28 +msgid "Default content" +msgstr "Standaard inhoud" + +#: extensions/postfix_autoreply/app_settings.py:30 +#, python-format +msgid "" +"I'm currently off. I'll answer as soon as I come back.\n" +"\n" +"Best regards,\n" +"%(name)s\n" +msgstr "Momenteel ben ik niet aanwezig. Ik zal antwoorden zodra ik terug kom.\n\nMet vriendelijke groet,\n%(name)s\n" + +#: extensions/postfix_autoreply/app_settings.py:36 +#, python-format +msgid "" +"Default content used when an auto-reply message is created automatically. " +"The '%(name)s' macro will be replaced by the user's full name." +msgstr "Standaard inhoud gebruikt wanneer een automatisch antwoord bericht automatisch is aangemaakt. De '%(name)s' macro zal vervangen worden door de naam van de gebruiker." + +#: extensions/postfix_autoreply/forms.py:13 +msgid "Activate your auto reply from this date" +msgstr "Activeer uw auto-antwoord vanaf deze datum" + +#: extensions/postfix_autoreply/forms.py:19 +msgid "Until" +msgstr "Tot" + +#: extensions/postfix_autoreply/forms.py:21 +msgid "Activate your auto reply until this date" +msgstr "Activeer uw auto-antwoord tot deze datum" + +#: extensions/postfix_autoreply/forms.py:49 +#: extensions/postfix_autoreply/forms.py:55 +msgid "This date is over" +msgstr "Deze datum is over" + +#: extensions/postfix_autoreply/forms.py:59 +msgid "Must be greater than start date" +msgstr "De datum moet in de toekomst vanaf de startdatum liggen" + +#: extensions/postfix_autoreply/general_callbacks.py:33 +#: extensions/postfix_autoreply/templates/postfix_autoreply/autoreply.html:2 +msgid "Auto-reply message" +msgstr "Auto-antwoord bericht" + +#: extensions/postfix_autoreply/general_callbacks.py:118 +msgid "Enable auto-reply" +msgstr "Activeer auto-antwoord" + +#: extensions/postfix_autoreply/general_callbacks.py:120 +msgid "Enable or disable Postfix auto-reply" +msgstr "Activeer of Deactiveer Postfix auto-antwoord" + +#: extensions/postfix_autoreply/models.py:20 +msgid "subject" +msgstr "onderwerp" + +#: extensions/postfix_autoreply/models.py:21 +msgid "The subject that will appear in sent emails" +msgstr "Het onderwerp dat zal verschijnen in verstuurde mails" + +#: extensions/postfix_autoreply/models.py:24 +msgid "content" +msgstr "inhoud" + +#: extensions/postfix_autoreply/models.py:25 +msgid "The content that will appear in sent emails" +msgstr "De inhoud die zal verschijnen in versuurde mails" + +#: extensions/postfix_autoreply/models.py:29 +msgid "Activate/Deactivate your auto reply" +msgstr "Activeer/Deactiveer uw auto-antwoord" + +#: extensions/postfix_autoreply/views.py:34 +msgid "Auto reply message updated successfully." +msgstr "Auto antwoord bericht successvol aangepast." + +#: extensions/postfix_autoreply/templates/postfix_autoreply/autoreply.html:2 +msgid "Define a message to automatically send when you are off" +msgstr "Maak een bericht dat automatisch verstuurd word wanneer u niet aanwezig bent" + +#: extensions/postfix_relay_domains/__init__.py:72 +msgid "Relay domains support for Postfix" +msgstr "Activeer doorstuur domeinen ondersteuning voor Postfix" + +#: extensions/postfix_relay_domains/__init__.py:89 +#: extensions/postfix_relay_domains/general_callbacks.py:100 +#: extensions/postfix_relay_domains/limits_callbacks.py:16 +msgid "Relay domains" +msgstr "Doorstuur domeinen" + +#: extensions/postfix_relay_domains/app_settings.py:10 +msgid "Postfix's master.cf path" +msgstr "Map waarin Postfix's master.cf staat" + +#: extensions/postfix_relay_domains/app_settings.py:12 +msgid "Path to the master.cf configuration file" +msgstr "Locatie van Postfix's master.cf" + +#: extensions/postfix_relay_domains/forms.py:15 +msgid "" +"Alias(es) of this relay domain. Indicate only one name per input, press " +"ENTER to add a new input." +msgstr "Alias(sen) van dit doorstuur domein. Geef slechts een adres per regel op, en toets ENTER om een nieuwe regel te krijgen." + +#: extensions/postfix_relay_domains/forms.py:51 +#: extensions/postfix_relay_domains/general_callbacks.py:93 +msgid "relay domain alias" +msgstr "doorstuur domein alias" + +#: extensions/postfix_relay_domains/forms.py:69 +#: extensions/postfix_relay_domains/general_callbacks.py:92 +msgid "relay domain" +msgstr "doorstuur domein" + +#: extensions/postfix_relay_domains/general_callbacks.py:35 +msgid "Add relay domain" +msgstr "Doorstuur domein toevoegen" + +#: extensions/postfix_relay_domains/general_callbacks.py:101 +msgid "Maximum number of relay domains this user can create" +msgstr "Maximum aantal doorstuur domeinen die deze gebruiker aan kan maken" + +#: extensions/postfix_relay_domains/general_callbacks.py:103 +#: extensions/postfix_relay_domains/limits_callbacks.py:24 +msgid "Relay domain aliases" +msgstr "Doorstuur domein aliassen" + +#: extensions/postfix_relay_domains/general_callbacks.py:104 +msgid "Maximum number of relay domain aliases this user can create" +msgstr "Maximum aantal van doorstuur domeinen die deze gebruiker aan kan maken" + +#: extensions/postfix_relay_domains/general_callbacks.py:111 +msgid "" +"\n" +"
  • relaydomain; name; target host; service; enabled; verify recipients
  • \n" +"
  • relaydomainalias; name; target; enabled
  • \n" +msgstr "\n
  • relaydomain; name; target host; service; enabled; verify recipients
  • \n
  • relaydomainalias; name; target; enabled
  • \n" + +#: extensions/postfix_relay_domains/limits_callbacks.py:19 +msgid "Maximum number of allowed relay domains for a new administrator" +msgstr "Maximum aantal toegestane doorstuur domeinen voor een nieuwe administrator" + +#: extensions/postfix_relay_domains/limits_callbacks.py:27 +msgid "Maximum number of allowed relay domain aliases for a new administrator" +msgstr "Maximum aantal doorstuur domein aliassen voor een nieuwe administrator" + +#: extensions/postfix_relay_domains/models.py:59 +msgid "The service name" +msgstr "De servicenaam" + +#: extensions/postfix_relay_domains/models.py:80 +msgid "target host" +msgstr "doel host" + +#: extensions/postfix_relay_domains/models.py:81 +msgid "Remote destination of this domain" +msgstr "Externe bestemming van dit domein" + +#: extensions/postfix_relay_domains/models.py:89 +msgid "verify recipients" +msgstr "controleer ontvangers" + +#: extensions/postfix_relay_domains/models.py:90 +msgid "Check for valid recipients" +msgstr "Controleer geldigheid ontvangers" + +#: extensions/postfix_relay_domains/models.py:103 +msgid "Relay Domain" +msgstr "Doorstuur Domein" + +#: extensions/postfix_relay_domains/models.py:164 +msgid "The relay domain this alias points to" +msgstr "Het doorstuur domein verwijst naar" + +#: extensions/postfix_relay_domains/models.py:195 +#, python-format +msgid "Relay domain %s does not exist" +msgstr "Doorstuur domein %s bestaat niet" + +#: extensions/postfix_relay_domains/views.py:26 +msgid "Relay domain created" +msgstr "Doorstuur domein aangemaakt" + +#: extensions/postfix_relay_domains/views.py:31 +msgid "New relay domain" +msgstr "Nieuw doorstuur domein" + +#: extensions/postfix_relay_domains/views.py:54 +msgid "Relay domain modified" +msgstr "Doorstuur domein aangepast" + +#: extensions/postfix_relay_domains/views.py:77 +msgid "Relay domain deleted" +msgstr "Doorstuur domein verwijderd" + +#: extensions/postfix_relay_domains/templates/postfix_relay_domains/service_field.html:2 +msgid "Scan" +msgstr "Scan" + +#: extensions/sievefilters/__init__.py:12 +msgid "Plugin to easily create server-side filters" +msgstr "Plugin om gemakkelijk filters op de server in te stellen (bijvoorbeeld om uw mail te sorteren)" + +#: extensions/sievefilters/__init__.py:18 +msgid "Sieve filters" +msgstr "Sieve filters" + +#: extensions/sievefilters/__init__.py:19 +#: extensions/sievefilters/general_callbacks.py:14 +#: extensions/sievefilters/templates/sievefilters/index.html:2 +msgid "Message filters" +msgstr "Berichtfilters" + +#: extensions/sievefilters/app_settings.py:19 +msgid "ManageSieve settings" +msgstr "ManageSieve instellingen" + +#: extensions/sievefilters/app_settings.py:24 +msgid "Address of your MANAGESIEVE server" +msgstr "Adres van uw MANAGESIEVE server" + +#: extensions/sievefilters/app_settings.py:30 +msgid "Listening port of your MANAGESIEVE server" +msgstr "Poort van uw MANAGESIEVE server" + +#: extensions/sievefilters/app_settings.py:34 +msgid "Connect using STARTTLS" +msgstr "Verbind dmv. STARTTLS" + +#: extensions/sievefilters/app_settings.py:36 +msgid "Use the STARTTLS extension" +msgstr "Gebruik de STARTTLS extensie (aanbevolen)" + +#: extensions/sievefilters/app_settings.py:40 +msgid "Authentication mechanism" +msgstr "Authenticatie mechanisme" + +#: extensions/sievefilters/app_settings.py:43 +msgid "Prefered authentication mechanism" +msgstr "Geprefereerde authenticatie mechanisme" + +#: extensions/sievefilters/app_settings.py:54 +msgid "Editor mode" +msgstr "Editor mode" + +#: extensions/sievefilters/app_settings.py:56 +msgid "Select the mode you want the editor to work in" +msgstr "Selecteer de mode waarin u de editor wilt hebben" + +#: extensions/sievefilters/forms.py:16 +msgid "Check to activate this filters set" +msgstr "Aanvinken om deze filterset te activeren" + +#: extensions/sievefilters/forms.py:62 +msgid "All of the following" +msgstr "Alles van de volgende" + +#: extensions/sievefilters/forms.py:63 +msgid "Any of the following" +msgstr "Elke van het volgende" + +#: extensions/sievefilters/forms.py:70 +msgid "contains" +msgstr "bevat" + +#: extensions/sievefilters/forms.py:71 +msgid "does not contain" +msgstr "bevat niet" + +#: extensions/sievefilters/forms.py:72 +msgid "is" +msgstr "is" + +#: extensions/sievefilters/forms.py:73 +msgid "is not" +msgstr "is niet" + +#: extensions/sievefilters/forms.py:78 +msgid "Sender" +msgstr "Afzender" + +#: extensions/sievefilters/forms.py:79 +msgid "Recipient" +msgstr "Ontvanger" + +#: extensions/sievefilters/forms.py:80 extensions/webmail/forms.py:8 +msgid "Cc" +msgstr "CC" + +#: extensions/sievefilters/forms.py:81 +msgid "Size" +msgstr "Grootte" + +#: extensions/sievefilters/forms.py:82 +msgid "is greater than" +msgstr "is groter dan" + +#: extensions/sievefilters/forms.py:83 +msgid "is less than" +msgstr "is minder dan" + +#: extensions/sievefilters/forms.py:87 +msgid "Move message to" +msgstr "Verplaats bericht naar" + +#: extensions/sievefilters/forms.py:89 +msgid "Redirect message to" +msgstr "Bericht doorsturen naar" + +#: extensions/sievefilters/lib.py:40 +msgid "Connection to MANAGESIEVE server failed, check your configuration" +msgstr "Verbinding naar MANAGESIEVE server mislukt, controleer uw configuratie" + +#: extensions/sievefilters/lib.py:85 +msgid "Not enough space on server" +msgstr "Niet genoeg ruimte beschikbaar op de server" + +#: extensions/sievefilters/views.py:78 +msgid "Failed to retrieve filters set" +msgstr "Mislukt om de filterset te te laden" + +#: extensions/sievefilters/views.py:138 +#: extensions/sievefilters/templatetags/sfilters_tags.py:33 +msgid "New filter" +msgstr "Nieuw filter" + +#: extensions/sievefilters/views.py:146 +msgid "Filter created" +msgstr "Filter aangemaakt" + +#: extensions/sievefilters/views.py:160 +#: extensions/sievefilters/templatetags/sfilters_tags.py:78 +msgid "Edit filter" +msgstr "Filter bewerken" + +#: extensions/sievefilters/views.py:169 +msgid "Filter modified" +msgstr "Filter aangepast" + +#: extensions/sievefilters/views.py:192 +msgid "Filter removed" +msgstr "Filter verwijderd" + +#: extensions/sievefilters/views.py:193 +msgid "Failed to remove filter" +msgstr "Mislukt om het filter te verwijderen" + +#: extensions/sievefilters/views.py:208 +msgid "Filters set saved" +msgstr "Filterset opgeslagen" + +#: extensions/sievefilters/views.py:224 +msgid "Filters set created" +msgstr "Filterset aangemaakt" + +#: extensions/sievefilters/views.py:228 +msgid "Create a new filters set" +msgstr "Maak een nieuwe filterset aan" + +#: extensions/sievefilters/views.py:247 +msgid "Filters set deleted" +msgstr "Filterset verwijderd" + +#: extensions/sievefilters/views.py:260 +msgid "Filters set activated" +msgstr "Filterset geactiveerd" + +#: extensions/sievefilters/templates/sievefilters/filter.html:6 +msgid "Conditions" +msgstr "Voorwaarden" + +#: extensions/sievefilters/templates/sievefilters/guieditor.html:7 +msgid "Active" +msgstr "Actief" + +#: extensions/sievefilters/templates/sievefilters/guieditor.html:24 +msgid "No filter defined yet" +msgstr "Nog geen filter aangemaakt" + +#: extensions/sievefilters/templates/sievefilters/index.html:76 +msgid "Filter sets" +msgstr "Filtersets" + +#: extensions/sievefilters/templates/sievefilters/index.html:80 +msgid "active" +msgstr "actief" + +#: extensions/sievefilters/templates/sievefilters/index.html:94 +msgid "" +"No filters set defined yet. You must create one in order to define filters." +msgstr "Nog geen filterset gedefinieerd. U moet een filterset aanmaken om filters in te kunnen stellen." + +#: extensions/sievefilters/templatetags/sfilters_tags.py:15 +msgid "New filters set" +msgstr "Nieuwe filterset" + +#: extensions/sievefilters/templatetags/sfilters_tags.py:44 +msgid "Save filters set" +msgstr "Filterset opslaan" + +#: extensions/sievefilters/templatetags/sfilters_tags.py:52 +msgid "Activate filters set" +msgstr "Filterset activeren" + +#: extensions/sievefilters/templatetags/sfilters_tags.py:57 +msgid "Remove filters set" +msgstr "Filterset verwijderen" + +#: extensions/sievefilters/templatetags/sfilters_tags.py:62 +msgid "Download" +msgstr "Download" + +#: extensions/sievefilters/templatetags/sfilters_tags.py:86 +msgid "Remove this filter" +msgstr "Dit filter verwijderen" + +#: extensions/sievefilters/templatetags/sfilters_tags.py:94 +msgid "Move this filter up" +msgstr "Dit filter omhoogplaatsen" + +#: extensions/sievefilters/templatetags/sfilters_tags.py:102 +msgid "Move this filter down" +msgstr "Dit filter omlaagplaatsen" + +#: extensions/stats/__init__.py:21 +msgid "Graphical statistics about emails traffic using RRDtool" +msgstr "Grafische statistieken over emailverkeer dmv. RRDtool" + +#: extensions/stats/__init__.py:29 +#: extensions/stats/templates/stats/index.html:2 +msgid "Graphical statistics" +msgstr "Grafische statistieken" + +#: extensions/stats/app_settings.py:14 +msgid "Path to the log file" +msgstr "Locatie van logbestand" + +#: extensions/stats/app_settings.py:16 +msgid "Path to log file used to collect statistics" +msgstr "Locatie van logbestand gebruik om statistieken te maken" + +#: extensions/stats/app_settings.py:20 +msgid "Directory to store RRD files" +msgstr "Map om RRD bestanden op te slaan" + +#: extensions/stats/app_settings.py:22 +msgid "Path to directory where RRD files are stored" +msgstr "Locatie van map waar RRD bestanden staan" + +#: extensions/stats/app_settings.py:26 +msgid "Directory to store PNG files" +msgstr "Map om PNG bestanden op te slaan" + +#: extensions/stats/app_settings.py:28 +msgid "Path to directory where PNG files are stored" +msgstr "Locatie van map waar de PNG bestanden staan" + +#: extensions/stats/general_callbacks.py:12 +msgid "Statistics" +msgstr "Statistieken" + +#: extensions/stats/graph_templates.py:22 +msgid "Average normal traffic" +msgstr "Gemiddeld normaal verkeer" + +#: extensions/stats/graph_templates.py:23 +#: extensions/stats/graph_templates.py:36 +msgid "msgs/min" +msgstr "berichten/min" + +#: extensions/stats/graph_templates.py:28 +msgid "sent messages" +msgstr "verstuurde berichten" + +#: extensions/stats/graph_templates.py:30 +msgid "received messages" +msgstr "ontvangen berichten" + +#: extensions/stats/graph_templates.py:35 +msgid "Average bad traffic" +msgstr "Gemiddeld slecht verkeer" + +#: extensions/stats/graph_templates.py:41 +msgid "bounced messages" +msgstr "geweigerde berichten" + +#: extensions/stats/graph_templates.py:43 +msgid "rejected messages" +msgstr "geweigerde berichten" + +#: extensions/stats/graph_templates.py:48 +msgid "Average normal traffic size" +msgstr "Gemiddeld normaal verkeer grootte" + +#: extensions/stats/graph_templates.py:49 +msgid "bytes/min" +msgstr "bytes/min" + +#: extensions/stats/graph_templates.py:54 +msgid "received size" +msgstr "ontvangen grootte" + +#: extensions/stats/graph_templates.py:56 +msgid "sent size" +msgstr "verstuurde grootte" + +#: extensions/stats/graph_templates.py:78 +msgid "Mail traffic" +msgstr "Mailverkeer" + +#: extensions/stats/grapher.py:8 +msgid "Day" +msgstr "Dag" + +#: extensions/stats/grapher.py:9 +msgid "Week" +msgstr "Week" + +#: extensions/stats/grapher.py:10 +msgid "Month" +msgstr "Maand" + +#: extensions/stats/grapher.py:11 +msgid "Year" +msgstr "Jaar" + +#: extensions/stats/grapher.py:60 +msgid "Total" +msgstr "Totaal" + +#: extensions/stats/views.py:28 extensions/stats/templates/stats/graphs.html:4 +msgid "No statistics available" +msgstr "Geen statistieken beschikbaar" + +#: extensions/stats/views.py:47 +msgid "Unknown graphic set" +msgstr "Onbekende grafische set" + +#: extensions/stats/views.py:70 +msgid "Bad custom period" +msgstr "Ongeldige aangepaste periode" + +#: extensions/stats/templates/stats/index.html:33 +msgid "Search a domain" +msgstr "Zoek een domein" + +#: extensions/stats/templates/stats/index.html:37 +msgid "Predefined time ranges" +msgstr "Voorgedefinieerde tijdspannes" + +#: extensions/stats/templates/stats/index.html:44 +msgid "Custom time range" +msgstr "Aangepaste tijdspanne" + +#: extensions/stats/templates/stats/index.html:57 +msgid "Apply" +msgstr "Toepassen" + +#: extensions/webmail/__init__.py:13 +msgid "Simple IMAP webmail" +msgstr "Simpele IMAP webmail" + +#: extensions/webmail/app_settings.py:14 +msgid "Maximum attachment size" +msgstr "Maximum grootte van bijlage" + +#: extensions/webmail/app_settings.py:16 +msgid "Maximum attachment size in bytes (or KB, MB, GB if specified)" +msgstr "Maximum grootte van een bijlage in bytes (of KB, MB, GB als opgegeven)" + +#: extensions/webmail/app_settings.py:19 +msgid "IMAP settings" +msgstr "IMAP instellingen" + +#: extensions/webmail/app_settings.py:24 +msgid "Address of your IMAP server" +msgstr "Adres van uw IMAP server" + +#: extensions/webmail/app_settings.py:30 +msgid "Use a secured connection to access IMAP server" +msgstr "Gebruik een beveiligde verbinding met de IMAP server (aanbevolen)" + +#: extensions/webmail/app_settings.py:36 +msgid "Listening port of your IMAP server" +msgstr "Poort van uw IMAP server" + +#: extensions/webmail/app_settings.py:39 +msgid "SMTP settings" +msgstr "SMTP instellingen" + +#: extensions/webmail/app_settings.py:44 +msgid "Address of your SMTP server" +msgstr "Adres van uw IMAP server" + +#: extensions/webmail/app_settings.py:48 +msgid "Secured connection mode" +msgstr "Beveiligde verbinding gebruiken" + +#: extensions/webmail/app_settings.py:49 +msgid "None" +msgstr "Geen" + +#: extensions/webmail/app_settings.py:53 +msgid "Use a secured connection to access SMTP server" +msgstr "Gebruik een beveiligde verbinding met de SMTP server (aanbevolen)" + +#: extensions/webmail/app_settings.py:60 +msgid "Listening port of your SMTP server" +msgstr "Poort van de SMTP server" + +#: extensions/webmail/app_settings.py:64 +msgid "Authentication required" +msgstr "Authenticatie vereist" + +#: extensions/webmail/app_settings.py:66 +msgid "Server needs authentication" +msgstr "De server vereist authenticatie" + +#: extensions/webmail/app_settings.py:77 +msgid "Default message display mode" +msgstr "Standaard bericht weergave" + +#: extensions/webmail/app_settings.py:79 +msgid "The default mode used when displaying a message" +msgstr "De standaard mode wanneer een bericht weergegeven wordt" + +#: extensions/webmail/app_settings.py:85 +msgid "Enable HTML links display" +msgstr "Activeer HTML links scherm" + +#: extensions/webmail/app_settings.py:86 +msgid "Enable/Disable HTML links display" +msgstr "Zet het weergeven van HTML links aan of uit" + +#: extensions/webmail/app_settings.py:92 +msgid "Sets the maximum number of messages displayed in a page" +msgstr "Stelt het maximum aantal weergegeven berichten per pagina in" + +#: extensions/webmail/app_settings.py:97 +msgid "Listing refresh rate" +msgstr "Lijst automatisch vernieuwen" + +#: extensions/webmail/app_settings.py:98 +msgid "Automatic folder refresh rate (in seconds)" +msgstr "Map automatisch vernieuwen (in seconden)" + +#: extensions/webmail/app_settings.py:103 +msgid "Mailboxes container's width" +msgstr "Breedte van de mailbox container" + +#: extensions/webmail/app_settings.py:104 +msgid "The width of the mailbox list container" +msgstr "De breedte van de mailbox lijst container" + +#: extensions/webmail/app_settings.py:111 +msgid "Trash folder" +msgstr "Prullenbak" + +#: extensions/webmail/app_settings.py:112 +msgid "Folder where deleted messages go" +msgstr "Map voor verwijderde berichten" + +#: extensions/webmail/app_settings.py:117 +msgid "Sent folder" +msgstr "Verstuurd" + +#: extensions/webmail/app_settings.py:118 +msgid "Folder where copies of sent messages go" +msgstr "Map waar verzonden berichten worden opgeslagen" + +#: extensions/webmail/app_settings.py:123 +msgid "Drafts folder" +msgstr "Concepten" + +#: extensions/webmail/app_settings.py:124 +msgid "Folder where drafts go" +msgstr "Map waar concepten heen gaan" + +#: extensions/webmail/app_settings.py:127 +msgid "Composing messages" +msgstr "Berichten opstellen" + +#: extensions/webmail/app_settings.py:131 +msgid "Default editor" +msgstr "Standaard editor" + +#: extensions/webmail/app_settings.py:133 +msgid "The default editor to use when composing a message" +msgstr "De standaard editor voor het schrijven van een bericht" + +#: extensions/webmail/app_settings.py:139 +msgid "Signature text" +msgstr "Handtekening text" + +#: extensions/webmail/app_settings.py:140 +msgid "User defined email signature" +msgstr "Gebruiker opgegeven email handtekening" + +#: extensions/webmail/app_settings.py:160 +msgid "Value must be a positive integer (> 0)" +msgstr "Waarde moet een positieve integer zijn (>0)" + +#: extensions/webmail/exceptions.py:20 +msgid "Server response" +msgstr "Antwoord van server" + +#: extensions/webmail/exceptions.py:34 +msgid "Unknown action" +msgstr "Onbekende actie" + +#: extensions/webmail/forms.py:9 +msgid "Cci" +msgstr "BCc" + +#: extensions/webmail/general_callbacks.py:14 +msgid "Webmail" +msgstr "Webmail" + +#: extensions/webmail/imaputils.py:242 +msgid "Failed to retrieve hierarchy delimiter" +msgstr "Mislukt om de hiërarchische scheiding te verkrijgen" + +#: extensions/webmail/imaputils.py:283 +#, python-format +msgid "Connection to IMAP server failed: %s" +msgstr "Verbinding met de IMAP server mislukt: %s" + +#: extensions/webmail/lib.py:371 +msgid "wrote:" +msgstr "schreef:" + +#: extensions/webmail/lib.py:437 extensions/webmail/lib.py:441 +msgid "Original message" +msgstr "Oorspronkelijk bericht" + +#: extensions/webmail/views.py:85 +#, python-format +msgid "%(count)d message deleted" +msgid_plural "%(count)d messages deleted" +msgstr[0] "%(count)d bericht verwijderd" +msgstr[1] "%(count)d berichten verwijderd" + +#: extensions/webmail/views.py:116 lib/email_listing.py:138 +msgid "Empty mailbox" +msgstr "Mailbox leegmaken" + +#: extensions/webmail/views.py:142 +msgid "Mailbox created" +msgstr "Mailbox aangemaakt" + +#: extensions/webmail/views.py:148 +#: extensions/webmail/templatetags/webmail_tags.py:175 +msgid "Create a new mailbox" +msgstr "Nieuwe mailbox aanmaken" + +#: extensions/webmail/views.py:167 extensions/webmail/views.py:202 +msgid "Edit mailbox" +msgstr "Wijzing mailbox" + +#: extensions/webmail/views.py:183 +msgid "Mailbox updated" +msgstr "Mailbox aangepast" + +#: extensions/webmail/views.py:255 +msgid "Failed to save attachment: " +msgstr "Mislukt om deze bijlage(s) op te slaan:" + +#: extensions/webmail/views.py:258 +#, python-format +msgid "Attachment is too big (limit: %s)" +msgstr "Bijlage is te groot (maximaal toegestane grootte: %s)" + +#: extensions/webmail/views.py:264 +#: extensions/webmail/templates/webmail/compose.html:15 +#: extensions/webmail/templates/webmail/headers.html:4 +msgid "Attachments" +msgstr "Bijlagen" + +#: extensions/webmail/views.py:281 +msgid "Bad query" +msgstr "Verkeerde query" + +#: extensions/webmail/views.py:293 +msgid "Failed to remove attachment: " +msgstr "Mislukt om bijlage te verwijderen:" + +#: extensions/webmail/views.py:298 +msgid "Unknown attachment" +msgstr "Onbekende bijlage" + +#: extensions/webmail/templates/webmail/attachments.html:6 +msgid "Attach" +msgstr "Bijvoegen" + +#: extensions/webmail/templates/webmail/attachments.html:9 +msgid "Uploading.." +msgstr "Uploaden.." + +#: extensions/webmail/templates/webmail/compose.html:7 +msgid "Add Cc field" +msgstr "Cc veld toevoegen" + +#: extensions/webmail/templates/webmail/compose.html:7 +msgid "Add Cci field" +msgstr "BCc veld toevoegen" + +#: extensions/webmail/templates/webmail/compose.html:18 +#, python-format +msgid "%(counter)s file" +msgid_plural "%(counter)s files" +msgstr[0] "%(counter)s bestand" +msgstr[1] "%(counter)s bestanden" + +#: extensions/webmail/templates/webmail/folder.html:6 +msgid "Parent mailbox" +msgstr "Hoofd mailbox" + +#: extensions/webmail/templates/webmail/index.html:40 +msgid "Mailbox quota" +msgstr "Mailbox limiet" + +#: extensions/webmail/templatetags/webmail_tags.py:20 +msgid "Reply" +msgstr "Antwoorden" + +#: extensions/webmail/templatetags/webmail_tags.py:24 +msgid "Reply all" +msgstr "Allen antwoorden" + +#: extensions/webmail/templatetags/webmail_tags.py:34 +msgid "Display options" +msgstr "Weergave opties" + +#: extensions/webmail/templatetags/webmail_tags.py:37 +msgid "Activate links" +msgstr "Activeer links" + +#: extensions/webmail/templatetags/webmail_tags.py:39 +msgid "Disable links" +msgstr "Links uitschakelen" + +#: extensions/webmail/templatetags/webmail_tags.py:48 +msgid "Close this message" +msgstr "Sluit dit bericht" + +#: extensions/webmail/templatetags/webmail_tags.py:63 +msgid "Back" +msgstr "Terug" + +#: extensions/webmail/templatetags/webmail_tags.py:67 +msgid "Send" +msgstr "Versturen" + +#: extensions/webmail/templatetags/webmail_tags.py:80 +msgid "New message" +msgstr "Nieuw bericht" + +#: extensions/webmail/templatetags/webmail_tags.py:89 +msgid "Mark messages" +msgstr "Markeer berichten" + +#: extensions/webmail/templatetags/webmail_tags.py:93 +msgid "As read" +msgstr "Als gelezen" + +#: extensions/webmail/templatetags/webmail_tags.py:96 +msgid "As unread" +msgstr "Als ongelezen" + +#: extensions/webmail/templatetags/webmail_tags.py:105 +msgid "Compress folder" +msgstr "Comprimeer map" + +#: extensions/webmail/templatetags/webmail_tags.py:114 +msgid "Empty folder" +msgstr "Lege map" + +#: extensions/webmail/templatetags/webmail_tags.py:183 +msgid "Edit the selected mailbox" +msgstr "Bewerk geselecteerde mailbox" + +#: extensions/webmail/templatetags/webmail_tags.py:188 +msgid "Remove the selected mailbox" +msgstr "Verwijder geselecteerde mailbox" + +#: lib/dbutils.py:36 +#, python-format +msgid "Connection to database %s not configured" +msgstr "Verbinding naar database %s niet ingesteld" + +#: lib/exceptions.py:67 +#, python-format +msgid "Permission denied: %s" +msgstr "Toegang geweigerd: %s" + +#: lib/exceptions.py:68 +msgid "Permission denied" +msgstr "Toegang geweigerd" + +#: lib/formutils.py:187 lib/formutils.py:195 +msgid "Enter a valid domain name" +msgstr "Vul een geledige domeinnaam in" + +#: lib/formutils.py:244 +msgid "Yes" +msgstr "Ja" + +#: lib/formutils.py:244 +msgid "No" +msgstr "Nee" + +#: lib/ldaputils.py:111 +#, python-format +msgid "Failed to update password: %s" +msgstr "Mislukt om wachtwoord aan te passen: %s" + +#: lib/permissions.py:20 +msgid "Simple user" +msgstr "Gebruiker" + +#: lib/permissions.py:22 +msgid "Super administrator" +msgstr "Hoofdbeheerder" + +#: lib/permissions.py:28 +msgid "Domain administrator" +msgstr "Domein administrator" + +#: lib/permissions.py:73 +#, python-format +msgid "Failed to grant access (%s)" +msgstr "Mislukt om toegang te verkrijgen (%s)" + +#: lib/tables.py:251 +msgid "No entries to display" +msgstr "Niets om weer te geven" + +#: lib/templatetags/lib_tags.py:59 +msgid "activate" +msgstr "activeren" + +#: templates/404.html:4 templates/500.html:4 +msgid "Sorry" +msgstr "Sorry" + +#: templates/404.html:5 +msgid "The requested page does not exist." +msgstr "De opgegeven pagina bestaat niet." + +#: templates/500.html:5 +msgid "An internal error occured." +msgstr "Er is een interne fout opgetreden." + +#: templates/nlayout.html:62 +msgid "Powered by Modoboa" +msgstr "Mogelijk gemaakt dankzij Modoboa" + +#: templates/common/email_searchbar.html:5 +msgid "From address" +msgstr "Van adres" + +#: templates/common/email_searchbar.html:10 +msgid "Both" +msgstr "Beiden" + +#: templates/common/email_searchbar.html:12 +msgid "Search..." +msgstr "Zoeken..." + +#: templates/common/error.html:4 +msgid "Error:" +msgstr "Foutmelding:" + +#: templates/common/generic_field.html:3 +#: templates/common/generic_fields_group.html:3 +msgid "Help:" +msgstr "Help:" + +#: templates/common/generic_modal.html:11 +#: templates/common/wizard_forms.html:29 +msgid "Close" +msgstr "Sluiten" + +#: templates/registration/login.html:5 +msgid "Welcome to Modoboa" +msgstr "Welkom bij Modoboa" + +#: templates/registration/login.html:34 +msgid "Remember me" +msgstr "Onthoud mij" + +#: templates/registration/login.html:36 +msgid "Log in" +msgstr "Inloggen" diff --git a/modoboa/locale/nl_NL/LC_MESSAGES/djangojs.po b/modoboa/locale/nl_NL/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..ef85e1053 --- /dev/null +++ b/modoboa/locale/nl_NL/LC_MESSAGES/djangojs.po @@ -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 , 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 \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..." diff --git a/modoboa/templates/common/generic_field.html b/modoboa/templates/common/generic_field.html index 05dd55fc8..0f6e6bf36 100644 --- a/modoboa/templates/common/generic_field.html +++ b/modoboa/templates/common/generic_field.html @@ -1,9 +1,9 @@ {% load i18n core_tags lib_tags %} -