diff --git a/modoboa/admin/__init__.py b/modoboa/admin/__init__.py new file mode 100644 index 000000000..4002eb9dd --- /dev/null +++ b/modoboa/admin/__init__.py @@ -0,0 +1 @@ +default_app_config = "modoboa.admin.apps.AdminConfig" diff --git a/modoboa/admin/app_settings.py b/modoboa/admin/app_settings.py new file mode 100644 index 000000000..7a8678e59 --- /dev/null +++ b/modoboa/admin/app_settings.py @@ -0,0 +1,151 @@ +"""Modoboa admin settings and events.""" + +import os + +from django import forms +from django.conf import settings +from django.utils.translation import ugettext_lazy + +from ..lib import events +from modoboa.lib import parameters +from modoboa.lib.form_utils import YesNoField, SeparatorField +from modoboa.lib.sysutils import exec_cmd + + +ADMIN_EVENTS = [ + "DomainCreated", + "DomainModified", + "DomainDeleted", + "DomainOwnershipRemoved", + "GetTagsForDomain", + "ExtraDomainMenuEntries", + "ExtraDomainFilters", + "ExtraDomainQsetFilters", + "ExtraDomainTypes", + "ExtraDomainWizardSteps", + "GetDomainModifyLink", + "CheckDomainName", + + "DomainAliasCreated", + "DomainAliasDeleted", + + "MailboxCreated", + "MailboxDeleted", + "MailboxModified", + + "MailboxAliasCreated", + "MailboxAliasDeleted", + + "ExtraDomainForm", + "FillDomainInstances", + + "ExtraAccountForm", + "CheckExtraAccountForm", + "FillAccountInstances", + + "ExtraDomainImportHelp", + "ImportObject" +] + + +class AdminParametersForm(parameters.AdminParametersForm): + app = "admin" + + mbsep = SeparatorField(label=ugettext_lazy("Mailboxes")) + + handle_mailboxes = YesNoField( + label=ugettext_lazy("Handle mailboxes on filesystem"), + initial="no", + help_text=ugettext_lazy( + "Rename or remove mailboxes on the filesystem when they get" + " renamed or removed within Modoboa" + ) + ) + + mailboxes_owner = forms.CharField( + label=ugettext_lazy("Mailboxes owner"), + initial="vmail", + help_text=ugettext_lazy( + "The UNIX account who owns mailboxes on the filesystem" + ) + ) + + default_domain_quota = forms.IntegerField( + label=ugettext_lazy("Default domain quota"), + initial=0, + help_text=ugettext_lazy( + "Default quota (in MB) applied to freshly created domains with no " + "value specified. A value of 0 means no quota." + ) + ) + + auto_account_removal = YesNoField( + label=ugettext_lazy("Automatic account removal"), + initial="no", + help_text=ugettext_lazy( + "When a mailbox is removed, also remove the associated account") + ) + + auto_create_domain_and_mailbox = YesNoField( + label=ugettext_lazy("Automatic domain/mailbox creation"), + initial="yes", + help_text=ugettext_lazy( + "Create a domain and a mailbox when an account is automatically " + "created." + ) + ) + + # Visibility rules + visibility_rules = { + "mailboxes_owner": "handle_mailboxes=yes", + } + + def __init__(self, *args, **kwargs): + super(AdminParametersForm, self).__init__(*args, **kwargs) + self.field_widths = { + "default_domain_quota": 2 + } + hide_fields = False + dpath = None + code, output = exec_cmd("which dovecot") + if not code: + dpath = output.strip() + else: + known_paths = getattr( + settings, "DOVECOT_LOOKUP_PATH", + ("/usr/sbin/dovecot", "/usr/local/sbin/dovecot") + ) + for fpath in known_paths: + if os.path.isfile(fpath) and os.access(fpath, os.X_OK): + dpath = fpath + if dpath: + try: + code, version = exec_cmd("%s --version" % dpath) + except OSError: + hide_fields = True + else: + if code or not version.strip().startswith("2"): + hide_fields = True + else: + hide_fields = True + if hide_fields: + del self.fields["handle_mailboxes"] + del self.fields["mailboxes_owner"] + + def clean_default_domain_quota(self): + """Ensure quota is a positive integer.""" + if self.cleaned_data['default_domain_quota'] < 0: + raise forms.ValidationError( + ugettext_lazy('Must be a positive integer') + ) + return self.cleaned_data['default_domain_quota'] + + +def load_admin_settings(): + """Load admin settings.""" + from .app_settings import AdminParametersForm + + parameters.register( + AdminParametersForm, ugettext_lazy("Administration")) + events.declare(ADMIN_EVENTS) + from . import callbacks diff --git a/modoboa/admin/apps.py b/modoboa/admin/apps.py new file mode 100644 index 000000000..92a55ff2d --- /dev/null +++ b/modoboa/admin/apps.py @@ -0,0 +1,14 @@ +"""AppConfig for admin.""" + +from django.apps import AppConfig + + +class AdminConfig(AppConfig): + + """App configuration.""" + + name = "modoboa.admin" + verbose_name = "Modoboa admin console" + + def ready(self): + from . import handlers diff --git a/modoboa/admin/callbacks.py b/modoboa/admin/callbacks.py new file mode 100644 index 000000000..094faa01c --- /dev/null +++ b/modoboa/admin/callbacks.py @@ -0,0 +1,256 @@ +"""Event callbacks for modoboa admin.""" + +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext as _, ugettext_lazy + +from modoboa.lib import parameters, events +from modoboa.lib.email_utils import split_mailbox +from modoboa.lib.exceptions import PermDeniedException, BadRequest, Conflict + +from .models import ( + Domain, DomainAlias, Mailbox, Alias +) + + +PERMISSIONS = { + "DomainAdmins": [ + ["core", "user", "add_user"], + ["core", "user", "change_user"], + ["core", "user", "delete_user"], + ["admin", "domain", "view_domains"], + ["admin", "domain", "view_domain"], + ["admin", "mailbox", "add_mailbox"], + ["admin", "mailbox", "change_mailbox"], + ["admin", "mailbox", "delete_mailbox"], + ["admin", "alias", "add_alias"], + ["admin", "alias", "change_alias"], + ["admin", "alias", "delete_alias"], + ["admin", "mailbox", "view_mailboxes"], + ["admin", "alias", "view_aliases"], + ["admin", "domainalias", "view_domaliases"], + ["admin", "domainalias", "add_domainalias"], + ["admin", "domainalias", "change_domainalias"], + ["admin", "domainalias", "delete_domainalias"] + ] +} + + +@events.observe("GetExtraRoles") +def extra_roles(user, account): + """Return new roles.""" + filters = events.raiseQueryEvent( + "UserCanSetRole", user, "DomainAdmins", account + ) + if user.has_perm("admin.add_domain") and \ + (not filters or True in filters): + return [("DomainAdmins", _("Domain administrator"))] + return [] + + +@events.observe("GetExtraRolePermissions") +def extra_permissions(rolename): + """Return extra permissions for :kw:`rolename`.""" + return PERMISSIONS.get(rolename, []) + + +@events.observe("ExtraUprefsRoutes") +def extra_routes(): + from django.conf.urls import url + return [ + url(r'^user/forward/', + 'modoboa.admin.views.user.forward', + name="user_forward"), + ] + + +@events.observe("AdminMenuDisplay") +def admin_menu(target, user): + if target != "top_menu": + return [] + entries = [] + if user.has_perm("admin.view_domains"): + entries += [ + {"name": "domains", + "url": reverse("admin:domain_list"), + "label": _("Domains")} + ] + conditions = ( + user.has_perm("core.add_user"), + user.has_perm("admin.add_alias") + ) + if any(conditions): + entries += [ + {"name": "identities", + "url": reverse("admin:identity_list"), + "label": _("Identities")}, + ] + return entries + + +@events.observe("UserMenuDisplay") +def user_menu(target, user): + if target != "uprefs_menu": + return [] + if not hasattr(user, "mailbox"): + return [] + return [ + {"name": "forward", + "class": "ajaxnav", + "url": "forward/", + "label": ugettext_lazy("Forward")} + ] + + +@events.observe("RoleChanged") +def grant_access_to_all_objects(user, role): + from django.contrib.contenttypes.models import ContentType + from modoboa.lib.permissions import grant_access_to_objects + from modoboa.core.models import User + + if role != "SuperAdmins": + return + grant_access_to_objects( + user, User.objects.all(), + ContentType.objects.get_for_model(User) + ) + grant_access_to_objects( + user, Domain.objects.all(), + ContentType.objects.get_for_model(Domain) + ) + grant_access_to_objects( + user, DomainAlias.objects.all(), + ContentType.objects.get_for_model(DomainAlias) + ) + grant_access_to_objects( + user, Mailbox.objects.all(), + ContentType.objects.get_for_model(Mailbox) + ) + grant_access_to_objects( + user, Alias.objects.all(), + ContentType.objects.get_for_model(Alias) + ) + + +@events.observe("AccountExported") +def export_admin_domains(admin): + result = [admin.mailbox.quota] \ + if hasattr(admin, "mailbox") else [""] + if admin.group != "DomainAdmins": + return result + return result + [dom.name for dom in Domain.objects.get_for_admin(admin)] + + +@events.observe("AccountImported") +def import_account_mailbox(user, account, row): + """Handle extra fields when an account is imported. + + Expected fields: + + email address; quota; [domain; ...] + + :param User user: user importing the account + :param User account: account being imported + :param list rom: list of fields (strings) + """ + account.email = row[0].strip() + if account.email: + account.save() + mailbox, domname = split_mailbox(account.email) + try: + domain = Domain.objects.get(name=domname) + except Domain.DoesNotExist: + raise BadRequest( + _("Account import failed (%s): domain does not exist" + % account.username) + ) + if not user.can_access(domain): + raise PermDeniedException + try: + mb = Mailbox.objects.get(address=mailbox, domain=domain) + except Mailbox.DoesNotExist: + pass + else: + 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) + mb.set_quota( + quota, override_rules=user.has_perm("admin.change_domain") + ) + mb.save(creator=user) + if account.group == "DomainAdmins": + for domname in row[2:]: + try: + dom = Domain.objects.get(name=domname.strip()) + except Domain.DoesNotExist: + continue + dom.add_admin(account) + + +@events.observe("AccountAutoCreated") +def account_auto_created(user): + from modoboa.core.models import User + from modoboa.lib.permissions import grant_access_to_object + from .lib import check_if_domain_exists + + if parameters.get_admin("AUTO_CREATE_DOMAIN_AND_MAILBOX") == "no": + return + localpart, domname = split_mailbox(user.username) + if user.group != 'SimpleUsers' and domname is None: + return + sadmins = User.objects.filter(is_superuser=True) + try: + domain = Domain.objects.get(name=domname) + except Domain.DoesNotExist: + label = check_if_domain_exists( + domname, [(DomainAlias, _('domain alias'))]) + if label is not None: + return + domain = Domain(name=domname, enabled=True, quota=0) + domain.save(creator=sadmins[0]) + for su in sadmins[1:]: + grant_access_to_object(su, domain) + try: + mb = Mailbox.objects.get(domain=domain, address=localpart) + except Mailbox.DoesNotExist: + mb = Mailbox( + address=localpart, domain=domain, user=user, use_domain_quota=True + ) + mb.set_quota(override_rules=True) + mb.save(creator=sadmins[0]) + for su in sadmins[1:]: + grant_access_to_object(su, mb) + + +@events.observe("UserLogin") +def user_logged_in(request, username, password): + from modoboa.lib.cryptutils import encrypt + + if hasattr(request.user, "mailbox"): + request.session["password"] = encrypt(password) + + +@events.observe("AccountDeleted") +def account_deleted(account, byuser, **kwargs): + """'AccountDeleted' listener. + + When an account is deleted, we also need to remove its mailbox (if + any). + """ + if not hasattr(account, "mailbox"): + return + mb = account.mailbox + if not byuser.can_access(mb): + raise PermDeniedException + keep_mb_dir = kwargs.get("keep_mb_dir", True) + mb.delete(keepdir=keep_mb_dir) diff --git a/modoboa/admin/factories.py b/modoboa/admin/factories.py new file mode 100644 index 000000000..29eccfeca --- /dev/null +++ b/modoboa/admin/factories.py @@ -0,0 +1,110 @@ +""" +Fixtures factories. +""" +import factory + +from modoboa.core.factories import PermissionFactory, UserFactory +from . import models + + +class DomainFactory(PermissionFactory): + + """Factory to create Domains.""" + + class Meta: + model = models.Domain + django_get_or_create = ("name", ) + + type = "domain" + quota = 10 + enabled = True + + +class DomainAliasFactory(PermissionFactory): + + """Factory to create DomainAlias objects.""" + + class Meta: + model = models.DomainAlias + django_get_or_create = ("name", ) + + target = factory.SubFactory(DomainFactory) + enabled = True + + +class MailboxFactory(PermissionFactory): + + """A factory to create Mailbox instances.""" + + class Meta: + model = models.Mailbox + django_get_or_create = ("address", "domain") + + domain = factory.SubFactory(DomainFactory) + user = factory.SubFactory(UserFactory) + quota = 10 + enabled = True + + +class AliasRecipientFactory(factory.django.DjangoModelFactory): + + class Meta: + model = models.AliasRecipient + + +class AliasFactory(PermissionFactory): + + class Meta: + model = models.Alias + + enabled = True + + +def populate_database(): + """Create test data. + + 2 domains, 1 domain admin, 2 users + """ + dom = DomainFactory(name="test.com") + admin = UserFactory( + username="admin@test.com", groups=('DomainAdmins', ), + password='{PLAIN}toto' + ) + MailboxFactory(address='admin', domain=dom, user=admin) + account = UserFactory.create( + username="user@test.com", groups=('SimpleUsers',), + ) + MailboxFactory.create(address='user', domain=dom, user=account) + + al = AliasFactory.create( + address='forward@test.com', domain=dom + ) + AliasRecipientFactory.create( + address='user@external.com', alias=al) + + al = AliasFactory.create( + address='alias@test.com', domain=dom + ) + mb = account.mailbox + AliasRecipientFactory.create( + address=mb.full_address, alias=al, r_mailbox=mb) + + al = AliasFactory.create( + address='postmaster@test.com', domain=dom + ) + for address in ['toto@titi.com', 'test@truc.fr']: + AliasRecipientFactory.create(address=address, alias=al) + + dom.add_admin(admin) + + dom2 = DomainFactory.create(name='test2.com', quota=0) + admin = UserFactory.create( + username='admin@test2.com', groups=('DomainAdmins',), + password='{PLAIN}toto' + ) + MailboxFactory.create(address='admin', domain=dom2, user=admin) + u = UserFactory.create( + username='user@test2.com', groups=('SimpleUsers',) + ) + MailboxFactory.create(address='user', domain=dom2, user=u) + dom2.add_admin(admin) diff --git a/modoboa/admin/forms/__init__.py b/modoboa/admin/forms/__init__.py new file mode 100644 index 000000000..dbb5a36ef --- /dev/null +++ b/modoboa/admin/forms/__init__.py @@ -0,0 +1,19 @@ +from .domain import ( + DomainFormGeneral, DomainFormOptions, DomainForm, DomainWizard +) +from .account import ( + AccountFormGeneral, AccountFormMail, AccountPermissionsForm, + AccountForm, AccountWizard +) +from .alias import AliasForm +from .forward import ForwardForm +from .import_ import ImportDataForm, ImportIdentitiesForm +from .export import ExportDataForm, ExportDomainsForm, ExportIdentitiesForm + +__all__ = [ + 'DomainFormGeneral', 'DomainFormOptions', 'DomainForm', + 'AccountFormGeneral', 'AccountFormMail', 'AccountPermissionsForm', + 'AccountForm', 'AliasForm', 'ImportDataForm', 'ImportIdentitiesForm', + 'ExportDataForm', 'ExportDomainsForm', 'ExportIdentitiesForm', + 'ForwardForm', 'DomainWizard', 'AccountWizard' +] diff --git a/modoboa/admin/forms/account.py b/modoboa/admin/forms/account.py new file mode 100644 index 000000000..d14a9a365 --- /dev/null +++ b/modoboa/admin/forms/account.py @@ -0,0 +1,512 @@ +"""Forms related to accounts management.""" + +from collections import OrderedDict + +from django import forms +from django.core.urlresolvers import reverse +from django.http import QueryDict +from django.utils.translation import ugettext as _, ugettext_lazy + +from passwords.fields import PasswordField + +from modoboa.core.models import User +from modoboa.lib import events, parameters +from modoboa.lib.email_utils import split_mailbox +from modoboa.lib.exceptions import PermDeniedException, Conflict, NotFound +from modoboa.lib.form_utils import ( + DomainNameField, DynamicForm, TabForms, WizardForm, WizardStep +) +from modoboa.lib.permissions import get_account_roles +from modoboa.lib.web_utils import render_to_json_response + +from ..models import Domain, Mailbox, Alias + + +class AccountFormGeneral(forms.ModelForm): + + """General account form.""" + + username = forms.CharField( + label=ugettext_lazy("Username"), + help_text=ugettext_lazy( + "The user's name. Must be a valid e-mail address for simple users " + "or administrators with a mailbox." + ) + ) + role = forms.ChoiceField( + label=ugettext_lazy("Role"), + choices=[('', ugettext_lazy("Choose"))], + help_text=ugettext_lazy("What level of permission this user will have") + ) + password1 = PasswordField( + label=ugettext_lazy("Password"), widget=forms.widgets.PasswordInput + ) + password2 = PasswordField( + label=ugettext_lazy("Confirmation"), + widget=forms.widgets.PasswordInput, + help_text=ugettext_lazy( + "Enter the same password as above, for verification." + ) + ) + + class Meta: + model = User + fields = ( + "username", "first_name", "last_name", "role", "is_active", + "master_user" + ) + + def __init__(self, user, *args, **kwargs): + super(AccountFormGeneral, self).__init__(*args, **kwargs) + self.fields = OrderedDict( + (key, self.fields[key]) for key in + ['role', 'username', 'first_name', 'last_name', 'password1', + 'password2', 'master_user', 'is_active'] + ) + self.fields["is_active"].label = _("Enabled") + self.user = user + if user.group == "DomainAdmins": + self.fields["role"] = forms.CharField( + label="", + widget=forms.HiddenInput(attrs={"class": "form-control"}), + required=False + ) + else: + self.fields["role"].choices = [('', ugettext_lazy("Choose"))] + self.fields["role"].choices += \ + get_account_roles(user, kwargs['instance']) \ + if 'instance' in kwargs else get_account_roles(user) + + if not user.is_superuser: + del self.fields["master_user"] + + if "instance" in kwargs: + account = kwargs["instance"] + domain_disabled = ( + hasattr(account, "mailbox") and + not account.mailbox.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 hasattr(self.instance, "mailbox"): + return False + return not self.instance.mailbox.domain.enabled + + def clean_role(self): + if self.user.group == "DomainAdmins": + if self.instance == self.user: + return "DomainAdmins" + return "SimpleUsers" + return self.cleaned_data["role"] + + def clean_username(self): + from django.core.validators import validate_email + if "role" not in self.cleaned_data: + return self.cleaned_data["username"] + if self.cleaned_data["role"] != "SimpleUsers": + return self.cleaned_data["username"] + uname = self.cleaned_data["username"].lower() + validate_email(uname) + return uname + + def clean_master_user(self): + """Check role before allowing this mode.""" + conditions = ( + self.cleaned_data["master_user"], + self.cleaned_data["role"] != "SuperAdmins" + ) + if all(conditions): + raise forms.ValidationError( + _("Only super administrators are allowed for this mode") + ) + return self.cleaned_data["master_user"] + + def clean_password2(self): + password1 = self.cleaned_data.get("password1", "") + password2 = self.cleaned_data["password2"] + if password1 != password2: + raise forms.ValidationError( + _("The two password fields didn't match.")) + return password2 + + def save(self, commit=True): + account = super(AccountFormGeneral, self).save(commit=False) + if self.user == account and not self.cleaned_data["is_active"]: + raise PermDeniedException(_("You can't disable your own account")) + if commit: + if "password1" in self.cleaned_data \ + and self.cleaned_data["password1"] != "": + account.set_password(self.cleaned_data["password1"]) + account.save() + account.set_role(self.cleaned_data["role"]) + return account + + +class AccountFormMail(forms.Form, DynamicForm): + + """Form to handle mail part.""" + + email = forms.EmailField(label=ugettext_lazy("E-mail"), required=False) + quota = forms.IntegerField( + label=ugettext_lazy("Quota"), + required=False, + help_text=_("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)."), + widget=forms.widgets.TextInput(attrs={"class": "form-control"}) + ) + quota_act = forms.BooleanField(required=False) + aliases = forms.EmailField( + label=ugettext_lazy("Alias(es)"), + required=False, + help_text=ugettext_lazy( + "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)." + ) + ) + + def __init__(self, *args, **kwargs): + if "instance" in kwargs: + self.mb = kwargs["instance"] + del kwargs["instance"] + else: + self.mb = None + super(AccountFormMail, self).__init__(*args, **kwargs) + self.field_widths = { + "quota": 3 + } + self.extra_fields = [] + result = events.raiseQueryEvent('ExtraFormFields', 'mailform', self.mb) + for fname, field in result: + self.fields[fname] = field + self.extra_fields.append(fname) + if self.mb is not None: + self.fields["email"].required = True + cpt = 1 + qset = self.mb.aliasrecipient_set.select_related("alias") + for ralias in qset: + if ralias.alias.recipients_count >= 2: + continue + name = "aliases_%d" % cpt + self._create_field(forms.EmailField, name, ralias.alias.address) + cpt += 1 + self.fields["email"].initial = self.mb.full_address + self.fields["quota_act"].initial = self.mb.use_domain_quota + if not self.mb.use_domain_quota and self.mb.quota: + self.fields["quota"].initial = self.mb.quota + else: + self.fields["quota_act"].initial = True + + if len(args) and isinstance(args[0], QueryDict): + self._load_from_qdict(args[0], "aliases", forms.EmailField) + + def clean_email(self): + """Ensure lower case emails""" + return self.cleaned_data["email"].lower() + + def clean(self): + """Custom fields validation. + + Check if quota is >= 0 only when the domain value is not used. + """ + super(AccountFormMail, self).clean() + if not self.cleaned_data["quota_act"] \ + and self.cleaned_data['quota'] is not None: + if self.cleaned_data["quota"] < 0: + self.add_error("quota", _("Must be a positive integer")) + return self.cleaned_data + + def create_mailbox(self, user, account): + """Create a mailbox associated to :kw:`account`.""" + locpart, domname = split_mailbox(self.cleaned_data["email"]) + try: + domain = Domain.objects.get(name=domname) + except Domain.DoesNotExist: + raise NotFound(_("Domain does not exist")) + if not user.can_access(domain): + raise PermDeniedException + try: + Mailbox.objects.get(address=locpart, domain=domain) + except Mailbox.DoesNotExist: + pass + else: + raise Conflict( + _("Mailbox %s already exists" % self.cleaned_data["email"]) + ) + events.raiseEvent("CanCreate", user, "mailboxes") + self.mb = Mailbox(address=locpart, domain=domain, user=account, + use_domain_quota=self.cleaned_data["quota_act"]) + self.mb.set_quota(self.cleaned_data["quota"], + user.has_perm("admin.add_domain")) + self.mb.save(creator=user) + + def update_mailbox(self, user, account): + newaddress = None + if self.cleaned_data["email"] != self.mb.full_address: + newaddress = self.cleaned_data["email"] + elif (account.group == "SimpleUsers" and + account.username != self.mb.full_address): + newaddress = account.username + if newaddress is not None: + self.mb.old_full_address = self.mb.full_address + local_part, domname = split_mailbox(newaddress) + try: + domain = Domain.objects.get(name=domname) + except Domain.DoesNotExist: + raise NotFound(_("Domain does not exist")) + if not user.can_access(domain): + raise PermDeniedException + self.mb.rename(local_part, domain) + + self.mb.use_domain_quota = self.cleaned_data["quota_act"] + override_rules = True \ + if not self.mb.quota or user.has_perm("admin.add_domain") \ + else False + self.mb.set_quota(self.cleaned_data["quota"], override_rules) + self.mb.save() + events.raiseEvent('MailboxModified', self.mb) + + def _update_aliases(self, user, account): + """Update mailbox aliases.""" + aliases = [] + for name, value in self.cleaned_data.iteritems(): + if not name.startswith("aliases"): + continue + if value == "": + continue + aliases.append(value.lower()) + + qset = self.mb.aliasrecipient_set.select_related("alias").filter( + alias__internal=False) + for ralias in qset: + if ralias.alias.address not in aliases: + alias = ralias.alias + ralias.delete() + if alias.recipients_count >= 2: + continue + alias.delete() + else: + aliases.remove(ralias.alias.address) + if not aliases: + return + events.raiseEvent( + "CanCreate", user, "mailbox_aliases", len(aliases) + ) + for alias in aliases: + if self.mb.aliasrecipient_set.select_related("alias").filter( + alias__address=alias).exists(): + continue + local_part, domname = split_mailbox(alias) + al = Alias(address=alias, enabled=account.is_active) + al.domain = Domain.objects.get(name=domname) + al.save() + al.set_recipients([self.mb.full_address]) + al.post_create(user) + + def save(self, user, account): + """Save or update account mailbox.""" + if self.cleaned_data["email"] == "": + return None + + if self.cleaned_data["quota_act"]: + self.cleaned_data["quota"] = None + + if not hasattr(self, "mb") or self.mb is None: + self.create_mailbox(user, account) + else: + self.update_mailbox(user, account) + events.raiseEvent( + 'SaveExtraFormFields', 'mailform', self.mb, self.cleaned_data + ) + + account.email = self.cleaned_data["email"] + account.save() + + self._update_aliases(user, account) + + return self.mb + + +class AccountPermissionsForm(forms.Form, DynamicForm): + domains = DomainNameField( + label=ugettext_lazy("Domain(s)"), + required=False, + help_text=ugettext_lazy("Domain(s) that user administrates") + ) + + def __init__(self, *args, **kwargs): + if "instance" in kwargs: + self.account = kwargs["instance"] + del kwargs["instance"] + + super(AccountPermissionsForm, self).__init__(*args, **kwargs) + + if not hasattr(self, "account") or self.account is None: + return + for pos, dom in enumerate(Domain.objects.get_for_admin(self.account)): + name = "domains_%d" % (pos + 1) + self._create_field(DomainNameField, name, dom.name) + if len(args) and isinstance(args[0], QueryDict): + self._load_from_qdict(args[0], "domains", DomainNameField) + + def save(self): + current_domains = [ + dom.name for dom in Domain.objects.get_for_admin(self.account) + ] + for name, value in self.cleaned_data.items(): + if not name.startswith("domains"): + continue + if value in ["", None]: + continue + if value not in current_domains: + domain = Domain.objects.get(name=value) + domain.add_admin(self.account) + + for domain in Domain.objects.get_for_admin(self.account): + if not filter(lambda name: self.cleaned_data[name] == domain.name, + self.cleaned_data.keys()): + domain.remove_admin(self.account) + + +class AccountForm(TabForms): + + """Account edition form.""" + + def __init__(self, request, *args, **kwargs): + self.user = request.user + self.forms = [ + dict(id="general", title=_("General"), + formtpl="admin/account_general_form.html", + cls=AccountFormGeneral, + new_args=[self.user], mandatory=True), + dict(id="mail", + title=_("Mail"), formtpl="admin/mailform.html", + cls=AccountFormMail), + dict( + id="perms", title=_("Permissions"), + formtpl="admin/permsform.html", + cls=AccountPermissionsForm + ) + ] + cbargs = [self.user] + if "instances" in kwargs: + cbargs += [kwargs["instances"]["general"]] + self.forms += events.raiseQueryEvent("ExtraAccountForm", *cbargs) + + super(AccountForm, self).__init__(request, *args, **kwargs) + + def extra_context(self, context): + account = self.instances["general"] + context.update({ + 'title': account.username, + 'formid': 'accountform', + 'action': reverse("admin:account_change", + args=[account.id]), + }) + + def check_perms(self, account): + if account.is_superuser: + return False + return self.user.has_perm("admin.add_domain") \ + and account.has_perm("core.add_user") + + def _before_is_valid(self, form): + if form["id"] == "general": + return True + + if hasattr(self, "check_%s" % form["id"]): + if not getattr(self, "check_%s" % form["id"])(self.account): + return False + return True + + extra_forms = events.raiseQueryEvent( + "CheckExtraAccountForm", self.account, form) + if False in extra_forms: + return False + return True + + def is_valid(self): + """Two steps validation. + """ + self.instances["general"].oldgroup = self.instances["general"].group + if super(AccountForm, self).is_valid(mandatory_only=True): + self.account = self.forms[0]["instance"].save() + return super(AccountForm, self).is_valid(optional_only=True) + return False + + def save(self): + """Custom save method + + As forms interact with each other, it is simpler to make + custom code to save them. + """ + events.raiseEvent( + "AccountModified", self.instances["general"], self.account + ) + self.forms[1]["instance"].save(self.user, self.account) + if len(self.forms) <= 2: + return + for f in self.forms[2:]: + f["instance"].save() + + def done(self): + return render_to_json_response(_("Account updated")) + + +class AccountWizard(WizardForm): + + """Account creation wizard.""" + + def __init__(self, request): + super(AccountWizard, self).__init__(request) + self.add_step( + WizardStep( + "general", AccountFormGeneral, _("General"), + new_args=[request.user] + ) + ) + self.add_step( + WizardStep( + "mail", AccountFormMail, _("Mail"), + "admin/mailform.html" + ) + ) + + def extra_context(self, context): + context.update({ + 'title': _("New account"), + 'action': reverse("admin:account_add"), + 'formid': 'newaccount_form' + }) + + def done(self): + from modoboa.lib.web_utils import render_to_json_response + + account = self.first_step.form.save() + account.post_create(self.request.user) + mailform = self.steps[1].form + mailform.save(self.request.user, account) + return render_to_json_response(_("Account created")) diff --git a/modoboa/admin/forms/alias.py b/modoboa/admin/forms/alias.py new file mode 100644 index 000000000..55dc77c70 --- /dev/null +++ b/modoboa/admin/forms/alias.py @@ -0,0 +1,100 @@ +"""Forms related to aliases management.""" + +from collections import OrderedDict + +from django import forms +from django.http import QueryDict +from django.utils.translation import ugettext as _, ugettext_lazy + +from modoboa.lib.email_utils import split_mailbox +from modoboa.lib.form_utils import ( + DynamicForm +) + +from ..models import Domain, Alias +from .base import EmailField + + +class AliasForm(forms.ModelForm, DynamicForm): + + """A form to create/modify an alias.""" + + address = EmailField( + label=ugettext_lazy("Email address"), + help_text=ugettext_lazy( + "The alias address. To create a catchall alias, just enter the " + "domain name (@domain.tld)." + ), + widget=forms.TextInput(attrs={"class": "form-control"}) + ) + recipients = EmailField( + label=ugettext_lazy("Recipients"), required=False, + help_text=ugettext_lazy( + "Addresses this alias will point to. Indicate only one address " + "per input, press ENTER to add a new input." + ), + widget=forms.TextInput(attrs={"class": "form-control"}) + ) + + class Meta: + model = Alias + fields = ("address", "enabled", ) + + def __init__(self, user, *args, **kwargs): + self.user = user + super(AliasForm, self).__init__(*args, **kwargs) + self.fields = OrderedDict( + (key, self.fields[key]) for key in + ['address', '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: + alias = kwargs["instance"] + if not alias.domain.enabled: + self.fields["enabled"].widget.attrs["disabled"] = "disabled" + cpt = 1 + for rcpt in alias.aliasrecipient_set.filter(alias__internal=False): + name = "recipients_%d" % cpt + self._create_field(forms.EmailField, name, rcpt.address, 2) + cpt += 1 + + def clean_address(self): + """Check if address points to a local domain.""" + localpart, domname = split_mailbox(self.cleaned_data["address"]) + try: + domain = Domain.objects.get(name=domname) + except Domain.DoesNotExist: + raise forms.ValidationError(_("Domain does not exist")) + if not self.user.can_access(domain): + raise forms.ValidationError( + _("You don't have access to this domain") + ) + return self.cleaned_data["address"].lower() + + def clean(self): + """Check it there is at least one recipient.""" + super(AliasForm, self).clean() + for field, value in self.cleaned_data.items(): + if field.startswith("recipients") and value: + return self.cleaned_data + self.add_error("recipients", _("No recipient defined")) + return self.cleaned_data + + def save(self, commit=True): + """Custom save method.""" + alias = super(AliasForm, self).save(commit=False) + local_part, domname = split_mailbox(self.cleaned_data["address"]) + alias.domain = Domain.objects.get(name=domname) + if commit: + alias.save() + address_list = [ + value for field, value in self.cleaned_data.items() + if field.startswith("recipients") and value + ] + alias.set_recipients(address_list) + return alias diff --git a/modoboa/admin/forms/base.py b/modoboa/admin/forms/base.py new file mode 100644 index 000000000..f562bd4c9 --- /dev/null +++ b/modoboa/admin/forms/base.py @@ -0,0 +1,26 @@ +"""Custom form tools.""" + +import re + +from django.core.validators import EmailValidator +from django.forms.fields import EmailField as OriginalEmailField + + +class CustomEmailValidator(EmailValidator): + + """Allow empty user_part.""" + + user_regex = re.compile( + r"(^$" + r"|^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*\Z" # dot-atom + r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"\Z)', # quoted-string + re.IGNORECASE) + +validate_email = CustomEmailValidator() + + +class EmailField(OriginalEmailField): + + """A field that accepts email addresses.""" + + default_validators = [validate_email] diff --git a/modoboa/admin/forms/domain.py b/modoboa/admin/forms/domain.py new file mode 100644 index 000000000..2b9f6435c --- /dev/null +++ b/modoboa/admin/forms/domain.py @@ -0,0 +1,405 @@ +"""Forms related to domains management.""" + +from django import forms +from django.core.urlresolvers import reverse +from django.http import QueryDict +from django.utils.translation import ugettext as _, ugettext_lazy + +from modoboa.core.models import User +from modoboa.lib import events, parameters +from modoboa.lib.exceptions import ModoboaException, Conflict +from modoboa.lib.form_utils import ( + DomainNameField, YesNoField, WizardForm, WizardStep, DynamicForm, TabForms +) +from modoboa.lib.web_utils import render_to_json_response + +from ..lib import check_if_domain_exists +from ..models import ( + Domain, DomainAlias, Mailbox, Alias, Quota +) + +DOMAIN_TYPES = [ + ("domain", _("Domain")), +] +DOMAIN_TYPES += events.raiseQueryEvent("ExtraDomainTypes") + + +class DomainFormGeneral(forms.ModelForm, DynamicForm): + + """A form to create/edit a domain.""" + + type = forms.ChoiceField( + label=ugettext_lazy("Type"), choices=DOMAIN_TYPES + ) + quota = forms.IntegerField( + label=ugettext_lazy("Quota"), + required=False, + help_text=ugettext_lazy( + "Default quota in MB applied to mailboxes. Leave empty to use the " + "default value." + ) + ) + aliases = DomainNameField( + label=ugettext_lazy("Alias(es)"), + required=False, + help_text=ugettext_lazy( + "Alias(es) of this domain. Indicate only one name per input, " + "press ENTER to add a new input." + ) + ) + + class Meta: + model = Domain + fields = ("name", "type", "quota", "aliases", "enabled") + + def __init__(self, *args, **kwargs): + self.oldname = None + if "instance" in kwargs: + self.oldname = kwargs["instance"].name + super(DomainFormGeneral, self).__init__(*args, **kwargs) + + self.field_widths = { + "quota": 3 + } + + if len(args) and isinstance(args[0], QueryDict): + self._load_from_qdict(args[0], "aliases", DomainNameField) + elif "instance" in kwargs: + d = kwargs["instance"] + for pos, dalias in enumerate(d.domainalias_set.all()): + name = "aliases_%d" % (pos + 1) + self._create_field(forms.CharField, name, dalias.name, 3) + + def clean_quota(self): + if self.cleaned_data['quota'] is None: + return int(parameters.get_admin('DEFAULT_DOMAIN_QUOTA')) + if self.cleaned_data['quota'] < 0: + raise forms.ValidationError(_("Must be a positive integer")) + return self.cleaned_data['quota'] + + def clean(self): + """Custom fields validation. + + We want to prevent duplicate names between domains and domain + aliases. Extensions have the possibility to declare other + objects (see *CheckDomainName* event). + + The validation way is not very smart... + """ + cleaned_data = super(DomainFormGeneral, self).clean() + if self._errors: + return cleaned_data + name = cleaned_data["name"] + label = check_if_domain_exists( + name, [(DomainAlias, _('domain alias'))]) + if label is not None: + self.add_error( + "name", _("A %s with this name already exists") + % unicode(label) + ) + for k in cleaned_data.keys(): + if not k.startswith("aliases"): + continue + if cleaned_data[k] == "": + del cleaned_data[k] + continue + if cleaned_data[k] == name: + self._errors[k] = self.error_class( + [_("A %s with this name already exists") % _("domain")] + ) + del cleaned_data[k] + continue + label = check_if_domain_exists( + cleaned_data[k], [(Domain, _("domain"))]) + if label is not None: + self.add_error( + k, _("A %s with this name already exists") + % unicode(label) + ) + return cleaned_data + + def update_mailbox_quotas(self, domain): + """Update all quota records associated to this domain. + + This method must be called only when a domain gets renamed. As + the primary key used for a quota is an email address, rename a + domain will change all associated email addresses, so it will + change the primary keys used for quotas. The consequence is we + can't issue regular UPDATE queries using the .save() method of + a Quota instance (it will trigger an INSERT as the primary key + has changed). + + So, we use this ugly hack to bypass this behaviour. It is not + perfomant at all as it will generate one query per quota + record to update. + """ + for q in Quota.objects.filter(username__contains="@%s" % self.oldname): + username = q.username.replace( + '@%s' % self.oldname, '@%s' % domain.name) + Quota.objects.create( + username=username, bytes=q.bytes, messages=q.messages) + q.delete() + + def save(self, user, commit=True, domalias_post_create=False): + """Custom save method + + Updating a domain may have consequences on other objects + (domain alias, mailbox, quota). The most tricky part concerns + quotas update. + + """ + d = super(DomainFormGeneral, self).save(commit=False) + if commit: + old_mail_homes = None + if self.oldname is not None and d.name != self.oldname: + d.name = self.oldname + old_mail_homes = \ + dict((mb.id, mb.mail_home) for mb in d.mailbox_set.all()) + d.name = self.cleaned_data['name'] + d.save() + Mailbox.objects.filter(domain=d, use_domain_quota=True) \ + .update(quota=d.quota) + aliases = [] + for k, v in self.cleaned_data.iteritems(): + if not k.startswith("aliases"): + continue + if v in ["", None]: + continue + aliases.append(v) + for dalias in d.domainalias_set.all(): + if dalias.name not in aliases: + dalias.delete() + else: + aliases.remove(dalias.name) + if aliases: + events.raiseEvent( + "CanCreate", user, "domain_aliases", len(aliases)) + for alias in aliases: + try: + d.domainalias_set.get(name=alias) + except DomainAlias.DoesNotExist: + pass + else: + continue + al = DomainAlias(name=alias, target=d, enabled=d.enabled) + al.save(creator=user) if domalias_post_create else al.save() + + if old_mail_homes is not None: + self.update_mailbox_quotas(d) + for mb in d.mailbox_set.all(): + mb.rename_dir(old_mail_homes[mb.id]) + + return d + + +class DomainFormOptions(forms.Form): + + """A form containing options for domain creation.""" + + create_dom_admin = YesNoField( + label=ugettext_lazy("Create a domain administrator"), + initial="no", + help_text=ugettext_lazy( + "Automatically create an administrator for this domain" + ) + ) + + dom_admin_username = forms.CharField( + label=ugettext_lazy("Name"), + initial="admin", + help_text=ugettext_lazy( + "The administrator's name. Don't include the domain's name here, " + "it will be automatically appended." + ), + required=False + ) + + create_aliases = YesNoField( + label=ugettext_lazy("Create aliases"), + initial="yes", + help_text=ugettext_lazy( + "Automatically create standard aliases for this domain" + ), + required=False + ) + + def __init__(self, user, *args, **kwargs): + super(DomainFormOptions, self).__init__(*args, **kwargs) + result = events.raiseQueryEvent('UserCanSetRole', user, 'DomainAdmins') + if False in result: + self.fields = {} + return + if args: + if args[0].get("create_dom_admin", "no") == "yes": + self.fields["dom_admin_username"].required = True + self.fields["create_aliases"].required = True + + def clean_dom_admin_username(self): + """Ensure admin username is an email address.""" + if '@' in self.cleaned_data["dom_admin_username"]: + raise forms.ValidationError(_("Invalid format")) + return self.cleaned_data["dom_admin_username"] + + def save(self, *args, **kwargs): + if not self.fields: + return + if self.cleaned_data["create_dom_admin"] == "no": + return + user = kwargs.pop("user") + domain = kwargs.pop("domain") + username = "%s@%s" % ( + self.cleaned_data["dom_admin_username"], domain.name) + try: + da = User.objects.get(username=username) + except User.DoesNotExist: + pass + else: + raise Conflict(_("User '%s' already exists" % username)) + events.raiseEvent("CanCreate", user, "mailboxes") + da = User(username=username, email=username, is_active=True) + da.set_password("password") + da.save() + da.set_role("DomainAdmins") + da.post_create(user) + + mb = Mailbox( + address=self.cleaned_data["dom_admin_username"], domain=domain, + user=da, use_domain_quota=True + ) + mb.set_quota( + override_rules=user.has_perm("admin.change_domain")) + mb.save(creator=user) + + if domain.type == "domain" and \ + self.cleaned_data["create_aliases"] == "yes": + events.raiseEvent("CanCreate", user, "mailbox_aliases") + alias = Alias( + address="postmaster@{}".format(domain.name), + domain=domain, enabled=True + ) + alias.save() + alias.set_recipients([mb.full_address]) + alias.post_create(user) + + domain.add_admin(da) + + +class DomainForm(TabForms): + + """Domain edition form.""" + + template_name = "admin/editdomainform.html" + + def __init__(self, request, *args, **kwargs): + self.user = request.user + self.forms = [] + self.domain = None + if self.user.has_perm("admin.change_domain"): + self.forms.append({ + "id": "general", + "title": _("General"), + "formtpl": "admin/domain_general_form.html", + "cls": DomainFormGeneral, + "mandatory": True + }) + + cbargs = [self.user] + if "instances" in kwargs: + self.domain = kwargs["instances"]["general"] + cbargs += [self.domain] + self.forms += events.raiseQueryEvent("ExtraDomainForm", *cbargs) + if not self.forms: + self.active_id = "admins" + super(DomainForm, self).__init__(request, *args, **kwargs) + + def extra_context(self, context): + domadmins = [u for u in self.domain.admins + if self.request.user.can_access(u) and not u.is_superuser] + if not self.request.user.is_superuser: + domadmins = [u for u in domadmins if u.group == "DomainAdmins"] + context.update({ + "title": self.domain.name, + "action": reverse( + "admin:domain_change", args=[self.domain.pk]), + "formid": "domform", + "domain": self.domain, + "domadmins": domadmins + }) + + def is_valid(self): + """Custom validation. + + We just save the current name before it is potentially + modified. + + """ + if "general" in self.instances: + self.instances["general"].oldname = self.instances["general"].name + return super(DomainForm, self).is_valid() + + def save(self): + """Custom save method. + + As forms interact with each other, it is easier to make custom + code to save them. + """ + if not self.forms: + return + first_form = self.forms[0]["instance"] + options = {} + if isinstance(first_form, DomainFormGeneral): + options["domalias_post_create"] = True + first_form.save(self.request.user, **options) + for f in self.forms[1:]: + f["instance"].save(self.request.user) + + def done(self): + if "general" in self.instances: + events.raiseEvent("DomainModified", self.instances["general"]) + return render_to_json_response(_("Domain modified")) + + +class DomainWizard(WizardForm): + + """Domain creation wizard.""" + + def __init__(self, request): + super(DomainWizard, self).__init__(request) + self.add_step( + WizardStep( + "general", DomainFormGeneral, _("General"), + "admin/domain_general_form.html" + ) + ) + steps = events.raiseQueryEvent("ExtraDomainWizardSteps") + for step in steps: + self.add_step(step) + self.add_step( + WizardStep( + "options", DomainFormOptions, _("Options"), + "admin/domain_options_form.html", + [self.request.user] + ) + ) + + def extra_context(self, context): + context.update({ + "title": _("New domain"), + "action": reverse("admin:domain_add"), + "formid": "domform" + }) + + def done(self): + genform = self.first_step.form + domain = genform.save(self.request.user) + domain.post_create(self.request.user) + for step in self.steps[1:]: + if not step.check_access(self): + continue + try: + step.form.save(user=self.request.user, domain=domain) + except ModoboaException: + from django.db import transaction + transaction.rollback() + raise + return render_to_json_response(_("Domain created")) diff --git a/modoboa/admin/forms/export.py b/modoboa/admin/forms/export.py new file mode 100644 index 000000000..f1f8a868b --- /dev/null +++ b/modoboa/admin/forms/export.py @@ -0,0 +1,45 @@ +from django import forms +from django.utils.translation import ugettext_lazy + + +class ExportDataForm(forms.Form): + filename = forms.CharField( + label=ugettext_lazy("File name"), + max_length=100, + required=False, + widget=forms.TextInput(attrs={"class": "form-control"}) + ) + sepchar = forms.CharField( + label=ugettext_lazy("Separator"), + max_length=1, + required=False, + widget=forms.TextInput(attrs={"class": "form-control"}) + ) + + def __init__(self, *args, **kwargs): + super(ExportDataForm, self).__init__(*args, **kwargs) + self.fields["sepchar"].widget.attrs = {"class": "col-md-1 form-control"} + + def clean_sepchar(self): + if self.cleaned_data["sepchar"] == "": + return ";" + return str(self.cleaned_data["sepchar"]) + + def clean_filename(self): + if self.cleaned_data["filename"] == "": + return self.fields["filename"].initial + return str(self.cleaned_data["filename"]) + + +class ExportDomainsForm(ExportDataForm): + + def __init__(self, *args, **kwargs): + super(ExportDomainsForm, self).__init__(*args, **kwargs) + self.fields["filename"].initial = "modoboa-domains.csv" + + +class ExportIdentitiesForm(ExportDataForm): + + def __init__(self, *args, **kwargs): + super(ExportIdentitiesForm, self).__init__(*args, **kwargs) + self.fields["filename"].initial = "modoboa-identities.csv" diff --git a/modoboa/admin/forms/forward.py b/modoboa/admin/forms/forward.py new file mode 100644 index 000000000..5e2db8509 --- /dev/null +++ b/modoboa/admin/forms/forward.py @@ -0,0 +1,46 @@ +"""Forms related to forwards management.""" + +from django import forms +from django.utils.translation import ugettext as _, ugettext_lazy + +from modoboa.lib.email_utils import split_mailbox +from modoboa.lib.exceptions import BadRequest, PermDeniedException + +from ..models import Domain + + +class ForwardForm(forms.Form): + dest = forms.CharField( + label=ugettext_lazy("Recipient(s)"), + widget=forms.Textarea(attrs={"class": "form-control"}), + required=False, + help_text=ugettext_lazy( + "Indicate one or more recipients separated by a ','") + ) + keepcopies = forms.BooleanField( + label=ugettext_lazy("Keep local copies"), + required=False, + help_text=ugettext_lazy( + "Forward messages and store copies into your local mailbox") + ) + + def get_recipients(self): + recipients = [] + rawdata = self.cleaned_data["dest"].strip() + if not rawdata: + return + for rcpt in rawdata.split(","): + local_part, domname = split_mailbox(rcpt) + if not local_part or not domname: + raise BadRequest("Invalid mailbox syntax for %s" % rcpt) + try: + Domain.objects.get(name=domname) + except Domain.DoesNotExist: + recipients += [rcpt] + else: + raise PermDeniedException( + _("You can't define a forward to a local destination. " + "Please ask your administrator to create an alias " + "instead.") + ) + return recipients diff --git a/modoboa/admin/forms/import_.py b/modoboa/admin/forms/import_.py new file mode 100644 index 000000000..0091e4ba8 --- /dev/null +++ b/modoboa/admin/forms/import_.py @@ -0,0 +1,43 @@ +"""Forms related to import operations.""" + +from django import forms +from django.utils.translation import ugettext_lazy + + +class ImportDataForm(forms.Form): + + """Base form to import objects.""" + + sourcefile = forms.FileField(label=ugettext_lazy("Select a file")) + sepchar = forms.CharField( + label=ugettext_lazy("Separator"), + max_length=1, + required=False, + widget=forms.TextInput(attrs={"class": "form-control"}) + ) + continue_if_exists = forms.BooleanField( + label=ugettext_lazy("Continue on error"), required=False, + help_text=ugettext_lazy("Don't treat duplicated objects as error") + ) + + def __init__(self, *args, **kwargs): + super(ImportDataForm, self).__init__(*args, **kwargs) + self.fields["sepchar"].widget.attrs = {"class": "col-md-1 form-control"} + + def clean_sepchar(self): + if self.cleaned_data["sepchar"] == "": + return ";" + return str(self.cleaned_data["sepchar"]) + + +class ImportIdentitiesForm(ImportDataForm): + + """A form to import identities.""" + + crypt_password = forms.BooleanField( + label=ugettext_lazy("Crypt passwords"), required=False, + help_text=ugettext_lazy( + "Check this option if passwords contained in your file " + "are not crypted" + ) + ) diff --git a/modoboa/admin/handlers.py b/modoboa/admin/handlers.py new file mode 100644 index 000000000..f5cedfdad --- /dev/null +++ b/modoboa/admin/handlers.py @@ -0,0 +1,76 @@ +"""Django signal handlers for admin.""" + +from django.db.models import signals +from django.dispatch import receiver + +from . import models + + +@receiver(signals.post_save, sender=models.DomainAlias) +def create_alias_for_domainalias(sender, instance, **kwargs): + """Create a dedicated alias for domain alias.""" + if not kwargs.get("created"): + return + alias = models.Alias.objects.create( + address="@{}".format(instance.name), enabled=True, internal=True) + models.AliasRecipient.objects.create( + address="@{}".format(instance.target.name), alias=alias) + + +@receiver(signals.post_delete, sender=models.DomainAlias) +def remove_alias_for_domainalias(sender, instance, **kwargs): + """Remove the alias associated to domain alias.""" + models.Alias.objects.filter( + address="@{}".format(instance.name)).delete() + + +@receiver(signals.post_save, sender=models.Mailbox) +def manage_alias_for_mailbox(sender, instance, **kwargs): + """Create or update a "self alias" for mailbox (catchall).""" + if kwargs.get("created"): + alias, created = models.Alias.objects.get_or_create( + address=instance.full_address, domain=instance.domain, + internal=True) + models.AliasRecipient.objects.create( + address=instance.full_address, alias=alias, r_mailbox=instance) + return + old_address = getattr(instance, "old_full_address", None) + if old_address is None or old_address == instance.full_address: + return + alr = models.AliasRecipient.objects.select_related("alias").get( + alias__address=old_address, address=old_address, + r_mailbox=instance, alias__internal=True) + alr.address = instance.full_address + alr.save() + alr.alias.address = instance.full_address + alr.alias.save() + + +@receiver(signals.pre_delete, sender=models.Mailbox) +def mailbox_deleted_handler(sender, **kwargs): + """``Mailbox`` pre_delete signal receiver + + In order to properly handle deletions (ie. we don't want to leave + orphan records into the db), we define this custom receiver. + + It manually removes the mailbox from the aliases it is linked to + and then remove all empty aliases. + """ + from modoboa.lib import events + from modoboa.lib.permissions import ungrant_access_to_object + + mb = kwargs['instance'] + events.raiseEvent("MailboxDeleted", mb) + ungrant_access_to_object(mb) + for ralias in mb.aliasrecipient_set.select_related("alias"): + alias = ralias.alias + ralias.delete() + if not alias.aliasrecipient_set.exists(): + alias.delete() + + +@receiver(signals.post_delete, sender=models.Mailbox) +def remove_alias_for_mailbox(sender, instance, **kwargs): + """Remove "self alias" for this mailbox.""" + models.Alias.objects.filter( + address=instance.full_address).delete() diff --git a/modoboa/admin/lib.py b/modoboa/admin/lib.py new file mode 100644 index 000000000..f5da1820e --- /dev/null +++ b/modoboa/admin/lib.py @@ -0,0 +1,155 @@ +# coding: utf-8 + +"""Internal library for admin.""" + +from functools import wraps +from itertools import chain + +from django.contrib.contenttypes.models import ContentType +from django.db.models import Q +from django.utils.translation import ugettext as _ + +from modoboa.core.models import User +from modoboa.lib import events +from modoboa.lib.exceptions import PermDeniedException + +from .models import Domain, DomainAlias, Alias + + +def needs_mailbox(): + """Check if the current user owns at least one mailbox + + Some applications (the webmail for example) need a mailbox to + work. + """ + def decorator(f): + @wraps(f) + def wrapped_f(request, *args, **kwargs): + if hasattr(request.user, "mailbox"): + return f(request, *args, **kwargs) + raise PermDeniedException(_("A mailbox is required")) + return wrapped_f + return decorator + + +def get_identities(user, searchquery=None, idtfilter=None, grpfilter=None): + """Return all the identities owned by a user. + + :param user: the desired user + :param str searchquery: search pattern + :param list idtfilter: identity type filters + :param list grpfilter: group names filters + :return: a queryset + """ + accounts = [] + if idtfilter is None or not idtfilter or idtfilter == "account": + ids = user.objectaccess_set \ + .filter(content_type=ContentType.objects.get_for_model(user)) \ + .values_list('object_id', flat=True) + q = Q(pk__in=ids) + if searchquery is not None: + q &= Q(username__icontains=searchquery) \ + | Q(email__icontains=searchquery) + if grpfilter is not None and grpfilter: + if grpfilter == "SuperAdmins": + q &= Q(is_superuser=True) + else: + q &= Q(groups__name=grpfilter) + accounts = User.objects.select_related().filter(q) + + aliases = [] + if idtfilter is None or not idtfilter \ + or (idtfilter in ["alias", "forward", "dlist"]): + alct = ContentType.objects.get_for_model(Alias) + ids = user.objectaccess_set.filter(content_type=alct) \ + .values_list('object_id', flat=True) + q = Q(pk__in=ids) + if searchquery is not None: + q &= ( + Q(address__icontains=searchquery) | + Q(domain__name__icontains=searchquery) + ) + aliases = Alias.objects.select_related().filter(q) + if idtfilter is not None and idtfilter: + aliases = [al for al in aliases if al.type == idtfilter] + return chain(accounts, aliases) + + +def get_domains(user, domfilter=None, searchquery=None, **extrafilters): + """Return all the domains the user can access. + + :param ``User`` user: user object + :param str searchquery: filter + :rtype: list + :return: a list of domains and/or relay domains + """ + domains = Domain.objects.get_for_admin(user) + if domfilter: + domains = domains.filter(type=domfilter) + if searchquery is not None: + q = Q(name__contains=searchquery) + q |= Q(domainalias__name__contains=searchquery) + domains = domains.filter(q).distinct() + qset_filters = events.raiseDictEvent( + "ExtraDomainQsetFilters", domfilter, extrafilters + ) + if qset_filters: + domains = domains.filter(**qset_filters) + return domains + + +def check_if_domain_exists(name, extra_checks=None): + """Check if a domain already exists. + + We not only look for domains, we also look for every object that + could conflict with a domain (domain alias, etc.) + + """ + dtypes = events.raiseQueryEvent('CheckDomainName') + if extra_checks is not None: + dtypes = extra_checks + dtypes + for dtype, label in dtypes: + if dtype.objects.filter(name=name).exists(): + return label + return None + + +def import_domain(user, row, formopts): + """Specific code for domains import""" + if not user.has_perm("admin.add_domain"): + raise PermDeniedException(_("You are not allowed to import domains")) + dom = Domain() + dom.from_csv(user, row) + + +def import_domainalias(user, row, formopts): + """Specific code for domain aliases import""" + if not user.has_perm("admin.add_domainalias"): + raise PermDeniedException( + _("You are not allowed to import domain aliases.")) + domalias = DomainAlias() + domalias.from_csv(user, row) + + +def import_account(user, row, formopts): + """Specific code for accounts import""" + account = User() + account.from_csv(user, row, formopts["crypt_password"]) + + +def _import_alias(user, row, **kwargs): + """Specific code for aliases import""" + alias = Alias() + alias.from_csv(user, row, **kwargs) + + +def import_alias(user, row, formopts): + _import_alias(user, row, expected_elements=4) + + +def import_forward(user, row, formopts): + _import_alias(user, row, expected_elements=4) + + +def import_dlist(user, row, formopts): + _import_alias(user, row) diff --git a/modoboa/admin/locale/cs_CZ/LC_MESSAGES/django.po b/modoboa/admin/locale/cs_CZ/LC_MESSAGES/django.po new file mode 100644 index 000000000..2c210b51e --- /dev/null +++ b/modoboa/admin/locale/cs_CZ/LC_MESSAGES/django.po @@ -0,0 +1,890 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Miroslav Abrahám , 2013-2015 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: 2015-01-06 10:05+0000\n" +"Last-Translator: Miroslav Abrahám \n" +"Language-Team: Czech (Czech Republic) (http://www.transifex.com/projects/p/" +"modoboa/language/cs_CZ/)\n" +"Language: cs_CZ\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" + +#: admin/app_settings.py:54 admin/templates/admin/editdomainform.html:25 +msgid "Mailboxes" +msgstr "Schránky" + +#: admin/app_settings.py:57 +msgid "Handle mailboxes on filesystem" +msgstr "Spravovat schránky v souborovém systému" + +#: admin/app_settings.py:60 +msgid "" +"Rename or remove mailboxes on the filesystem when they get renamed or " +"removed within Modoboa" +msgstr "" +"Přejmenovat nebo odstranit schránky v souborovém systému, pokud jsou " +"přejmenovány nebo odstraněny v Modoboa" + +#: admin/app_settings.py:66 +msgid "Mailboxes owner" +msgstr "Majitel schránky" + +#: admin/app_settings.py:69 +msgid "The UNIX account who owns mailboxes on the filesystem" +msgstr "Účet UNIXu, který vlastní schránky v souborovém systému" + +#: admin/app_settings.py:74 +msgid "Default domain quota" +msgstr "Výchozí kvóta domény" + +#: admin/app_settings.py:77 +msgid "" +"Default quota (in MB) applied to freshly created domains with no value " +"specified. A value of 0 means no quota." +msgstr "" +"Výchozí kvóta (v MB), které se použije u nově vytvořených domén bez uvedené " +"hodnoty kvóty. Hodnota 0 znamená, že se kvóta nepoužívá." + +#: admin/app_settings.py:83 +msgid "Automatic account removal" +msgstr "Automatické odstranění účtu" + +#: admin/app_settings.py:86 +msgid "When a mailbox is removed, also remove the associated account" +msgstr "Když je schránka odstraněná, odstranit také přiřazený účet uživatele" + +#: admin/app_settings.py:90 +msgid "Automatic domain/mailbox creation" +msgstr "Automatické vytvoření domény/schránky" + +#: admin/app_settings.py:93 +msgid "Create a domain and a mailbox when an account is automatically created." +msgstr "Při založení účtu automaticky vytvořit doménu a schránku." + +#: admin/app_settings.py:139 admin/forms/account.py:239 +#: admin/forms/domain.py:77 +msgid "Must be a positive integer" +msgstr "Musí se jednat o kladné číslo" + +#: admin/app_settings.py:149 +msgid "Administration" +msgstr "Správa" + +#: admin/callbacks.py:46 +#, fuzzy +#| msgid "Create a domain administrator" +msgid "Domain administrator" +msgstr "Vytvořit správce domény" + +#: admin/callbacks.py:75 admin/templates/admin/domain_statistics.html:30 +#: admin/templates/admin/domains.html:3 +msgid "Domains" +msgstr "Domény" + +#: admin/callbacks.py:85 admin/templates/admin/domain_statistics.html:36 +#: admin/templates/admin/domain_statistics.html:50 +#: admin/templates/admin/identities.html:5 +msgid "Identities" +msgstr "Identity" + +#: admin/callbacks.py:100 admin/templates/admin/forward.html:2 +msgid "Forward" +msgstr "Přeposlat" + +#: admin/callbacks.py:163 +#, python-format +msgid "Account import failed (%s): domain does not exist" +msgstr "Import účtu nebyl úspěšný (%s): doména neexistuje" + +#: admin/callbacks.py:173 admin/forms/account.py:257 +#, python-format +msgid "Mailbox %s already exists" +msgstr "Schránka %s již existuje" + +#: admin/callbacks.py:181 +#, python-format +msgid "Account import failed (%s): wrong quota value" +msgstr "Import účtu selhal (%s): chybná hodnota kvóty" + +#: admin/callbacks.py:216 admin/forms/domain.py:94 +msgid "domain alias" +msgstr "doménový alias" + +#: admin/forms/account.py:30 admin/templates/admin/editdomainform.html:13 +msgid "Username" +msgstr "Uživatelské jméno" + +#: admin/forms/account.py:32 +msgid "" +"The user's name. Must be a valid e-mail address for simple users or " +"administrators with a mailbox." +msgstr "" +"Název uživatele. U běžných uživatelů musí být uvedena platná e-mailová " +"adresa nebo u administrátorů schránka." + +#: admin/forms/account.py:37 +msgid "Role" +msgstr "Role" + +#: admin/forms/account.py:38 admin/forms/account.py:75 +msgid "Choose" +msgstr "Vybrat" + +#: admin/forms/account.py:39 +msgid "What level of permission this user will have" +msgstr "Jaká úroveň oprávnění bude tomuto uživateli nastavena" + +#: admin/forms/account.py:42 +msgid "Password" +msgstr "Heslo" + +#: admin/forms/account.py:45 +msgid "Confirmation" +msgstr "Potvrzení" + +#: admin/forms/account.py:48 +msgid "Enter the same password as above, for verification." +msgstr "Vložte stejné heslo jako v poli výše, pro jeho ověření." + +#: admin/forms/account.py:66 +msgid "Enabled" +msgstr "Povoleno" + +#: admin/forms/account.py:141 +msgid "Only super administrators are allowed for this mode" +msgstr "" + +#: admin/forms/account.py:150 +msgid "The two password fields didn't match." +msgstr "Obě pole s heslem se neshodují." + +#: admin/forms/account.py:156 +msgid "You can't disable your own account" +msgstr "Nemůžete zakázat váš vlastní účet" + +#: admin/forms/account.py:170 +msgid "E-mail" +msgstr "E-mail" + +#: admin/forms/account.py:172 admin/forms/domain.py:35 +msgid "Quota" +msgstr "Kvóta" + +#: admin/forms/account.py:174 +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 "" +"Kvóta pro tuto chránku v MB. Definujte vlastní hodnotu nebo použijte výchozí " +"hodnotu domény. Ponechejte prázdné pro neomezenou kvótu (není povoleno " +"správcům domény)." + +#: admin/forms/account.py:182 admin/forms/domain.py:43 +#: admin/templates/admin/domains.html:26 +msgid "Alias(es)" +msgstr "Alias(y)" + +#: admin/forms/account.py:185 +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(y) této schránky. Vložte pouze jeden alias na řádek, stiskněte ENTER " +"pro vložení dalšího aliasu. Pro vytvoření doménového koše vložte '*' (např. " +"*@domena.cz)." + +#: admin/forms/account.py:248 admin/forms/account.py:279 +#: admin/forms/alias.py:72 +msgid "Domain does not exist" +msgstr "Doména neexistuje" + +#: admin/forms/account.py:355 +msgid "Domain(s)" +msgstr "Doména(y)" + +#: admin/forms/account.py:357 +msgid "Domain(s) that user administrates" +msgstr "Doména(y) pod správou uživatele" + +#: admin/forms/account.py:401 admin/forms/account.py:487 +#: admin/forms/domain.py:300 admin/forms/domain.py:370 +msgid "General" +msgstr "Obecné" + +#: admin/forms/account.py:406 admin/forms/account.py:493 +msgid "Mail" +msgstr "Mail" + +#: admin/forms/account.py:409 +msgid "Permissions" +msgstr "Oprávnění" + +#: admin/forms/account.py:476 +msgid "Account updated" +msgstr "Účet aktualizován" + +#: admin/forms/account.py:500 +msgid "New account" +msgstr "Nový účet" + +#: admin/forms/account.py:512 +msgid "Account created" +msgstr "Účet vytvořen" + +#: admin/forms/alias.py:23 +msgid "Email address" +msgstr "Emailová adresa" + +#: admin/forms/alias.py:25 +#, fuzzy +#| msgid "" +#| "The alias address (without the domain part). For a 'catch-all' address, " +#| "just enter an * character." +msgid "" +"The alias address. To create a catchall alias, just enter the domain name " +"(@domain.tld)." +msgstr "" +"Alias adresy (bez části označující doménu). Pro vytvoření doménového koše " +"vložte znak '*'." + +#: admin/forms/alias.py:31 +msgid "Recipients" +msgstr "Příjemci" + +#: admin/forms/alias.py:33 +#, fuzzy +#| msgid "" +#| "Mailbox(es) this alias will point to. Indicate only one address per " +#| "input, press ENTER to add a new input." +msgid "" +"Addresses this alias will point to. Indicate only one address per input, " +"press ENTER to add a new input." +msgstr "" +"Schránka/Schránky, na kterou/které bude tento alias odkazovat. Uveďte na " +"řádek pouze jednu adresu; pro vložení další adresy stiskněte po vložení " +"ENTER." + +#: admin/forms/alias.py:75 +msgid "You don't have access to this domain" +msgstr "Nemáte oprávnění k přístupu k této doméně" + +#: admin/forms/alias.py:85 +msgid "No recipient defined" +msgstr "Nebyl určen příjemce" + +#: admin/forms/domain.py:22 admin/models/domain.py:76 +#: admin/templates/admin/domain_statistics.html:49 +msgid "Domain" +msgstr "Doména" + +#: admin/forms/domain.py:32 admin/templates/admin/domain_statistics.html:26 +msgid "Type" +msgstr "Typ" + +#: admin/forms/domain.py:38 +msgid "" +"Default quota in MB applied to mailboxes. Leave empty to use the default " +"value." +msgstr "" +"Výchozí kvóta v MB pro schránky. Ponechejte prázdné pro výchozí nastavení " +"kvóty." + +#: admin/forms/domain.py:46 +msgid "" +"Alias(es) of this domain. Indicate only one name per input, press ENTER to " +"add a new input." +msgstr "" +"Alias(y) této domény. Uveďte na řádek pouze jeden alias; pro vložení dalšího " +"aliasu stiskněte po vložení ENTER." + +#: admin/forms/domain.py:97 admin/forms/domain.py:108 +#: admin/forms/domain.py:116 +#, python-format +msgid "A %s with this name already exists" +msgstr "%s s tímto názvem již existuje" + +#: admin/forms/domain.py:108 admin/forms/domain.py:113 +msgid "domain" +msgstr "doména" + +#: admin/forms/domain.py:200 +msgid "Create a domain administrator" +msgstr "Vytvořit správce domény" + +#: admin/forms/domain.py:203 +msgid "Automatically create an administrator for this domain" +msgstr "Automaticky vytvořit správce této domény" + +#: admin/forms/domain.py:208 admin/templates/admin/domains.html:25 +#: admin/templates/admin/editdomainform.html:13 +#: admin/templates/admin/identity_headers.html:2 +msgid "Name" +msgstr "Jméno" + +#: admin/forms/domain.py:211 +msgid "" +"The administrator's name. Don't include the domain's name here, it will be " +"automatically appended." +msgstr "" +"Jméno správce. Nevkládejte sem název domény, název bude automaticky doplněn." + +#: admin/forms/domain.py:218 +msgid "Create aliases" +msgstr "Vytvořit aliasy" + +#: admin/forms/domain.py:221 +msgid "Automatically create standard aliases for this domain" +msgstr "Automaticky vytvořit běžné aliasy pro tuto doménu" + +#: admin/forms/domain.py:240 +msgid "Invalid format" +msgstr "Neplatný formát" + +#: admin/forms/domain.py:257 +#, python-format +msgid "User '%s' already exists" +msgstr "Uživatel '%s' již existuje" + +#: admin/forms/domain.py:359 +msgid "Domain modified" +msgstr "Doména upravena" + +#: admin/forms/domain.py:379 +msgid "Options" +msgstr "Možnosti" + +#: admin/forms/domain.py:387 +msgid "New domain" +msgstr "Nová doména" + +#: admin/forms/domain.py:405 +msgid "Domain created" +msgstr "Doména vytvořena" + +#: admin/forms/export.py:7 +msgid "File name" +msgstr "Název souboru" + +#: admin/forms/export.py:13 admin/forms/import_.py:13 +msgid "Separator" +msgstr "Oddělovač" + +#: admin/forms/forward.py:14 +msgid "Recipient(s)" +msgstr "Příjemce(i)" + +#: admin/forms/forward.py:18 +msgid "Indicate one or more recipients separated by a ','" +msgstr "Uveďte jednoho nebo více příjemců oddělených pomocí ','" + +#: admin/forms/forward.py:21 +msgid "Keep local copies" +msgstr "Ponechat místní kopie" + +#: admin/forms/forward.py:24 +msgid "Forward messages and store copies into your local mailbox" +msgstr "Přeposílat zprávy a ukládat kopie do místní schránky" + +#: admin/forms/forward.py:42 +msgid "" +"You can't define a forward to a local destination. Please ask your " +"administrator to create an alias instead." +msgstr "" +"Nemůžete nastavit přeposílání na místní adresu. Požádejte správce o " +"vytvoření aliasu." + +#: admin/forms/import_.py:11 +msgid "Select a file" +msgstr "Vybrat soubor" + +#: admin/forms/import_.py:19 +msgid "Continue on error" +msgstr "Při výskytu chyby pokračovat" + +#: admin/forms/import_.py:20 +msgid "Don't treat duplicated objects as error" +msgstr "Nepovažovat duplikáty objektů za chybu" + +#: admin/forms/import_.py:38 +msgid "Crypt passwords" +msgstr "Zašifrovat hesla" + +#: admin/forms/import_.py:40 +msgid "Check this option if passwords contained in your file are not crypted" +msgstr "Zatrhněte toto pole, pokud hesla ve vašem souboru nejsou zašifrována" + +#: admin/lib.py:30 +msgid "A mailbox is required" +msgstr "Schránka je vyžadována" + +#: admin/lib.py:120 +#, fuzzy +#| msgid "You can't delete your own domain" +msgid "You are not allowed to import domains" +msgstr "Nemůžete smazat vlastní doménu" + +#: admin/lib.py:129 +msgid "You are not allowed to import domain aliases." +msgstr "" + +#: admin/models/alias.py:26 admin/models/mailbox.py:77 +msgid "address" +msgstr "adresa" + +#: admin/models/alias.py:28 +#, fuzzy +#| msgid "The alias name" +msgid "The alias address." +msgstr "Název aliasu" + +#: admin/models/alias.py:33 admin/models/domain.py:37 +#: admin/models/domain_alias.py:24 +msgid "enabled" +msgstr "povoleno" + +#: admin/models/alias.py:34 admin/models/domain_alias.py:25 +msgid "Check to activate this alias" +msgstr "Zatrhněte pole pro povolení tohoto aliasu" + +#: admin/models/alias.py:79 +msgid "distribution list" +msgstr "distribuční seznam" + +#: admin/models/alias.py:80 +msgid "forward" +msgstr "přeposlat" + +#: admin/models/alias.py:81 +msgid "alias" +msgstr "alias" + +#: admin/models/alias.py:112 +#, fuzzy +#| msgid "Invalid request" +msgid "Invalid address" +msgstr "Neplatný požadavek" + +#: admin/models/alias.py:133 +#, fuzzy +#| msgid "Local recipient %s not found" +msgid "Local recipient {}@{} not found" +msgstr "Místní příjemce %s nebyl nalezen" + +#: admin/models/alias.py:162 +#, python-format +msgid "Invalid line: %s" +msgstr "Neplatný řádek: %s" + +#: admin/models/alias.py:168 +#, python-format +msgid "Domain '%s' does not exist" +msgstr "Doména '%s' neexistuje" + +#: admin/models/domain.py:33 admin/models/domain_alias.py:17 +msgid "name" +msgstr "název" + +#: admin/models/domain.py:34 +msgid "The domain name" +msgstr "Doménové jméno" + +#: admin/models/domain.py:38 +msgid "Check to activate this domain" +msgstr "Zatrhněte pole pro povolení této domény" + +#: admin/models/domain.py:160 admin/models/domain_alias.py:47 +msgid "Invalid line" +msgstr "Neplatný řádek" + +#: admin/models/domain.py:168 +#, python-format +msgid "Invalid quota value for domain '%s'" +msgstr "Neplatná hodnota kvóty u domény '%s'" + +#: admin/models/domain_alias.py:18 +msgid "The alias name" +msgstr "Název aliasu" + +#: admin/models/domain_alias.py:20 +msgid "target" +msgstr "cíl" + +#: admin/models/domain_alias.py:21 +msgid "The domain this alias points to" +msgstr "Doména, na kterou tento alias odkazuje" + +#: admin/models/domain_alias.py:56 +#, python-format +msgid "Unknown domain %s" +msgstr "Neznámá doména %s" + +#: admin/models/mailbox.py:79 +msgid "Mailbox address (without the @domain.tld part)" +msgstr "Adresa schránky (bez části @doména.tld)" + +#: admin/models/mailbox.py:144 +#, python-format +msgid "Failed to retrieve mailbox location (%s)" +msgstr "Nebylo možné zjistit umístění schránky (%s)" + +#: admin/models/mailbox.py:233 +#, python-format +msgid "Quota is greater than the allowed domain's limit (%dM)" +msgstr "Zvolená kvóta je větší než povolené omezení domény (%dM)" + +#: admin/models/mailbox.py:239 +msgid "A quota is required" +msgstr "Kvóta je vyžadována" + +#: admin/templates/admin/account_general_form.html:11 +#: admin/templates/admin/aliasform.html:8 +msgid "Domain is disabled" +msgstr "Doména je zakázána" + +#: admin/templates/admin/domain_general_form.html:4 +#: admin/templates/admin/mailform.html:3 +#: admin/templates/admin/newdomainform.html:3 +msgid "MB" +msgstr "MB" + +#: admin/templates/admin/domain_options_form.html:7 +msgid "You can't create a new domain administrator" +msgstr "Nemůžete vytvořit nového správce domény" + +#: admin/templates/admin/domain_statistics.html:15 +#: admin/templatetags/admin_tags.py:45 +msgid "Statistics" +msgstr "Statistiky" + +#: admin/templates/admin/domain_statistics.html:15 +msgid "Domains and identities" +msgstr "Domény a identity" + +#: admin/templates/admin/domain_statistics.html:22 +msgid "Global counters" +msgstr "Globální počitadla" + +#: admin/templates/admin/domain_statistics.html:27 +msgid "Quantity" +msgstr "Počet" + +#: admin/templates/admin/domain_statistics.html:33 +#: admin/templates/admin/editdomainform.html:24 +msgid "Domain aliases" +msgstr "Doménové aliasy" + +#: admin/templates/admin/domain_statistics.html:45 +msgid "Per-domain counters" +msgstr "Počitadla dle domén" + +#: admin/templates/admin/domains.html:27 +#: admin/templates/admin/identity_headers.html:4 +msgid "Tags" +msgstr "Značky" + +#: admin/templates/admin/domains.html:28 +#: admin/templates/admin/identity_headers.html:5 +msgid "Actions" +msgstr "Akce" + +#: admin/templates/admin/domains_table.html:4 +msgid "disabled" +msgstr "zákázáno" + +#: admin/templates/admin/editdomainform.html:4 +msgid "Administrators" +msgstr "Správci" + +#: admin/templates/admin/editdomainform.html:7 +msgid "Miscellaneous" +msgstr "Různé" + +#: admin/templates/admin/editdomainform.html:19 +msgid "No domain administrator defined" +msgstr " Nebyl určen správce domény" + +#: admin/templates/admin/editdomainform.html:22 +msgid "Creation date" +msgstr "Datum vytvoření" + +#: admin/templates/admin/editdomainform.html:23 +msgid "Last modification date" +msgstr "Datum poslední změny" + +#: admin/templates/admin/editdomainform.html:26 +msgid "Mailbox aliases" +msgstr "Aliasy schránek" + +#: admin/templates/admin/forward.html:2 +msgid "Automatically forward emails to another address" +msgstr "Automaticky přesměrovat emaily na jinou adresu" + +#: admin/templates/admin/forward.html:9 admin/views/alias.py:113 +msgid "Update" +msgstr "Aktualizovat" + +#: admin/templates/admin/identity_headers.html:3 +msgid "Fullname/Recipient" +msgstr "Celé jméno/Příjemce" + +#: admin/templates/admin/importform.html:6 +msgid "Help" +msgstr "Nápověda" + +#: admin/templates/admin/importform.html:13 +msgid "Importing..." +msgstr "Importuji..." + +#: admin/templates/admin/mailboxes_raw.html:3 +msgid "Choose a value" +msgstr "Zvolte hodnotu" + +#: admin/templates/admin/mailform.html:3 +msgid "Use domain default value" +msgstr "Použít výchozí hodnotu domény" + +#: admin/templates/admin/quota_headers.html:2 +msgid "Address" +msgstr "Adresa" + +#: admin/templates/admin/quota_headers.html:3 +msgid "Value" +msgstr "Hodnota" + +#: admin/templates/admin/quota_headers.html:4 +msgid "Limit" +msgstr "Omezení" + +#: admin/templates/admin/quota_headers.html:5 +#, python-format +msgid "Usage (%%)" +msgstr "Využití (%%)" + +#: admin/templates/admin/searchform.html:6 +msgid "Search" +msgstr "Hledat" + +#: admin/templatetags/admin_tags.py:19 +msgid "enabled_m" +msgstr "Aktivovaný" + +#: admin/templatetags/admin_tags.py:19 +msgid "enabled_f" +msgstr "Aktivovaná" + +#: admin/templatetags/admin_tags.py:40 +msgid "List domains" +msgstr "Seznam domén" + +#: admin/templatetags/admin_tags.py:53 +msgid "Add domain" +msgstr "Přidat doménu" + +#: admin/templatetags/admin_tags.py:62 admin/templatetags/admin_tags.py:126 +#: admin/views/import_.py:99 admin/views/import_.py:133 +msgid "Import" +msgstr "Import" + +#: admin/templatetags/admin_tags.py:68 admin/templatetags/admin_tags.py:132 +#: admin/views/export.py:41 admin/views/export.py:69 +msgid "Export" +msgstr "Export" + +#: admin/templatetags/admin_tags.py:92 +msgid "List identities" +msgstr "Seznam identit" + +#: admin/templatetags/admin_tags.py:97 +msgid "List quotas" +msgstr "Seznam kvót" + +#: admin/templatetags/admin_tags.py:102 +msgid "Add account" +msgstr "Přidat účet" + +#: admin/templatetags/admin_tags.py:108 +msgid "Add alias" +msgstr "Přidat alias" + +#: admin/templatetags/admin_tags.py:114 +msgid "Add forward" +msgstr "Přidat přeposílání" + +#: admin/templatetags/admin_tags.py:120 +msgid "Add distribution list" +msgstr "Přidat distribuční seznam" + +#: admin/templatetags/admin_tags.py:152 +msgid "View the domain's identities" +msgstr "Zobrazit seznam identit domény" + +#: admin/templatetags/admin_tags.py:159 admin/templatetags/admin_tags.py:182 +#: admin/templatetags/admin_tags.py:190 +#, python-format +msgid "Delete %s?" +msgstr "Smazat %s?" + +#: admin/templatetags/admin_tags.py:267 +msgid "Remove this permission" +msgstr "Odstranit toto oprávnění" + +#: admin/views/alias.py:29 +msgid "Alias with this name already exists" +msgstr "Alias s tímto názvem již existuje" + +#: admin/views/alias.py:53 +msgid "Create" +msgstr "Vytvořit" + +#: admin/views/alias.py:65 +msgid "New distribution list" +msgstr "Nový distribuční seznam" + +#: admin/views/alias.py:67 +msgid "Distribution list created" +msgstr "Distribuční seznam vytvořen" + +#: admin/views/alias.py:76 +msgid "New alias" +msgstr "Nový alias" + +#: admin/views/alias.py:77 +msgid "Alias created" +msgstr "Alias vytvořen" + +#: admin/views/alias.py:86 +msgid "New forward" +msgstr "Nové přeposílání" + +#: admin/views/alias.py:87 +msgid "Forward created" +msgstr "Přeposílání vytvořeno" + +#: admin/views/alias.py:101 +msgid "Distribution list modified" +msgstr "Distribuční seznam upraven" + +#: admin/views/alias.py:103 +msgid "Forward modified" +msgstr "Přeposílání upraveno" + +#: admin/views/alias.py:105 +msgid "Alias modified" +msgstr "Alias upraven" + +#: admin/views/domain.py:132 +msgid "You can't delete your own domain" +msgstr "Nemůžete smazat vlastní doménu" + +#: admin/views/domain.py:135 +msgid "Domain deleted" +msgid_plural "Domains deleted" +msgstr[0] "Doména smazána" +msgstr[1] "Domény smazány" +msgstr[2] "Domén smazáno" + +#: admin/views/export.py:40 +msgid "Export identities" +msgstr "Exportovat identity" + +#: admin/views/export.py:68 +msgid "Export domains" +msgstr "Exportovat domény" + +#: admin/views/identity.py:108 admin/views/identity.py:200 +#: admin/views/identity.py:205 +msgid "Invalid request" +msgstr "Neplatný požadavek" + +#: admin/views/identity.py:190 +msgid "Account deleted" +msgid_plural "Accounts deleted" +msgstr[0] "Účet smazán" +msgstr[1] "Účty smazány" +msgstr[2] "Účtů smazáno" + +#: admin/views/import_.py:64 +#, python-format +msgid "Object already exists: %s" +msgstr "Objekt již existuje: %s" + +#: admin/views/import_.py:68 +#, python-format +msgid "%d objects imported successfully" +msgstr "%d objektů úspěšně importováno" + +#: admin/views/import_.py:86 +#, 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 "" +"Nahrajte CSV soubor, který bude mít jednotlivé řádky v některém z uvedených " +"formátů:\n" +"\n" +"

První element každého řádku je povinný a musí být shodný s některou z " +"předchozích hodnot.

\n" +"

Můžete také použít jiný oddělovač hodnot.

\n" + +#: admin/views/import_.py:98 +msgid "Import domains" +msgstr "Importovat domény" + +#: admin/views/import_.py:120 +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 "" +"Nahrajte CSV soubor, který bude mít jednotlivé řádky v některém z uvedených " +"formátů:\n" +"\n" +"

První element každého řádku je povinný a musí být shodný s některou z " +"předchozích hodnot.

\n" +"

Můžete také použít jiný oddělovač hodnot.

\n" + +#: admin/views/import_.py:132 +msgid "Import identities" +msgstr "Importovat identity" + +#: admin/views/user.py:34 +msgid "Forward updated" +msgstr "Přeposílání aktualizováno" diff --git a/modoboa/admin/locale/cs_CZ/LC_MESSAGES/djangojs.po b/modoboa/admin/locale/cs_CZ/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..c10167f33 --- /dev/null +++ b/modoboa/admin/locale/cs_CZ/LC_MESSAGES/djangojs.po @@ -0,0 +1,55 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Miroslav Abrahám , 2013,2015 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 08:20+0100\n" +"PO-Revision-Date: 2015-01-06 10:05+0000\n" +"Last-Translator: Miroslav Abrahám \n" +"Language-Team: Czech (Czech Republic) (http://www.transifex.com/projects/p/" +"modoboa/language/cs_CZ/)\n" +"Language: cs_CZ\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" + +#: static/admin/js/admin.js:126 +msgid "No more domain to show" +msgstr "Žádná další doména k zobrazení" + +#: static/admin/js/admin.js:145 +msgid "Do not delete domain directory" +msgstr "Nemazat adresář domény" + +#: static/admin/js/admin.js:147 +msgid "This operation will remove ALL data associated to this domain." +msgstr "Tento krok odstraní VŠECHNA data související s touto doménou." + +#: static/admin/js/admin.js:148 +msgid "" +"This operation will remove all data associated to this domain, excepting " +"accounts." +msgstr "" +"Tento krok odstraní VŠECHNA data související s touto doménou, vyjma účtů." + +#: static/admin/js/admin.js:231 +msgid "No domain administrator defined" +msgstr "Není určen správce domény" + +#: static/admin/js/admin.js:286 static/admin/js/admin.js.c:354 +msgid "No more identity to show" +msgstr "Žádná další identita k zobrazení" + +#: static/admin/js/admin.js:328 +msgid "Do not delete mailbox directory" +msgstr "Nemazat adresář schránky" + +#: static/admin/js/admin.js:357 +msgid "No more quota to show" +msgstr "Žádná další kvóta k zobrazení" diff --git a/modoboa/admin/locale/de/LC_MESSAGES/django.po b/modoboa/admin/locale/de/LC_MESSAGES/django.po new file mode 100644 index 000000000..e8551b317 --- /dev/null +++ b/modoboa/admin/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,876 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Arvedui , 2014 +# controlc.de, 2015 +# Patrick Koetter , 2010 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: 2015-09-09 09:33+0000\n" +"Last-Translator: controlc.de\n" +"Language-Team: German (http://www.transifex.com/tonio/modoboa/language/de/)\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: admin/app_settings.py:54 admin/templates/admin/editdomainform.html:25 +msgid "Mailboxes" +msgstr "Postfächer" + +#: admin/app_settings.py:57 +msgid "Handle mailboxes on filesystem" +msgstr "Postfächer im Dateisystem verwalten" + +#: admin/app_settings.py:60 +msgid "" +"Rename or remove mailboxes on the filesystem when they get renamed or " +"removed within Modoboa" +msgstr "" +"Postfächer auch in Dateisystem umbenennen oder entfernen wenn sie in Modoboa " +"umbenannt oder entfernt werden" + +#: admin/app_settings.py:66 +msgid "Mailboxes owner" +msgstr "Postfach-Besitzer" + +#: admin/app_settings.py:69 +msgid "The UNIX account who owns mailboxes on the filesystem" +msgstr "" +"UID des virtuellen Benutzers, der Besitzer der Domains, Mailboxen und " +"Nachrichten im Dateisystem ist." + +#: admin/app_settings.py:74 +msgid "Default domain quota" +msgstr "Standard-Domainquota" + +#: admin/app_settings.py:77 +msgid "" +"Default quota (in MB) applied to freshly created domains with no value " +"specified. A value of 0 means no quota." +msgstr "" +"Standardquota (in MB) für neu erstelle Domains ohne explizite Wertangabe. " +"Der Wert 0 bedeutet „Kein Quota“" + +#: admin/app_settings.py:83 +msgid "Automatic account removal" +msgstr "Automatische Konto-Entfernung" + +#: admin/app_settings.py:86 +msgid "When a mailbox is removed, also remove the associated account" +msgstr "Beim Löschen des Postfachs auch das Konto entfernen." + +#: admin/app_settings.py:90 +msgid "Automatic domain/mailbox creation" +msgstr "Automatische Domain- /Postfach-Erstellung" + +#: admin/app_settings.py:93 +msgid "Create a domain and a mailbox when an account is automatically created." +msgstr "" +"Erstelle eine Domain und ein Postfach, wenn ein Konto automatisch erstellt " +"wurde." + +#: admin/app_settings.py:139 admin/forms/account.py:239 +#: admin/forms/domain.py:77 +msgid "Must be a positive integer" +msgstr "Muss eine positive Ganzzahl sein" + +#: admin/app_settings.py:149 +msgid "Administration" +msgstr "Verwaltung" + +#: admin/callbacks.py:46 +msgid "Domain administrator" +msgstr "Domain Administrator" + +#: admin/callbacks.py:75 admin/templates/admin/domain_statistics.html:30 +#: admin/templates/admin/domains.html:3 +msgid "Domains" +msgstr "Domains" + +#: admin/callbacks.py:85 admin/templates/admin/domain_statistics.html:36 +#: admin/templates/admin/domain_statistics.html:50 +#: admin/templates/admin/identities.html:5 +msgid "Identities" +msgstr "Identitäten" + +#: admin/callbacks.py:100 admin/templates/admin/forward.html:2 +msgid "Forward" +msgstr "Weiterleiten" + +#: admin/callbacks.py:163 +#, python-format +msgid "Account import failed (%s): domain does not exist" +msgstr "Kann Konto nicht importieren (%s): Die Domain existiert nicht" + +#: admin/callbacks.py:173 admin/forms/account.py:257 +#, python-format +msgid "Mailbox %s already exists" +msgstr "Postfach %s existiert bereits" + +#: admin/callbacks.py:181 +#, python-format +msgid "Account import failed (%s): wrong quota value" +msgstr "Konto Import fehlgeschlagen (%s): ungültiger Quota-Wert" + +#: admin/callbacks.py:216 admin/forms/domain.py:94 +msgid "domain alias" +msgstr "Domain-Alias" + +#: admin/forms/account.py:30 admin/templates/admin/editdomainform.html:13 +msgid "Username" +msgstr "Benutzername" + +#: admin/forms/account.py:32 +msgid "" +"The user's name. Must be a valid e-mail address for simple users or " +"administrators with a mailbox." +msgstr "" +"Der Benutzername. Muss eine gültige E-Mail-Adresse für 'Normale Benutzer' " +"oder Administratoren mit einer Mailbox sein." + +#: admin/forms/account.py:37 +msgid "Role" +msgstr "Rolle" + +#: admin/forms/account.py:38 admin/forms/account.py:75 +msgid "Choose" +msgstr "Auswählen" + +#: admin/forms/account.py:39 +msgid "What level of permission this user will have" +msgstr "Rechte, die der Benutzer haben wird" + +#: admin/forms/account.py:42 +msgid "Password" +msgstr "Passwort" + +#: admin/forms/account.py:45 +msgid "Confirmation" +msgstr "Bestätigung" + +#: admin/forms/account.py:48 +msgid "Enter the same password as above, for verification." +msgstr "Passwort bestätigen" + +#: admin/forms/account.py:66 +msgid "Enabled" +msgstr "Aktiviert" + +#: admin/forms/account.py:141 +msgid "Only super administrators are allowed for this mode" +msgstr "Nur Super-Administratoren dürfen diesen Modus benutzen." + +#: admin/forms/account.py:150 +msgid "The two password fields didn't match." +msgstr "Die angegebenen Passwörter stimmen nicht überein" + +#: admin/forms/account.py:156 +msgid "You can't disable your own account" +msgstr "Sie können Ihr eigenes Konto nicht deaktivieren" + +#: admin/forms/account.py:170 +msgid "E-mail" +msgstr "E-Mail" + +#: admin/forms/account.py:172 admin/forms/domain.py:35 +msgid "Quota" +msgstr "Quota" + +#: admin/forms/account.py:174 +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 für dieses Postfach. Geben Sie einen individuellen Wert an oder " +"nutzen Sie den Standardwert der Domain. Lassen Sie das Feld leer für " +"unbegrenztes Quota (Domain-Administratoren ist dies nicht gestattet)." + +#: admin/forms/account.py:182 admin/forms/domain.py:43 +#: admin/templates/admin/domains.html:26 +msgid "Alias(es)" +msgstr "Alias(e)" + +#: admin/forms/account.py:185 +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(e) für dieses Postfach. Geben Sie jeweils nur eine Adresse pro Eingabe " +"an. Bestätigen Sie die Eingabe mit ENTER. Geben Sie '*' an, um einen " +"'catchall'-Alias (Bsp: *@example.com) zu erstellen." + +#: admin/forms/account.py:248 admin/forms/account.py:279 +#: admin/forms/alias.py:72 +msgid "Domain does not exist" +msgstr "Domain existiert nicht" + +#: admin/forms/account.py:355 +msgid "Domain(s)" +msgstr "Domain(s)" + +#: admin/forms/account.py:357 +msgid "Domain(s) that user administrates" +msgstr "Domain(s), die dieser Benutzer administriert" + +#: admin/forms/account.py:401 admin/forms/account.py:487 +#: admin/forms/domain.py:300 admin/forms/domain.py:370 +msgid "General" +msgstr "Allgemein" + +#: admin/forms/account.py:406 admin/forms/account.py:493 +msgid "Mail" +msgstr "E-Mail" + +#: admin/forms/account.py:409 +msgid "Permissions" +msgstr "Berechtigungen" + +#: admin/forms/account.py:476 +msgid "Account updated" +msgstr "Konto aktualisiert" + +#: admin/forms/account.py:500 +msgid "New account" +msgstr "Neues Konto" + +#: admin/forms/account.py:512 +msgid "Account created" +msgstr "Konto erstellt" + +#: admin/forms/alias.py:23 +msgid "Email address" +msgstr "E-Mail Adresse" + +#: admin/forms/alias.py:25 +msgid "" +"The alias address. To create a catchall alias, just enter the domain name " +"(@domain.tld)." +msgstr "" +"Die Alias-Adresse. Geben Sie nur den Domainnamen (@domain.tld) für eine " +"'catch-all' Alias an." + +#: admin/forms/alias.py:31 +msgid "Recipients" +msgstr "Empfänger" + +#: admin/forms/alias.py:33 +msgid "" +"Addresses this alias will point to. Indicate only one address per input, " +"press ENTER to add a new input." +msgstr "" +"Adressen, auf die der Alias verweist. Geben Sie jeweils nur eine Adresse ein " +"und bestätigen Sie mit ENTER." + +#: admin/forms/alias.py:75 +msgid "You don't have access to this domain" +msgstr "Der Zugriff auf diese Domain ist Ihnen nicht gestattet" + +#: admin/forms/alias.py:85 +msgid "No recipient defined" +msgstr "Keine Empfänger angegeben" + +#: admin/forms/domain.py:22 admin/models/domain.py:76 +#: admin/templates/admin/domain_statistics.html:49 +msgid "Domain" +msgstr "Domain" + +#: admin/forms/domain.py:32 admin/templates/admin/domain_statistics.html:26 +msgid "Type" +msgstr "Typ" + +#: admin/forms/domain.py:38 +msgid "" +"Default quota in MB applied to mailboxes. Leave empty to use the default " +"value." +msgstr "" +"Standardquota in MB für Mailboxen. Bei leerem Feld wird der Domain-Standard " +"genutzt." + +#: admin/forms/domain.py:46 +msgid "" +"Alias(es) of this domain. Indicate only one name per input, press ENTER to " +"add a new input." +msgstr "" +"Alias(e) für diese Domain. Geben Sie jeweils nur eine Adresse pro " +"Eingabefeld an. Bestätigen Sie mit ENTER, um ein neues Eingabefeld zu " +"erhalten." + +#: admin/forms/domain.py:97 admin/forms/domain.py:108 +#: admin/forms/domain.py:116 +#, python-format +msgid "A %s with this name already exists" +msgstr "Ein %s mit diesem Namen existiert bereits" + +#: admin/forms/domain.py:108 admin/forms/domain.py:113 +msgid "domain" +msgstr "Domain" + +#: admin/forms/domain.py:200 +msgid "Create a domain administrator" +msgstr "Domain-Administrator erstellen" + +#: admin/forms/domain.py:203 +msgid "Automatically create an administrator for this domain" +msgstr "Automatisch Administrator für diese Domain anlegen" + +#: admin/forms/domain.py:208 admin/templates/admin/domains.html:25 +#: admin/templates/admin/editdomainform.html:13 +#: admin/templates/admin/identity_headers.html:2 +msgid "Name" +msgstr "Name" + +#: admin/forms/domain.py:211 +msgid "" +"The administrator's name. Don't include the domain's name here, it will be " +"automatically appended." +msgstr "" +"Name des Administrators. Geben Sie nicht den Domainnamen mit an. Er wird " +"automatisch angehängt." + +#: admin/forms/domain.py:218 +msgid "Create aliases" +msgstr "Aliase erstellen" + +#: admin/forms/domain.py:221 +msgid "Automatically create standard aliases for this domain" +msgstr "Automatisch Standard-Aliase für Domain anlegen" + +#: admin/forms/domain.py:240 +msgid "Invalid format" +msgstr "Ungültiges Format" + +#: admin/forms/domain.py:257 +#, python-format +msgid "User '%s' already exists" +msgstr "Benutzer %s existiert bereits" + +#: admin/forms/domain.py:359 +msgid "Domain modified" +msgstr "Domain verändert" + +#: admin/forms/domain.py:379 +msgid "Options" +msgstr "Optionen" + +#: admin/forms/domain.py:387 +msgid "New domain" +msgstr "Neue Domain" + +#: admin/forms/domain.py:405 +msgid "Domain created" +msgstr "Domain erstellt." + +#: admin/forms/export.py:7 +msgid "File name" +msgstr "Dateiname" + +#: admin/forms/export.py:13 admin/forms/import_.py:13 +msgid "Separator" +msgstr "Separator" + +#: admin/forms/forward.py:14 +msgid "Recipient(s)" +msgstr "Empfänger" + +#: admin/forms/forward.py:18 +msgid "Indicate one or more recipients separated by a ','" +msgstr "Geben sie einen oder mehrere, durch ',' separierte Empfänger an" + +#: admin/forms/forward.py:21 +msgid "Keep local copies" +msgstr "Lokale Kopien behalten" + +#: admin/forms/forward.py:24 +msgid "Forward messages and store copies into your local mailbox" +msgstr "Nachricht weiterleiten und Kopie in Mailbox ablegen" + +#: admin/forms/forward.py:42 +msgid "" +"You can't define a forward to a local destination. Please ask your " +"administrator to create an alias instead." +msgstr "" +"Sie können keine Weiterleitung auf ein lokales Ziel anlegen. Bitten Sie " +"Ihren Administrator stattdessen einen Alias anzulegen." + +#: admin/forms/import_.py:11 +msgid "Select a file" +msgstr "Wählen Sie eine Datei" + +#: admin/forms/import_.py:19 +msgid "Continue on error" +msgstr "Bei Fehler fortfahren" + +#: admin/forms/import_.py:20 +msgid "Don't treat duplicated objects as error" +msgstr "Doppelte Objekte nicht als Fehler behandeln" + +#: admin/forms/import_.py:38 +msgid "Crypt passwords" +msgstr "Passwörter verschlüsseln" + +#: admin/forms/import_.py:40 +msgid "Check this option if passwords contained in your file are not crypted" +msgstr "Auswählen, wenn Kennwörter in Datei nicht verschlüsselt sind" + +#: admin/lib.py:30 +msgid "A mailbox is required" +msgstr "Ein Postfach ist erforderlich" + +#: admin/lib.py:120 +msgid "You are not allowed to import domains" +msgstr "Sie verfügen nicht über ausreichend Rechte, um Domains zu importieren." + +#: admin/lib.py:129 +msgid "You are not allowed to import domain aliases." +msgstr "" +"Sie verfügen nicht über ausreichend Rechte, um Domain Aliase zu importieren." + +#: admin/models/alias.py:26 admin/models/mailbox.py:77 +msgid "address" +msgstr "Adresse" + +#: admin/models/alias.py:28 +msgid "The alias address." +msgstr "Die Alias-Adresse" + +#: admin/models/alias.py:33 admin/models/domain.py:37 +#: admin/models/domain_alias.py:24 +msgid "enabled" +msgstr "aktiviert" + +#: admin/models/alias.py:34 admin/models/domain_alias.py:25 +msgid "Check to activate this alias" +msgstr "Auswählen, um den Alias zu aktivieren" + +#: admin/models/alias.py:79 +msgid "distribution list" +msgstr "Verteilerliste" + +#: admin/models/alias.py:80 +msgid "forward" +msgstr "Weiterleitung" + +#: admin/models/alias.py:81 +msgid "alias" +msgstr "Alias" + +#: admin/models/alias.py:112 +msgid "Invalid address" +msgstr "Ungültige Adresse" + +#: admin/models/alias.py:133 +msgid "Local recipient {}@{} not found" +msgstr "Lokaler Empfänger {}@{} nicht gefunden" + +#: admin/models/alias.py:162 +#, python-format +msgid "Invalid line: %s" +msgstr "Ungültige Zeile: %s" + +#: admin/models/alias.py:168 +#, python-format +msgid "Domain '%s' does not exist" +msgstr "Domain '%s' existiert nicht" + +#: admin/models/domain.py:33 admin/models/domain_alias.py:17 +msgid "name" +msgstr "Name" + +#: admin/models/domain.py:34 +msgid "The domain name" +msgstr "Name der Domain" + +#: admin/models/domain.py:38 +msgid "Check to activate this domain" +msgstr "Auswählen, um diese Domain zu aktivieren" + +#: admin/models/domain.py:160 admin/models/domain_alias.py:47 +msgid "Invalid line" +msgstr "Ungültige Zeile" + +#: admin/models/domain.py:168 +#, python-format +msgid "Invalid quota value for domain '%s'" +msgstr "Ungültiger Quota-Wert für Domain '%s'" + +#: admin/models/domain_alias.py:18 +msgid "The alias name" +msgstr "Der Alias-Name" + +#: admin/models/domain_alias.py:20 +msgid "target" +msgstr "Ziel" + +#: admin/models/domain_alias.py:21 +msgid "The domain this alias points to" +msgstr "Domain, auf die dieser Alias verweist" + +#: admin/models/domain_alias.py:56 +#, python-format +msgid "Unknown domain %s" +msgstr "Unbekannte Domain %s" + +#: admin/models/mailbox.py:79 +msgid "Mailbox address (without the @domain.tld part)" +msgstr "Postfach-Adresse (ohne den '@domain.tld' Teil)" + +#: admin/models/mailbox.py:144 +#, python-format +msgid "Failed to retrieve mailbox location (%s)" +msgstr "Der Speicherort des Postfachs (%s) konnte nicht bestimmt werden" + +#: admin/models/mailbox.py:233 +#, python-format +msgid "Quota is greater than the allowed domain's limit (%dM)" +msgstr "Das Quota ist größer als das maximale Quota der Domain (%dM)" + +#: admin/models/mailbox.py:239 +msgid "A quota is required" +msgstr "Ein Quota ist erforderlich" + +#: admin/templates/admin/account_general_form.html:11 +#: admin/templates/admin/aliasform.html:8 +msgid "Domain is disabled" +msgstr "Domain ist deaktiviert" + +#: admin/templates/admin/domain_general_form.html:4 +#: admin/templates/admin/mailform.html:3 +#: admin/templates/admin/newdomainform.html:3 +msgid "MB" +msgstr "MB" + +#: admin/templates/admin/domain_options_form.html:7 +msgid "You can't create a new domain administrator" +msgstr "Sie können keinen neuen Domain-Administrator erstellen" + +#: admin/templates/admin/domain_statistics.html:15 +#: admin/templatetags/admin_tags.py:45 +msgid "Statistics" +msgstr "Statistiken" + +#: admin/templates/admin/domain_statistics.html:15 +msgid "Domains and identities" +msgstr "Domains und Identitäten" + +#: admin/templates/admin/domain_statistics.html:22 +msgid "Global counters" +msgstr "Zentrale Zähler" + +#: admin/templates/admin/domain_statistics.html:27 +msgid "Quantity" +msgstr "Anzahl" + +#: admin/templates/admin/domain_statistics.html:33 +#: admin/templates/admin/editdomainform.html:24 +msgid "Domain aliases" +msgstr "Domain-Aliase" + +#: admin/templates/admin/domain_statistics.html:45 +msgid "Per-domain counters" +msgstr "pro Domain" + +#: admin/templates/admin/domains.html:27 +#: admin/templates/admin/identity_headers.html:4 +msgid "Tags" +msgstr "Tags" + +#: admin/templates/admin/domains.html:28 +#: admin/templates/admin/identity_headers.html:5 +msgid "Actions" +msgstr "Aktionen" + +#: admin/templates/admin/domains_table.html:4 +msgid "disabled" +msgstr "deaktiviert" + +#: admin/templates/admin/editdomainform.html:4 +msgid "Administrators" +msgstr "Administratoren" + +#: admin/templates/admin/editdomainform.html:7 +msgid "Miscellaneous" +msgstr "Verschiedenes" + +#: admin/templates/admin/editdomainform.html:19 +msgid "No domain administrator defined" +msgstr "Kein Domain-Administrator definiert" + +#: admin/templates/admin/editdomainform.html:22 +msgid "Creation date" +msgstr "Erstellungsdatum" + +#: admin/templates/admin/editdomainform.html:23 +msgid "Last modification date" +msgstr "Datum der letzten Änderung" + +#: admin/templates/admin/editdomainform.html:26 +msgid "Mailbox aliases" +msgstr "Postfach-Aliase" + +#: admin/templates/admin/forward.html:2 +msgid "Automatically forward emails to another address" +msgstr "Nachrichten automatisch an andere Adresse weiterleiten" + +#: admin/templates/admin/forward.html:9 admin/views/alias.py:113 +msgid "Update" +msgstr "Aktualisieren" + +#: admin/templates/admin/identity_headers.html:3 +msgid "Fullname/Recipient" +msgstr "Name/Empfänger" + +#: admin/templates/admin/importform.html:6 +msgid "Help" +msgstr "Hilfe" + +#: admin/templates/admin/importform.html:13 +msgid "Importing..." +msgstr "Importiere…" + +#: admin/templates/admin/mailboxes_raw.html:3 +msgid "Choose a value" +msgstr "Wert auswählen" + +#: admin/templates/admin/mailform.html:3 +msgid "Use domain default value" +msgstr "Standardwert der Domain verwenden" + +#: admin/templates/admin/quota_headers.html:2 +msgid "Address" +msgstr "Adresse" + +#: admin/templates/admin/quota_headers.html:3 +msgid "Value" +msgstr "Wert" + +#: admin/templates/admin/quota_headers.html:4 +msgid "Limit" +msgstr "Limit" + +#: admin/templates/admin/quota_headers.html:5 +#, python-format +msgid "Usage (%%)" +msgstr "Verwendung (%%)" + +#: admin/templates/admin/searchform.html:6 +msgid "Search" +msgstr "Suchen" + +#: admin/templatetags/admin_tags.py:19 +msgid "enabled_m" +msgstr "aktiviert_m" + +#: admin/templatetags/admin_tags.py:19 +msgid "enabled_f" +msgstr "aktiviert_f" + +#: admin/templatetags/admin_tags.py:40 +msgid "List domains" +msgstr "Domain-Liste" + +#: admin/templatetags/admin_tags.py:53 +msgid "Add domain" +msgstr "Domain hinzufügen" + +#: admin/templatetags/admin_tags.py:62 admin/templatetags/admin_tags.py:126 +#: admin/views/import_.py:99 admin/views/import_.py:133 +msgid "Import" +msgstr "Import" + +#: admin/templatetags/admin_tags.py:68 admin/templatetags/admin_tags.py:132 +#: admin/views/export.py:41 admin/views/export.py:69 +msgid "Export" +msgstr "Export" + +#: admin/templatetags/admin_tags.py:92 +msgid "List identities" +msgstr "Identitäten-Liste" + +#: admin/templatetags/admin_tags.py:97 +msgid "List quotas" +msgstr "Quota-Liste" + +#: admin/templatetags/admin_tags.py:102 +msgid "Add account" +msgstr "Konto hinzufügen" + +#: admin/templatetags/admin_tags.py:108 +msgid "Add alias" +msgstr "Alias hinzufügen" + +#: admin/templatetags/admin_tags.py:114 +msgid "Add forward" +msgstr "Weiterleitung hinzufügen" + +#: admin/templatetags/admin_tags.py:120 +msgid "Add distribution list" +msgstr "Verteilerliste hinzufügen" + +#: admin/templatetags/admin_tags.py:152 +msgid "View the domain's identities" +msgstr "Domain-Identitäten anzeigen" + +#: admin/templatetags/admin_tags.py:159 admin/templatetags/admin_tags.py:182 +#: admin/templatetags/admin_tags.py:190 +#, python-format +msgid "Delete %s?" +msgstr "%s löschen?" + +#: admin/templatetags/admin_tags.py:267 +msgid "Remove this permission" +msgstr "Diese Berechtigung entfernen" + +#: admin/views/alias.py:29 +msgid "Alias with this name already exists" +msgstr "Ein Alias mit diesem Namen existiert bereits" + +#: admin/views/alias.py:53 +msgid "Create" +msgstr "Erstellen" + +#: admin/views/alias.py:65 +msgid "New distribution list" +msgstr "Neue Verteilerliste" + +#: admin/views/alias.py:67 +msgid "Distribution list created" +msgstr "Verteilerliste erstellt" + +#: admin/views/alias.py:76 +msgid "New alias" +msgstr "Neuer Alias" + +#: admin/views/alias.py:77 +msgid "Alias created" +msgstr "Alias erstellt" + +#: admin/views/alias.py:86 +msgid "New forward" +msgstr "Neue Weiterleitung" + +#: admin/views/alias.py:87 +msgid "Forward created" +msgstr "Weiterleitung erstellt" + +#: admin/views/alias.py:101 +msgid "Distribution list modified" +msgstr "Verteilerliste verändert" + +#: admin/views/alias.py:103 +msgid "Forward modified" +msgstr "Weiterleitung verändert" + +#: admin/views/alias.py:105 +msgid "Alias modified" +msgstr "Alias verändert" + +#: admin/views/domain.py:132 +msgid "You can't delete your own domain" +msgstr "Sie können Ihre eigene Domain nicht löschen" + +#: admin/views/domain.py:135 +msgid "Domain deleted" +msgid_plural "Domains deleted" +msgstr[0] "Domain gelöscht" +msgstr[1] "Domains gelöscht" + +#: admin/views/export.py:40 +msgid "Export identities" +msgstr "Identitäten exportieren" + +#: admin/views/export.py:68 +msgid "Export domains" +msgstr "Domains exportieren" + +#: admin/views/identity.py:108 admin/views/identity.py:200 +#: admin/views/identity.py:205 +msgid "Invalid request" +msgstr "Ungültige Anfrage" + +#: admin/views/identity.py:190 +msgid "Account deleted" +msgid_plural "Accounts deleted" +msgstr[0] "Konto gelöscht" +msgstr[1] "Konten gelöscht" + +#: admin/views/import_.py:64 +#, python-format +msgid "Object already exists: %s" +msgstr "Objekt existiert bereits: %s" + +#: admin/views/import_.py:68 +#, python-format +msgid "%d objects imported successfully" +msgstr "%d Objekte erfolgreich importiert" + +#: admin/views/import_.py:86 +#, python-format +msgid "" +"Provide a CSV file where lines respect one of the following formats:\n" +"
    \n" +"
  • domain; name; quota; enabled
  • \n" +"
  • domainalias; name; targeted domain; enabled
  • \n" +" %s\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 "" +"Verwenden sie eine CSV-Datei mit einem der folgenden Formate:\n" +"
    \n" +"
  • domain; name; quota; enabled
  • \n" +"
  • domainalias; name; targeted domain; enabled
  • \n" +" %s\n" +"
\n" +"

Das erste Element jeder Zeile ist zwingend und muss gleich zum " +"vorhergehenden sein.

\n" +"

Ein anderes Zeichen als Trenner ist gestattet.

\n" + +#: admin/views/import_.py:98 +msgid "Import domains" +msgstr "Domains importieren" + +#: admin/views/import_.py:120 +msgid "" +"Provide a CSV file where lines respect one of the following formats:\n" +"
    \n" +"
  • account; loginname; password; first name; last name; enabled; group; " +"address; quota; [, domain, ...]
  • \n" +"
  • alias; address; enabled; internal recipient
  • \n" +"
  • forward; address; enabled; external recipient
  • \n" +"
  • dlist; address; enabled; recipient; recipient; ...
  • \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 "" +"Verwenden sie eine CSV-Datei in einem der folgenden Formate:\n" +"
    \n" +"
  • account; loginname; password; first name; last name; enabled; group; " +"address; quota; [, domain, ...]
  • \n" +"
  • alias; address; enabled; internal recipient
  • \n" +"
  • forward; address; enabled; external recipient
  • \n" +"
  • dlist; address; enabled; recipient; recipient; ...
  • \n" +"
\n" +"

Das erste Element einer Zeile ist zwingend und muss gleich mit einem der " +"vorhergehenden sein.

\n" +"\n" +"

Ein anderes Zeichen als Separator ist gestattet.

\n" + +#: admin/views/import_.py:132 +msgid "Import identities" +msgstr "Identitäten importieren" + +#: admin/views/user.py:34 +msgid "Forward updated" +msgstr "Weiterleitung aktualisiert" diff --git a/modoboa/admin/locale/de/LC_MESSAGES/djangojs.po b/modoboa/admin/locale/de/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..f1bfeb8a4 --- /dev/null +++ b/modoboa/admin/locale/de/LC_MESSAGES/djangojs.po @@ -0,0 +1,57 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Arvedui , 2014 +# Patrick Koetter , 2010 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 08:20+0100\n" +"PO-Revision-Date: 2014-12-01 12:12+0000\n" +"Last-Translator: Arvedui \n" +"Language-Team: German (http://www.transifex.com/projects/p/modoboa/language/" +"de/)\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: static/admin/js/admin.js:126 +msgid "No more domain to show" +msgstr "Keine weiteren Domains" + +#: static/admin/js/admin.js:145 +msgid "Do not delete domain directory" +msgstr "Verzeichnis der Domain nicht löschen" + +#: static/admin/js/admin.js:147 +msgid "This operation will remove ALL data associated to this domain." +msgstr "Diese Aktion wird ALLE mit der Domain verbundenen Daten löschen." + +#: static/admin/js/admin.js:148 +msgid "" +"This operation will remove all data associated to this domain, excepting " +"accounts." +msgstr "" +"Diese Aktion wird ALLE mit der Domain verbundenen Daten, außer Mailkonten, " +"löschen." + +#: static/admin/js/admin.js:231 +msgid "No domain administrator defined" +msgstr "Domain-Administrator fehlt" + +#: static/admin/js/admin.js:286 static/admin/js/admin.js.c:354 +msgid "No more identity to show" +msgstr "Keine weiteren Identitäten" + +#: static/admin/js/admin.js:328 +msgid "Do not delete mailbox directory" +msgstr "Verzeichnis der Mailbox nicht löschen" + +#: static/admin/js/admin.js:357 +msgid "No more quota to show" +msgstr "Keine weiteren Quotas" diff --git a/modoboa/admin/locale/en/LC_MESSAGES/django.po b/modoboa/admin/locale/en/LC_MESSAGES/django.po new file mode 100644 index 000000000..17013e50f --- /dev/null +++ b/modoboa/admin/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,822 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: admin/app_settings.py:54 admin/templates/admin/editdomainform.html:25 +msgid "Mailboxes" +msgstr "" + +#: admin/app_settings.py:57 +msgid "Handle mailboxes on filesystem" +msgstr "" + +#: admin/app_settings.py:60 +msgid "" +"Rename or remove mailboxes on the filesystem when they get renamed or " +"removed within Modoboa" +msgstr "" + +#: admin/app_settings.py:66 +msgid "Mailboxes owner" +msgstr "" + +#: admin/app_settings.py:69 +msgid "The UNIX account who owns mailboxes on the filesystem" +msgstr "" + +#: admin/app_settings.py:74 +msgid "Default domain quota" +msgstr "" + +#: admin/app_settings.py:77 +msgid "" +"Default quota (in MB) applied to freshly created domains with no value " +"specified. A value of 0 means no quota." +msgstr "" + +#: admin/app_settings.py:83 +msgid "Automatic account removal" +msgstr "" + +#: admin/app_settings.py:86 +msgid "When a mailbox is removed, also remove the associated account" +msgstr "" + +#: admin/app_settings.py:90 +msgid "Automatic domain/mailbox creation" +msgstr "" + +#: admin/app_settings.py:93 +msgid "Create a domain and a mailbox when an account is automatically created." +msgstr "" + +#: admin/app_settings.py:139 admin/forms/account.py:239 +#: admin/forms/domain.py:77 +msgid "Must be a positive integer" +msgstr "" + +#: admin/app_settings.py:149 +msgid "Administration" +msgstr "" + +#: admin/callbacks.py:46 +msgid "Domain administrator" +msgstr "" + +#: admin/callbacks.py:75 admin/templates/admin/domain_statistics.html:30 +#: admin/templates/admin/domains.html:3 +msgid "Domains" +msgstr "" + +#: admin/callbacks.py:85 admin/templates/admin/domain_statistics.html:36 +#: admin/templates/admin/domain_statistics.html:50 +#: admin/templates/admin/identities.html:5 +msgid "Identities" +msgstr "" + +#: admin/callbacks.py:100 admin/templates/admin/forward.html:2 +msgid "Forward" +msgstr "" + +#: admin/callbacks.py:163 +#, python-format +msgid "Account import failed (%s): domain does not exist" +msgstr "" + +#: admin/callbacks.py:173 admin/forms/account.py:257 +#, python-format +msgid "Mailbox %s already exists" +msgstr "" + +#: admin/callbacks.py:181 +#, python-format +msgid "Account import failed (%s): wrong quota value" +msgstr "" + +#: admin/callbacks.py:216 admin/forms/domain.py:94 +msgid "domain alias" +msgstr "" + +#: admin/forms/account.py:30 admin/templates/admin/editdomainform.html:13 +msgid "Username" +msgstr "" + +#: admin/forms/account.py:32 +msgid "" +"The user's name. Must be a valid e-mail address for simple users or " +"administrators with a mailbox." +msgstr "" + +#: admin/forms/account.py:37 +msgid "Role" +msgstr "" + +#: admin/forms/account.py:38 admin/forms/account.py:75 +msgid "Choose" +msgstr "" + +#: admin/forms/account.py:39 +msgid "What level of permission this user will have" +msgstr "" + +#: admin/forms/account.py:42 +msgid "Password" +msgstr "" + +#: admin/forms/account.py:45 +msgid "Confirmation" +msgstr "" + +#: admin/forms/account.py:48 +msgid "Enter the same password as above, for verification." +msgstr "" + +#: admin/forms/account.py:66 +msgid "Enabled" +msgstr "" + +#: admin/forms/account.py:141 +msgid "Only super administrators are allowed for this mode" +msgstr "" + +#: admin/forms/account.py:150 +msgid "The two password fields didn't match." +msgstr "" + +#: admin/forms/account.py:156 +msgid "You can't disable your own account" +msgstr "" + +#: admin/forms/account.py:170 +msgid "E-mail" +msgstr "" + +#: admin/forms/account.py:172 admin/forms/domain.py:35 +msgid "Quota" +msgstr "" + +#: admin/forms/account.py:174 +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 "" + +#: admin/forms/account.py:182 admin/forms/domain.py:43 +#: admin/templates/admin/domains.html:26 +msgid "Alias(es)" +msgstr "" + +#: admin/forms/account.py:185 +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 "" + +#: admin/forms/account.py:248 admin/forms/account.py:279 +#: admin/forms/alias.py:72 +msgid "Domain does not exist" +msgstr "" + +#: admin/forms/account.py:355 +msgid "Domain(s)" +msgstr "" + +#: admin/forms/account.py:357 +msgid "Domain(s) that user administrates" +msgstr "" + +#: admin/forms/account.py:401 admin/forms/account.py:487 +#: admin/forms/domain.py:300 admin/forms/domain.py:370 +msgid "General" +msgstr "" + +#: admin/forms/account.py:406 admin/forms/account.py:493 +msgid "Mail" +msgstr "" + +#: admin/forms/account.py:409 +msgid "Permissions" +msgstr "" + +#: admin/forms/account.py:476 +msgid "Account updated" +msgstr "" + +#: admin/forms/account.py:500 +msgid "New account" +msgstr "" + +#: admin/forms/account.py:512 +msgid "Account created" +msgstr "" + +#: admin/forms/alias.py:23 +msgid "Email address" +msgstr "" + +#: admin/forms/alias.py:25 +msgid "" +"The alias address. To create a catchall alias, just enter the domain name " +"(@domain.tld)." +msgstr "" + +#: admin/forms/alias.py:31 +msgid "Recipients" +msgstr "" + +#: admin/forms/alias.py:33 +msgid "" +"Addresses this alias will point to. Indicate only one address per input, " +"press ENTER to add a new input." +msgstr "" + +#: admin/forms/alias.py:75 +msgid "You don't have access to this domain" +msgstr "" + +#: admin/forms/alias.py:85 +msgid "No recipient defined" +msgstr "" + +#: admin/forms/domain.py:22 admin/models/domain.py:76 +#: admin/templates/admin/domain_statistics.html:49 +msgid "Domain" +msgstr "" + +#: admin/forms/domain.py:32 admin/templates/admin/domain_statistics.html:26 +msgid "Type" +msgstr "" + +#: admin/forms/domain.py:38 +msgid "" +"Default quota in MB applied to mailboxes. Leave empty to use the default " +"value." +msgstr "" + +#: admin/forms/domain.py:46 +msgid "" +"Alias(es) of this domain. Indicate only one name per input, press ENTER to " +"add a new input." +msgstr "" + +#: admin/forms/domain.py:97 admin/forms/domain.py:108 +#: admin/forms/domain.py:116 +#, python-format +msgid "A %s with this name already exists" +msgstr "" + +#: admin/forms/domain.py:108 admin/forms/domain.py:113 +msgid "domain" +msgstr "" + +#: admin/forms/domain.py:200 +msgid "Create a domain administrator" +msgstr "" + +#: admin/forms/domain.py:203 +msgid "Automatically create an administrator for this domain" +msgstr "" + +#: admin/forms/domain.py:208 admin/templates/admin/domains.html:25 +#: admin/templates/admin/editdomainform.html:13 +#: admin/templates/admin/identity_headers.html:2 +msgid "Name" +msgstr "" + +#: admin/forms/domain.py:211 +msgid "" +"The administrator's name. Don't include the domain's name here, it will be " +"automatically appended." +msgstr "" + +#: admin/forms/domain.py:218 +msgid "Create aliases" +msgstr "" + +#: admin/forms/domain.py:221 +msgid "Automatically create standard aliases for this domain" +msgstr "" + +#: admin/forms/domain.py:240 +msgid "Invalid format" +msgstr "" + +#: admin/forms/domain.py:257 +#, python-format +msgid "User '%s' already exists" +msgstr "" + +#: admin/forms/domain.py:359 +msgid "Domain modified" +msgstr "" + +#: admin/forms/domain.py:379 +msgid "Options" +msgstr "" + +#: admin/forms/domain.py:387 +msgid "New domain" +msgstr "" + +#: admin/forms/domain.py:405 +msgid "Domain created" +msgstr "" + +#: admin/forms/export.py:7 +msgid "File name" +msgstr "" + +#: admin/forms/export.py:13 admin/forms/import_.py:13 +msgid "Separator" +msgstr "" + +#: admin/forms/forward.py:14 +msgid "Recipient(s)" +msgstr "" + +#: admin/forms/forward.py:18 +msgid "Indicate one or more recipients separated by a ','" +msgstr "" + +#: admin/forms/forward.py:21 +msgid "Keep local copies" +msgstr "" + +#: admin/forms/forward.py:24 +msgid "Forward messages and store copies into your local mailbox" +msgstr "" + +#: admin/forms/forward.py:42 +msgid "" +"You can't define a forward to a local destination. Please ask your " +"administrator to create an alias instead." +msgstr "" + +#: admin/forms/import_.py:11 +msgid "Select a file" +msgstr "" + +#: admin/forms/import_.py:19 +msgid "Continue on error" +msgstr "" + +#: admin/forms/import_.py:20 +msgid "Don't treat duplicated objects as error" +msgstr "" + +#: admin/forms/import_.py:38 +msgid "Crypt passwords" +msgstr "" + +#: admin/forms/import_.py:40 +msgid "Check this option if passwords contained in your file are not crypted" +msgstr "" + +#: admin/lib.py:30 +msgid "A mailbox is required" +msgstr "" + +#: admin/lib.py:120 +msgid "You are not allowed to import domains" +msgstr "" + +#: admin/lib.py:129 +msgid "You are not allowed to import domain aliases." +msgstr "" + +#: admin/models/alias.py:26 admin/models/mailbox.py:77 +msgid "address" +msgstr "" + +#: admin/models/alias.py:28 +msgid "The alias address." +msgstr "" + +#: admin/models/alias.py:33 admin/models/domain.py:37 +#: admin/models/domain_alias.py:24 +msgid "enabled" +msgstr "" + +#: admin/models/alias.py:34 admin/models/domain_alias.py:25 +msgid "Check to activate this alias" +msgstr "" + +#: admin/models/alias.py:79 +msgid "distribution list" +msgstr "" + +#: admin/models/alias.py:80 +msgid "forward" +msgstr "" + +#: admin/models/alias.py:81 +msgid "alias" +msgstr "" + +#: admin/models/alias.py:112 +msgid "Invalid address" +msgstr "" + +#: admin/models/alias.py:133 +msgid "Local recipient {}@{} not found" +msgstr "" + +#: admin/models/alias.py:162 +#, python-format +msgid "Invalid line: %s" +msgstr "" + +#: admin/models/alias.py:168 +#, python-format +msgid "Domain '%s' does not exist" +msgstr "" + +#: admin/models/domain.py:33 admin/models/domain_alias.py:17 +msgid "name" +msgstr "" + +#: admin/models/domain.py:34 +msgid "The domain name" +msgstr "" + +#: admin/models/domain.py:38 +msgid "Check to activate this domain" +msgstr "" + +#: admin/models/domain.py:160 admin/models/domain_alias.py:47 +msgid "Invalid line" +msgstr "" + +#: admin/models/domain.py:168 +#, python-format +msgid "Invalid quota value for domain '%s'" +msgstr "" + +#: admin/models/domain_alias.py:18 +msgid "The alias name" +msgstr "" + +#: admin/models/domain_alias.py:20 +msgid "target" +msgstr "" + +#: admin/models/domain_alias.py:21 +msgid "The domain this alias points to" +msgstr "" + +#: admin/models/domain_alias.py:56 +#, python-format +msgid "Unknown domain %s" +msgstr "" + +#: admin/models/mailbox.py:79 +msgid "Mailbox address (without the @domain.tld part)" +msgstr "" + +#: admin/models/mailbox.py:144 +#, python-format +msgid "Failed to retrieve mailbox location (%s)" +msgstr "" + +#: admin/models/mailbox.py:233 +#, python-format +msgid "Quota is greater than the allowed domain's limit (%dM)" +msgstr "" + +#: admin/models/mailbox.py:239 +msgid "A quota is required" +msgstr "" + +#: admin/templates/admin/account_general_form.html:11 +#: admin/templates/admin/aliasform.html:8 +msgid "Domain is disabled" +msgstr "" + +#: admin/templates/admin/domain_general_form.html:4 +#: admin/templates/admin/mailform.html:3 +#: admin/templates/admin/newdomainform.html:3 +msgid "MB" +msgstr "" + +#: admin/templates/admin/domain_options_form.html:7 +msgid "You can't create a new domain administrator" +msgstr "" + +#: admin/templates/admin/domain_statistics.html:15 +#: admin/templatetags/admin_tags.py:45 +msgid "Statistics" +msgstr "" + +#: admin/templates/admin/domain_statistics.html:15 +msgid "Domains and identities" +msgstr "" + +#: admin/templates/admin/domain_statistics.html:22 +msgid "Global counters" +msgstr "" + +#: admin/templates/admin/domain_statistics.html:27 +msgid "Quantity" +msgstr "" + +#: admin/templates/admin/domain_statistics.html:33 +#: admin/templates/admin/editdomainform.html:24 +msgid "Domain aliases" +msgstr "" + +#: admin/templates/admin/domain_statistics.html:45 +msgid "Per-domain counters" +msgstr "" + +#: admin/templates/admin/domains.html:27 +#: admin/templates/admin/identity_headers.html:4 +msgid "Tags" +msgstr "" + +#: admin/templates/admin/domains.html:28 +#: admin/templates/admin/identity_headers.html:5 +msgid "Actions" +msgstr "" + +#: admin/templates/admin/domains_table.html:4 +msgid "disabled" +msgstr "" + +#: admin/templates/admin/editdomainform.html:4 +msgid "Administrators" +msgstr "" + +#: admin/templates/admin/editdomainform.html:7 +msgid "Miscellaneous" +msgstr "" + +#: admin/templates/admin/editdomainform.html:19 +msgid "No domain administrator defined" +msgstr "" + +#: admin/templates/admin/editdomainform.html:22 +msgid "Creation date" +msgstr "" + +#: admin/templates/admin/editdomainform.html:23 +msgid "Last modification date" +msgstr "" + +#: admin/templates/admin/editdomainform.html:26 +msgid "Mailbox aliases" +msgstr "" + +#: admin/templates/admin/forward.html:2 +msgid "Automatically forward emails to another address" +msgstr "" + +#: admin/templates/admin/forward.html:9 admin/views/alias.py:113 +msgid "Update" +msgstr "" + +#: admin/templates/admin/identity_headers.html:3 +msgid "Fullname/Recipient" +msgstr "" + +#: admin/templates/admin/importform.html:6 +msgid "Help" +msgstr "" + +#: admin/templates/admin/importform.html:13 +msgid "Importing..." +msgstr "" + +#: admin/templates/admin/mailboxes_raw.html:3 +msgid "Choose a value" +msgstr "" + +#: admin/templates/admin/mailform.html:3 +msgid "Use domain default value" +msgstr "" + +#: admin/templates/admin/quota_headers.html:2 +msgid "Address" +msgstr "" + +#: admin/templates/admin/quota_headers.html:3 +msgid "Value" +msgstr "" + +#: admin/templates/admin/quota_headers.html:4 +msgid "Limit" +msgstr "" + +#: admin/templates/admin/quota_headers.html:5 +#, python-format +msgid "Usage (%%)" +msgstr "" + +#: admin/templates/admin/searchform.html:6 +msgid "Search" +msgstr "" + +#: admin/templatetags/admin_tags.py:19 +msgid "enabled_m" +msgstr "" + +#: admin/templatetags/admin_tags.py:19 +msgid "enabled_f" +msgstr "" + +#: admin/templatetags/admin_tags.py:40 +msgid "List domains" +msgstr "" + +#: admin/templatetags/admin_tags.py:53 +msgid "Add domain" +msgstr "" + +#: admin/templatetags/admin_tags.py:62 admin/templatetags/admin_tags.py:126 +#: admin/views/import_.py:99 admin/views/import_.py:133 +msgid "Import" +msgstr "" + +#: admin/templatetags/admin_tags.py:68 admin/templatetags/admin_tags.py:132 +#: admin/views/export.py:41 admin/views/export.py:69 +msgid "Export" +msgstr "" + +#: admin/templatetags/admin_tags.py:92 +msgid "List identities" +msgstr "" + +#: admin/templatetags/admin_tags.py:97 +msgid "List quotas" +msgstr "" + +#: admin/templatetags/admin_tags.py:102 +msgid "Add account" +msgstr "" + +#: admin/templatetags/admin_tags.py:108 +msgid "Add alias" +msgstr "" + +#: admin/templatetags/admin_tags.py:114 +msgid "Add forward" +msgstr "" + +#: admin/templatetags/admin_tags.py:120 +msgid "Add distribution list" +msgstr "" + +#: admin/templatetags/admin_tags.py:152 +msgid "View the domain's identities" +msgstr "" + +#: admin/templatetags/admin_tags.py:159 admin/templatetags/admin_tags.py:182 +#: admin/templatetags/admin_tags.py:190 +#, python-format +msgid "Delete %s?" +msgstr "" + +#: admin/templatetags/admin_tags.py:267 +msgid "Remove this permission" +msgstr "" + +#: admin/views/alias.py:29 +msgid "Alias with this name already exists" +msgstr "" + +#: admin/views/alias.py:53 +msgid "Create" +msgstr "" + +#: admin/views/alias.py:65 +msgid "New distribution list" +msgstr "" + +#: admin/views/alias.py:67 +msgid "Distribution list created" +msgstr "" + +#: admin/views/alias.py:76 +msgid "New alias" +msgstr "" + +#: admin/views/alias.py:77 +msgid "Alias created" +msgstr "" + +#: admin/views/alias.py:86 +msgid "New forward" +msgstr "" + +#: admin/views/alias.py:87 +msgid "Forward created" +msgstr "" + +#: admin/views/alias.py:101 +msgid "Distribution list modified" +msgstr "" + +#: admin/views/alias.py:103 +msgid "Forward modified" +msgstr "" + +#: admin/views/alias.py:105 +msgid "Alias modified" +msgstr "" + +#: admin/views/domain.py:132 +msgid "You can't delete your own domain" +msgstr "" + +#: admin/views/domain.py:135 +msgid "Domain deleted" +msgid_plural "Domains deleted" +msgstr[0] "" +msgstr[1] "" + +#: admin/views/export.py:40 +msgid "Export identities" +msgstr "" + +#: admin/views/export.py:68 +msgid "Export domains" +msgstr "" + +#: admin/views/identity.py:108 admin/views/identity.py:200 +#: admin/views/identity.py:205 +msgid "Invalid request" +msgstr "" + +#: admin/views/identity.py:190 +msgid "Account deleted" +msgid_plural "Accounts deleted" +msgstr[0] "" +msgstr[1] "" + +#: admin/views/import_.py:64 +#, python-format +msgid "Object already exists: %s" +msgstr "" + +#: admin/views/import_.py:68 +#, python-format +msgid "%d objects imported successfully" +msgstr "" + +#: admin/views/import_.py:86 +#, python-format +msgid "" +"Provide a CSV file where lines respect one of the following formats:\n" +"
    \n" +"
  • domain; name; quota; enabled
  • \n" +"
  • domainalias; name; targeted domain; enabled
  • \n" +" %s\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 "" + +#: admin/views/import_.py:98 +msgid "Import domains" +msgstr "" + +#: admin/views/import_.py:120 +msgid "" +"Provide a CSV file where lines respect one of the following formats:\n" +"
    \n" +"
  • account; loginname; password; first name; last name; enabled; group; " +"address; quota; [, domain, ...]
  • \n" +"
  • alias; address; enabled; internal recipient
  • \n" +"
  • forward; address; enabled; external recipient
  • \n" +"
  • dlist; address; enabled; recipient; recipient; ...
  • \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 "" + +#: admin/views/import_.py:132 +msgid "Import identities" +msgstr "" + +#: admin/views/user.py:34 +msgid "Forward updated" +msgstr "" diff --git a/modoboa/admin/locale/en/LC_MESSAGES/djangojs.po b/modoboa/admin/locale/en/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..d963776e5 --- /dev/null +++ b/modoboa/admin/locale/en/LC_MESSAGES/djangojs.po @@ -0,0 +1,52 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 08:20+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: static/admin/js/admin.js:126 +msgid "No more domain to show" +msgstr "" + +#: static/admin/js/admin.js:145 +msgid "Do not delete domain directory" +msgstr "" + +#: static/admin/js/admin.js:147 +msgid "This operation will remove ALL data associated to this domain." +msgstr "" + +#: static/admin/js/admin.js:148 +msgid "" +"This operation will remove all data associated to this domain, excepting " +"accounts." +msgstr "" + +#: static/admin/js/admin.js:231 +msgid "No domain administrator defined" +msgstr "" + +#: static/admin/js/admin.js:286 static/admin/js/admin.js.c:354 +msgid "No more identity to show" +msgstr "" + +#: static/admin/js/admin.js:328 +msgid "Do not delete mailbox directory" +msgstr "" + +#: static/admin/js/admin.js:357 +msgid "No more quota to show" +msgstr "" diff --git a/modoboa/admin/locale/es/LC_MESSAGES/django.po b/modoboa/admin/locale/es/LC_MESSAGES/django.po new file mode 100644 index 000000000..a9d683d58 --- /dev/null +++ b/modoboa/admin/locale/es/LC_MESSAGES/django.po @@ -0,0 +1,899 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Pedro Gracia , 2011-2013 +# Pedro Gracia , 2010-2011 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: 2013-10-01 09:56+0000\n" +"Last-Translator: Pedro Gracia \n" +"Language-Team: Spanish (http://www.transifex.com/projects/p/modoboa/language/" +"es/)\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: admin/app_settings.py:54 admin/templates/admin/editdomainform.html:25 +msgid "Mailboxes" +msgstr "Mailboxes" + +#: admin/app_settings.py:57 +msgid "Handle mailboxes on filesystem" +msgstr "Manejar mailboxes en el sistema de fichero" + +#: admin/app_settings.py:60 +#, fuzzy +msgid "" +"Rename or remove mailboxes on the filesystem when they get renamed or " +"removed within Modoboa" +msgstr "" +"Renombrar o eliminar mailboxes en el sistema de ficheros cuando sean " +"renombrados o eliminados dentro de Modoboa" + +#: admin/app_settings.py:66 +msgid "Mailboxes owner" +msgstr "Propietario de los mailboxes" + +#: admin/app_settings.py:69 +msgid "The UNIX account who owns mailboxes on the filesystem" +msgstr "Cuenta de UNIX que posee los mailboxes en el sistema de ficheros" + +#: admin/app_settings.py:74 +#, fuzzy +msgid "Default domain quota" +msgstr "Límites por defecto" + +#: admin/app_settings.py:77 +msgid "" +"Default quota (in MB) applied to freshly created domains with no value " +"specified. A value of 0 means no quota." +msgstr "" + +#: admin/app_settings.py:83 +msgid "Automatic account removal" +msgstr "Elminar cuentas automáticamente" + +#: admin/app_settings.py:86 +msgid "When a mailbox is removed, also remove the associated account" +msgstr "Cuando un mailbox es eliminado, también lo es su cuenta asociada." + +#: admin/app_settings.py:90 +#, fuzzy +msgid "Automatic domain/mailbox creation" +msgstr "Dominio creado" + +#: admin/app_settings.py:93 +msgid "Create a domain and a mailbox when an account is automatically created." +msgstr "" + +#: admin/app_settings.py:139 admin/forms/account.py:239 +#: admin/forms/domain.py:77 +msgid "Must be a positive integer" +msgstr "" + +#: admin/app_settings.py:149 +#, fuzzy +msgid "Administration" +msgstr "Administradores" + +#: admin/callbacks.py:46 +#, fuzzy +#| msgid "Create a domain administrator" +msgid "Domain administrator" +msgstr "Crear un administrador del dominio" + +#: admin/callbacks.py:75 admin/templates/admin/domain_statistics.html:30 +#: admin/templates/admin/domains.html:3 +msgid "Domains" +msgstr "Dominios" + +#: admin/callbacks.py:85 admin/templates/admin/domain_statistics.html:36 +#: admin/templates/admin/domain_statistics.html:50 +#: admin/templates/admin/identities.html:5 +msgid "Identities" +msgstr "Identidades" + +#: admin/callbacks.py:100 admin/templates/admin/forward.html:2 +msgid "Forward" +msgstr "Siguiente" + +#: admin/callbacks.py:163 +#, python-format +msgid "Account import failed (%s): domain does not exist" +msgstr "Falló la importación de la cuenta (%s): el dominio no existe" + +#: admin/callbacks.py:173 admin/forms/account.py:257 +#, python-format +msgid "Mailbox %s already exists" +msgstr "El mailbox %s ya existe" + +#: admin/callbacks.py:181 +#, fuzzy, python-format +msgid "Account import failed (%s): wrong quota value" +msgstr "Falló la importación de la cuenta (%s): el dominio no existe" + +#: admin/callbacks.py:216 admin/forms/domain.py:94 +#, fuzzy +msgid "domain alias" +msgstr "Alias del dominio" + +#: admin/forms/account.py:30 admin/templates/admin/editdomainform.html:13 +msgid "Username" +msgstr "Nombre de usuario" + +#: admin/forms/account.py:32 +#, fuzzy +msgid "" +"The user's name. Must be a valid e-mail address for simple users or " +"administrators with a mailbox." +msgstr "El usuario normal '%s' debe tener una dirección de email válida" + +#: admin/forms/account.py:37 +msgid "Role" +msgstr "Rol" + +#: admin/forms/account.py:38 admin/forms/account.py:75 +msgid "Choose" +msgstr "Elija" + +#: admin/forms/account.py:39 +msgid "What level of permission this user will have" +msgstr "Qué nivel de permiso tendrá este usuario" + +#: admin/forms/account.py:42 +msgid "Password" +msgstr "Contraseña" + +#: admin/forms/account.py:45 +msgid "Confirmation" +msgstr "Confirmación" + +#: admin/forms/account.py:48 +msgid "Enter the same password as above, for verification." +msgstr "Introduzca la misma contraseña que arriba, para verificación." + +#: admin/forms/account.py:66 +msgid "Enabled" +msgstr "Activado" + +#: admin/forms/account.py:141 +msgid "Only super administrators are allowed for this mode" +msgstr "" + +#: admin/forms/account.py:150 +msgid "The two password fields didn't match." +msgstr "Las contraseñas no concuerdan" + +#: admin/forms/account.py:156 +msgid "You can't disable your own account" +msgstr "No puede deshabilitar su propia cuenta" + +#: admin/forms/account.py:170 +msgid "E-mail" +msgstr "Email" + +#: admin/forms/account.py:172 admin/forms/domain.py:35 +msgid "Quota" +msgstr "Cuota" + +#: admin/forms/account.py:174 +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 "" +"Cuota en MB para este mailbox. Defina un valor por defecto o use el del " +"dominio por defecto. Déjelo vacío para definir un valor ilimitado (no " +"permitido para los administradores del dominio)" + +#: admin/forms/account.py:182 admin/forms/domain.py:43 +#: admin/templates/admin/domains.html:26 +msgid "Alias(es)" +msgstr "Alias" + +#: admin/forms/account.py:185 +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 de este mailbox. Indique sólo una dirección por entrada, presione " +"ENTER para añadir una nueva entrada. Use el carácter '*' para crear una " +"dirección 'catchall' (recogerlos todos, ej. *@domain.tld)" + +#: admin/forms/account.py:248 admin/forms/account.py:279 +#: admin/forms/alias.py:72 +msgid "Domain does not exist" +msgstr "El dominio no existe" + +#: admin/forms/account.py:355 +msgid "Domain(s)" +msgstr "Dominio(s)" + +#: admin/forms/account.py:357 +msgid "Domain(s) that user administrates" +msgstr "Dominio(s) que el usuario administra" + +#: admin/forms/account.py:401 admin/forms/account.py:487 +#: admin/forms/domain.py:300 admin/forms/domain.py:370 +msgid "General" +msgstr "General" + +#: admin/forms/account.py:406 admin/forms/account.py:493 +msgid "Mail" +msgstr "Correo" + +#: admin/forms/account.py:409 +msgid "Permissions" +msgstr "Permisos" + +#: admin/forms/account.py:476 +msgid "Account updated" +msgstr "Cuenta actualizada" + +#: admin/forms/account.py:500 +msgid "New account" +msgstr "Nueva cuenta" + +#: admin/forms/account.py:512 +msgid "Account created" +msgstr "Cuenta creada" + +#: admin/forms/alias.py:23 +msgid "Email address" +msgstr "Dirección de email" + +#: admin/forms/alias.py:25 +#, fuzzy +#| msgid "" +#| "The alias address (without the domain part). For a 'catch-all' address, " +#| "just enter an * character." +msgid "" +"The alias address. To create a catchall alias, just enter the domain name " +"(@domain.tld)." +msgstr "" +"La dirección del alias (sin la parte del dominio). Para una dirección que lo " +"recoja todo, ponga un asterisco (*)." + +#: admin/forms/alias.py:31 +msgid "Recipients" +msgstr "Destinatarios" + +#: admin/forms/alias.py:33 +#, fuzzy +#| msgid "" +#| "Mailbox(es) this alias will point to. Indicate only one address per " +#| "input, press ENTER to add a new input." +msgid "" +"Addresses this alias will point to. Indicate only one address per input, " +"press ENTER to add a new input." +msgstr "" +"Mailbox(es) a los que apuntará este alias. Indique únicamente una dirección " +"por entrada, presione ENTER para añadir una nueva entrada." + +#: admin/forms/alias.py:75 +msgid "You don't have access to this domain" +msgstr "No tienes acceso a este dominio" + +#: admin/forms/alias.py:85 +msgid "No recipient defined" +msgstr "No ha puesto destinatario" + +#: admin/forms/domain.py:22 admin/models/domain.py:76 +#: admin/templates/admin/domain_statistics.html:49 +#, fuzzy +msgid "Domain" +msgstr "Dominios" + +#: admin/forms/domain.py:32 admin/templates/admin/domain_statistics.html:26 +msgid "Type" +msgstr "" + +#: admin/forms/domain.py:38 +#, fuzzy +msgid "" +"Default quota in MB applied to mailboxes. Leave empty to use the default " +"value." +msgstr "Cuota por defecto en MB que se aplicará a los mailboxes" + +#: admin/forms/domain.py:46 +msgid "" +"Alias(es) of this domain. Indicate only one name per input, press ENTER to " +"add a new input." +msgstr "" +"Alia(s) de este dominio. Indique únicamente un nombre por entrada, presione " +"ENTER para añadir una nueva entrada." + +#: admin/forms/domain.py:97 admin/forms/domain.py:108 +#: admin/forms/domain.py:116 +#, fuzzy, python-format +msgid "A %s with this name already exists" +msgstr "Ya existe un alias con este nombre" + +#: admin/forms/domain.py:108 admin/forms/domain.py:113 +#, fuzzy +msgid "domain" +msgstr "Dominios" + +#: admin/forms/domain.py:200 +msgid "Create a domain administrator" +msgstr "Crear un administrador del dominio" + +#: admin/forms/domain.py:203 +msgid "Automatically create an administrator for this domain" +msgstr "Automáticamente crear una administrador para este dominio" + +#: admin/forms/domain.py:208 admin/templates/admin/domains.html:25 +#: admin/templates/admin/editdomainform.html:13 +#: admin/templates/admin/identity_headers.html:2 +msgid "Name" +msgstr "Nombre" + +#: admin/forms/domain.py:211 +msgid "" +"The administrator's name. Don't include the domain's name here, it will be " +"automatically appended." +msgstr "" +"El nombre del administrador. No incluya el nombre del dominio aquí, será " +"añadido automáticamente." + +#: admin/forms/domain.py:218 +msgid "Create aliases" +msgstr "Crear alias" + +#: admin/forms/domain.py:221 +msgid "Automatically create standard aliases for this domain" +msgstr "Automáticamente crear alias estándar para este dominio" + +#: admin/forms/domain.py:240 +msgid "Invalid format" +msgstr "Formato inválido" + +#: admin/forms/domain.py:257 +#, python-format +msgid "User '%s' already exists" +msgstr "El usuario '%s' ya existe" + +#: admin/forms/domain.py:359 +msgid "Domain modified" +msgstr "Dominio modificado" + +#: admin/forms/domain.py:379 +msgid "Options" +msgstr "Opciones" + +#: admin/forms/domain.py:387 +msgid "New domain" +msgstr "Nuevo dominio" + +#: admin/forms/domain.py:405 +msgid "Domain created" +msgstr "Dominio creado" + +#: admin/forms/export.py:7 +msgid "File name" +msgstr "Nombre del fichero" + +#: admin/forms/export.py:13 admin/forms/import_.py:13 +msgid "Separator" +msgstr "Separador" + +#: admin/forms/forward.py:14 +msgid "Recipient(s)" +msgstr "Destinatario(s)" + +#: admin/forms/forward.py:18 +msgid "Indicate one or more recipients separated by a ','" +msgstr "Indique uno o más destinatarios separados por una ','" + +#: admin/forms/forward.py:21 +msgid "Keep local copies" +msgstr "Mantener copias locales" + +#: admin/forms/forward.py:24 +msgid "Forward messages and store copies into your local mailbox" +msgstr "Reenvía mensajes y guarda copias en su mailbox local" + +#: admin/forms/forward.py:42 +msgid "" +"You can't define a forward to a local destination. Please ask your " +"administrator to create an alias instead." +msgstr "" +"No puede definir un reenvío a un destino local. Por favor, pregunte a su " +"administrador para crear un alias en su lugar." + +#: admin/forms/import_.py:11 +msgid "Select a file" +msgstr "Seleccione un fichero" + +#: admin/forms/import_.py:19 +msgid "Continue on error" +msgstr "Continúe en error." + +#: admin/forms/import_.py:20 +msgid "Don't treat duplicated objects as error" +msgstr "No tratar objetos duplicados como error" + +#: admin/forms/import_.py:38 +msgid "Crypt passwords" +msgstr "Contraseñas cifradas" + +#: admin/forms/import_.py:40 +msgid "Check this option if passwords contained in your file are not crypted" +msgstr "" +"Marque esta opción si las contraseñas que contiene su fichero no están " +"cifradas" + +#: admin/lib.py:30 +#, fuzzy +msgid "A mailbox is required" +msgstr "Una cuota es obligatoria" + +#: admin/lib.py:120 +#, fuzzy +#| msgid "You can't delete your own domain" +msgid "You are not allowed to import domains" +msgstr "No puedo eliminar su propio dominio" + +#: admin/lib.py:129 +msgid "You are not allowed to import domain aliases." +msgstr "" + +#: admin/models/alias.py:26 admin/models/mailbox.py:77 +msgid "address" +msgstr "dirección" + +#: admin/models/alias.py:28 +#, fuzzy +#| msgid "The alias name" +msgid "The alias address." +msgstr "Nombre del alias" + +#: admin/models/alias.py:33 admin/models/domain.py:37 +#: admin/models/domain_alias.py:24 +msgid "enabled" +msgstr "activado" + +#: admin/models/alias.py:34 admin/models/domain_alias.py:25 +msgid "Check to activate this alias" +msgstr "Marcar para activar este alias" + +#: admin/models/alias.py:79 +msgid "distribution list" +msgstr "Lista de distribución" + +#: admin/models/alias.py:80 +msgid "forward" +msgstr "reenvío" + +#: admin/models/alias.py:81 +msgid "alias" +msgstr "alias" + +#: admin/models/alias.py:112 +#, fuzzy +#| msgid "Invalid request" +msgid "Invalid address" +msgstr "Petición inválida" + +#: admin/models/alias.py:133 +#, fuzzy +#| msgid "Local recipient %s not found" +msgid "Local recipient {}@{} not found" +msgstr "Destinatario local %s no encontrado" + +#: admin/models/alias.py:162 +#, python-format +msgid "Invalid line: %s" +msgstr "Línea inválida: %s" + +#: admin/models/alias.py:168 +#, python-format +msgid "Domain '%s' does not exist" +msgstr "El dominio '%s' no existe" + +#: admin/models/domain.py:33 admin/models/domain_alias.py:17 +msgid "name" +msgstr "nombre" + +#: admin/models/domain.py:34 +msgid "The domain name" +msgstr "Nombre del dominio" + +#: admin/models/domain.py:38 +msgid "Check to activate this domain" +msgstr "Marcar para activar este dominio" + +#: admin/models/domain.py:160 admin/models/domain_alias.py:47 +msgid "Invalid line" +msgstr "Línea inválida" + +#: admin/models/domain.py:168 +#, python-format +msgid "Invalid quota value for domain '%s'" +msgstr "" + +#: admin/models/domain_alias.py:18 +msgid "The alias name" +msgstr "Nombre del alias" + +#: admin/models/domain_alias.py:20 +msgid "target" +msgstr "objetivo" + +#: admin/models/domain_alias.py:21 +msgid "The domain this alias points to" +msgstr "El dominio al que este alias apunta" + +#: admin/models/domain_alias.py:56 +#, python-format +msgid "Unknown domain %s" +msgstr "Dominio desconocido %s" + +#: admin/models/mailbox.py:79 +msgid "Mailbox address (without the @domain.tld part)" +msgstr "Dirección del mailbox (sin la parte @dominio.tld)" + +#: admin/models/mailbox.py:144 +#, python-format +msgid "Failed to retrieve mailbox location (%s)" +msgstr "Fallo al recuperar la localización del mailbox (%s)" + +#: admin/models/mailbox.py:233 +#, python-format +msgid "Quota is greater than the allowed domain's limit (%dM)" +msgstr "La cuota es mayor que el límite permitido para el dominio (%dM)" + +#: admin/models/mailbox.py:239 +msgid "A quota is required" +msgstr "Una cuota es obligatoria" + +#: admin/templates/admin/account_general_form.html:11 +#: admin/templates/admin/aliasform.html:8 +#, fuzzy +msgid "Domain is disabled" +msgstr "Dominio eliminado" + +#: admin/templates/admin/domain_general_form.html:4 +#: admin/templates/admin/mailform.html:3 +#: admin/templates/admin/newdomainform.html:3 +msgid "MB" +msgstr "MB" + +#: admin/templates/admin/domain_options_form.html:7 +#, fuzzy +msgid "You can't create a new domain administrator" +msgstr "Crear un administrador del dominio" + +#: admin/templates/admin/domain_statistics.html:15 +#: admin/templatetags/admin_tags.py:45 +msgid "Statistics" +msgstr "Estadísticas" + +#: admin/templates/admin/domain_statistics.html:15 +#, fuzzy +msgid "Domains and identities" +msgstr "Listar identidades" + +#: admin/templates/admin/domain_statistics.html:22 +msgid "Global counters" +msgstr "" + +#: admin/templates/admin/domain_statistics.html:27 +#, fuzzy +msgid "Quantity" +msgstr "Cuarentena" + +#: admin/templates/admin/domain_statistics.html:33 +#: admin/templates/admin/editdomainform.html:24 +msgid "Domain aliases" +msgstr "Alias del dominio" + +#: admin/templates/admin/domain_statistics.html:45 +msgid "Per-domain counters" +msgstr "" + +#: admin/templates/admin/domains.html:27 +#: admin/templates/admin/identity_headers.html:4 +msgid "Tags" +msgstr "Tags" + +#: admin/templates/admin/domains.html:28 +#: admin/templates/admin/identity_headers.html:5 +msgid "Actions" +msgstr "Acciones" + +#: admin/templates/admin/domains_table.html:4 +#, fuzzy +msgid "disabled" +msgstr "activado" + +#: admin/templates/admin/editdomainform.html:4 +msgid "Administrators" +msgstr "Administradores" + +#: admin/templates/admin/editdomainform.html:7 +msgid "Miscellaneous" +msgstr "Otros" + +#: admin/templates/admin/editdomainform.html:19 +msgid "No domain administrator defined" +msgstr "No hay un dominio de administrador definido " + +#: admin/templates/admin/editdomainform.html:22 +msgid "Creation date" +msgstr "Fecha de creación" + +#: admin/templates/admin/editdomainform.html:23 +msgid "Last modification date" +msgstr "Última fecha de modificación" + +#: admin/templates/admin/editdomainform.html:26 +msgid "Mailbox aliases" +msgstr "Alias del mailbox" + +#: admin/templates/admin/forward.html:2 +msgid "Automatically forward emails to another address" +msgstr "Reenvía automáticamente los correos a otra dirección" + +#: admin/templates/admin/forward.html:9 admin/views/alias.py:113 +msgid "Update" +msgstr "Actualizar" + +#: admin/templates/admin/identity_headers.html:3 +msgid "Fullname/Recipient" +msgstr "Nombre completo/Destinatario" + +#: admin/templates/admin/importform.html:6 +msgid "Help" +msgstr "Ayuda" + +#: admin/templates/admin/importform.html:13 +msgid "Importing..." +msgstr "Importando..." + +#: admin/templates/admin/mailboxes_raw.html:3 +msgid "Choose a value" +msgstr "Elija un valor" + +#: admin/templates/admin/mailform.html:3 +msgid "Use domain default value" +msgstr "Usar el valor por defecto del dominio" + +#: admin/templates/admin/quota_headers.html:2 +msgid "Address" +msgstr "Dirección" + +#: admin/templates/admin/quota_headers.html:3 +msgid "Value" +msgstr "Valor" + +#: admin/templates/admin/quota_headers.html:4 +msgid "Limit" +msgstr "Límite" + +#: admin/templates/admin/quota_headers.html:5 +#, python-format +msgid "Usage (%%)" +msgstr "Uso (%%)" + +#: admin/templates/admin/searchform.html:6 +msgid "Search" +msgstr "Buscar" + +#: admin/templatetags/admin_tags.py:19 +msgid "enabled_m" +msgstr "Activado" + +#: admin/templatetags/admin_tags.py:19 +msgid "enabled_f" +msgstr "Activado" + +#: admin/templatetags/admin_tags.py:40 +#, fuzzy +msgid "List domains" +msgstr "Editar el dominio" + +#: admin/templatetags/admin_tags.py:53 +msgid "Add domain" +msgstr "Añadir dominio" + +#: admin/templatetags/admin_tags.py:62 admin/templatetags/admin_tags.py:126 +#: admin/views/import_.py:99 admin/views/import_.py:133 +msgid "Import" +msgstr "Importar" + +#: admin/templatetags/admin_tags.py:68 admin/templatetags/admin_tags.py:132 +#: admin/views/export.py:41 admin/views/export.py:69 +msgid "Export" +msgstr "Exportar" + +#: admin/templatetags/admin_tags.py:92 +msgid "List identities" +msgstr "Listar identidades" + +#: admin/templatetags/admin_tags.py:97 +msgid "List quotas" +msgstr "Listar cuotas" + +#: admin/templatetags/admin_tags.py:102 +msgid "Add account" +msgstr "Añadir cuenta" + +#: admin/templatetags/admin_tags.py:108 +msgid "Add alias" +msgstr "Añadir alias" + +#: admin/templatetags/admin_tags.py:114 +msgid "Add forward" +msgstr "Añadir reenvío" + +#: admin/templatetags/admin_tags.py:120 +msgid "Add distribution list" +msgstr "Añadir lista de distribución" + +#: admin/templatetags/admin_tags.py:152 +msgid "View the domain's identities" +msgstr "Ver las identidades del dominio" + +#: admin/templatetags/admin_tags.py:159 admin/templatetags/admin_tags.py:182 +#: admin/templatetags/admin_tags.py:190 +#, python-format +msgid "Delete %s?" +msgstr "¿Eliminar %s?" + +#: admin/templatetags/admin_tags.py:267 +msgid "Remove this permission" +msgstr "Eliminar este permiso" + +#: admin/views/alias.py:29 +msgid "Alias with this name already exists" +msgstr "Ya existe un alias con este nombre" + +#: admin/views/alias.py:53 +msgid "Create" +msgstr "Crear" + +#: admin/views/alias.py:65 +msgid "New distribution list" +msgstr "Nueva lista de distribución" + +#: admin/views/alias.py:67 +msgid "Distribution list created" +msgstr "Lista de distribución creada" + +#: admin/views/alias.py:76 +msgid "New alias" +msgstr "Nuevo alias" + +#: admin/views/alias.py:77 +msgid "Alias created" +msgstr "Alias creado" + +#: admin/views/alias.py:86 +msgid "New forward" +msgstr "Nuevo reenvío" + +#: admin/views/alias.py:87 +msgid "Forward created" +msgstr "Reenvío creado" + +#: admin/views/alias.py:101 +msgid "Distribution list modified" +msgstr "Lista de distribución modificada" + +#: admin/views/alias.py:103 +msgid "Forward modified" +msgstr "Reenvío modificado" + +#: admin/views/alias.py:105 +msgid "Alias modified" +msgstr "Alias modificado" + +#: admin/views/domain.py:132 +msgid "You can't delete your own domain" +msgstr "No puedo eliminar su propio dominio" + +#: admin/views/domain.py:135 +msgid "Domain deleted" +msgid_plural "Domains deleted" +msgstr[0] "Dominio eliminado" +msgstr[1] "Dominios eliminados" + +#: admin/views/export.py:40 +msgid "Export identities" +msgstr "Exportar identidades" + +#: admin/views/export.py:68 +msgid "Export domains" +msgstr "Exportar dominios" + +#: admin/views/identity.py:108 admin/views/identity.py:200 +#: admin/views/identity.py:205 +msgid "Invalid request" +msgstr "Petición inválida" + +#: admin/views/identity.py:190 +msgid "Account deleted" +msgid_plural "Accounts deleted" +msgstr[0] "Cuenta eliminada" +msgstr[1] "Cuentas eliminadas" + +#: admin/views/import_.py:64 +#, python-format +msgid "Object already exists: %s" +msgstr "El objeto ya existe: %s" + +#: admin/views/import_.py:68 +#, python-format +msgid "%d objects imported successfully" +msgstr "%d objetos importados con éxito" + +#: admin/views/import_.py:86 +#, fuzzy, python-format +msgid "" +"Provide a CSV file where lines respect one of the following formats:\n" +"
    \n" +"
  • domain; name; quota; enabled
  • \n" +"
  • domainalias; name; targeted domain; enabled
  • \n" +" %s\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 "" +"Provea un fichero CSV dónde las líneas respetan uno de los siguientes " +"formatos:⏎
    ⏎
  • dominio; nombre; cuota; activado
  • ⏎ " +"
  • aliasdeldominio; nombre; dominio objetivo; activado
  • ⏎ ⏎

    El primer elemento de cada línea es obligatorio y debe ser igual a " +"uno de los valores anteriores.

    ⏎

    Puede usar un carácter diferente como " +"separador.

    ⏎\n" + +#: admin/views/import_.py:98 +msgid "Import domains" +msgstr "Importar dominios" + +#: admin/views/import_.py:120 +#, fuzzy +msgid "" +"Provide a CSV file where lines respect one of the following formats:\n" +"
      \n" +"
    • account; loginname; password; first name; last name; enabled; group; " +"address; quota; [, domain, ...]
    • \n" +"
    • alias; address; enabled; internal recipient
    • \n" +"
    • forward; address; enabled; external recipient
    • \n" +"
    • dlist; address; enabled; recipient; recipient; ...
    • \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 "" +"Importe un fichero CSV dónde las líneas respetan uno de los siguientes " +"formatos:\n" +"
      \n" +"
    • account; loginname; password; first name; last name; enabled; group; " +"address[, domain, ...]
    • ⏎\n" +"
    • alias; address; enabled; internal recipient
    • ⏎\n" +"
    • forward; address; enabled; external recipient
    • ⏎\n" +"
    • dlist; address; enabled; recipient; recipient; ...
    • ⏎\n" +"
    ⏎\n" +"

    El primer elemento de cada línea es obligatorio y debe ser igual al de " +"los valores previos.

    \n" +"\n" +"

    Puede usar un carácter diferente como separador.

    \n" + +#: admin/views/import_.py:132 +msgid "Import identities" +msgstr "Importar identidades" + +#: admin/views/user.py:34 +msgid "Forward updated" +msgstr "Reenvío actualizado" diff --git a/modoboa/admin/locale/es/LC_MESSAGES/djangojs.po b/modoboa/admin/locale/es/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..df9e7317a --- /dev/null +++ b/modoboa/admin/locale/es/LC_MESSAGES/djangojs.po @@ -0,0 +1,57 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Pedro Gracia , 2011,2013 +# Pedro Gracia , 2010, 2011 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 08:20+0100\n" +"PO-Revision-Date: 2013-06-03 16:32+0000\n" +"Last-Translator: Pedro Gracia \n" +"Language-Team: Spanish (http://www.transifex.com/projects/p/modoboa/language/" +"es/)\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: static/admin/js/admin.js:126 +msgid "No more domain to show" +msgstr "" + +#: static/admin/js/admin.js:145 +msgid "Do not delete domain directory" +msgstr "No eleminar el directorio del dominio" + +#: static/admin/js/admin.js:147 +msgid "This operation will remove ALL data associated to this domain." +msgstr "Esta operación eliminará TODOS los datos asociados a este dominio." + +#: static/admin/js/admin.js:148 +msgid "" +"This operation will remove all data associated to this domain, excepting " +"accounts." +msgstr "" +"Esta operación eliminará todos los datos asociados a este dominio, excepto " +"las cuentas." + +#: static/admin/js/admin.js:231 +msgid "No domain administrator defined" +msgstr "No hay un administrador de dominio definido" + +#: static/admin/js/admin.js:286 static/admin/js/admin.js.c:354 +msgid "No more identity to show" +msgstr "" + +#: static/admin/js/admin.js:328 +msgid "Do not delete mailbox directory" +msgstr "No eliminar el directorio del mailbox" + +#: static/admin/js/admin.js:357 +msgid "No more quota to show" +msgstr "" diff --git a/modoboa/admin/locale/fr/LC_MESSAGES/django.po b/modoboa/admin/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 000000000..44cb91040 --- /dev/null +++ b/modoboa/admin/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,874 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Antoine Nguyen , 2015 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: 2015-09-09 08:33+0000\n" +"Last-Translator: Antoine Nguyen \n" +"Language-Team: French (http://www.transifex.com/tonio/modoboa/language/fr/)\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: admin/app_settings.py:54 admin/templates/admin/editdomainform.html:25 +msgid "Mailboxes" +msgstr "Boites" + +#: admin/app_settings.py:57 +msgid "Handle mailboxes on filesystem" +msgstr "Gérer les boites sur le système de fichiers" + +#: admin/app_settings.py:60 +msgid "" +"Rename or remove mailboxes on the filesystem when they get renamed or " +"removed within Modoboa" +msgstr "" +"Renommer ou supprimer les boites sur le système de fichiers quand elles sont " +"renommées ou supprimées dans Modoboa" + +#: admin/app_settings.py:66 +msgid "Mailboxes owner" +msgstr "Propriétaire des boites" + +#: admin/app_settings.py:69 +msgid "The UNIX account who owns mailboxes on the filesystem" +msgstr "Le compte UNIX qui possède les boites sur le système de fichiers" + +#: admin/app_settings.py:74 +msgid "Default domain quota" +msgstr "Quota de domaine par défaut" + +#: admin/app_settings.py:77 +msgid "" +"Default quota (in MB) applied to freshly created domains with no value " +"specified. A value of 0 means no quota." +msgstr "" +"Quota par défaut (en Mo) appliqué aux domaines fraichement créés et ne " +"spécifiant pas de valeur. 0 signifie qu'aucun quota ne sera appliqué." + +#: admin/app_settings.py:83 +msgid "Automatic account removal" +msgstr "Suppression automatique de compte" + +#: admin/app_settings.py:86 +msgid "When a mailbox is removed, also remove the associated account" +msgstr "" +"Lorsqu'une boite aux lettres est supprimée, supprimer le compte associé en " +"même temps" + +#: admin/app_settings.py:90 +msgid "Automatic domain/mailbox creation" +msgstr "Création automatique de domaine/boite aux lettres" + +#: admin/app_settings.py:93 +msgid "Create a domain and a mailbox when an account is automatically created." +msgstr "" +"Créer un domaine et une boite aux lettres quand un compte est créé " +"automatiquement." + +#: admin/app_settings.py:139 admin/forms/account.py:239 +#: admin/forms/domain.py:77 +msgid "Must be a positive integer" +msgstr "Doit être un entier positif" + +#: admin/app_settings.py:149 +msgid "Administration" +msgstr "Administration" + +#: admin/callbacks.py:46 +msgid "Domain administrator" +msgstr "Administrateur de domaine" + +#: admin/callbacks.py:75 admin/templates/admin/domain_statistics.html:30 +#: admin/templates/admin/domains.html:3 +msgid "Domains" +msgstr "Domaines" + +#: admin/callbacks.py:85 admin/templates/admin/domain_statistics.html:36 +#: admin/templates/admin/domain_statistics.html:50 +#: admin/templates/admin/identities.html:5 +msgid "Identities" +msgstr "Identités" + +#: admin/callbacks.py:100 admin/templates/admin/forward.html:2 +msgid "Forward" +msgstr "Transfert" + +#: admin/callbacks.py:163 +#, python-format +msgid "Account import failed (%s): domain does not exist" +msgstr "L'import du compte a échoué (%s): le domaine n'existe pas" + +#: admin/callbacks.py:173 admin/forms/account.py:257 +#, python-format +msgid "Mailbox %s already exists" +msgstr "La boite %s existe déjà" + +#: admin/callbacks.py:181 +#, python-format +msgid "Account import failed (%s): wrong quota value" +msgstr "L'import du compte a échoué (%s): mauvais quota" + +#: admin/callbacks.py:216 admin/forms/domain.py:94 +msgid "domain alias" +msgstr "alias de domaine" + +#: admin/forms/account.py:30 admin/templates/admin/editdomainform.html:13 +msgid "Username" +msgstr "Nom d'utilisateur" + +#: admin/forms/account.py:32 +msgid "" +"The user's name. Must be a valid e-mail address for simple users or " +"administrators with a mailbox." +msgstr "" +"Le nom de l'utilisateur. Doit être une adresse e-mail valide pour les " +"utilisateurs simples ou les administrateurs avec une boite aux lettres." + +#: admin/forms/account.py:37 +msgid "Role" +msgstr "Rôle" + +#: admin/forms/account.py:38 admin/forms/account.py:75 +msgid "Choose" +msgstr "Choisir" + +#: admin/forms/account.py:39 +msgid "What level of permission this user will have" +msgstr "Quel niveau de permission cet utilisateur aura" + +#: admin/forms/account.py:42 +msgid "Password" +msgstr "Mot de passe" + +#: admin/forms/account.py:45 +msgid "Confirmation" +msgstr "Confirmation" + +#: admin/forms/account.py:48 +msgid "Enter the same password as above, for verification." +msgstr "Entrer le même mot de passe, pour vérification." + +#: admin/forms/account.py:66 +msgid "Enabled" +msgstr "Activé" + +#: admin/forms/account.py:141 +msgid "Only super administrators are allowed for this mode" +msgstr "Seuls les super administrateurs sont autorisés pour ce mode" + +#: admin/forms/account.py:150 +msgid "The two password fields didn't match." +msgstr "Les deux mots de passe ne correspondent pas." + +#: admin/forms/account.py:156 +msgid "You can't disable your own account" +msgstr "Vous ne pouvez pas désactiver votre propre compte" + +#: admin/forms/account.py:170 +msgid "E-mail" +msgstr "E-mail" + +#: admin/forms/account.py:172 admin/forms/domain.py:35 +msgid "Quota" +msgstr "Quota" + +#: admin/forms/account.py:174 +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 en Mo pour cette boite. Définir une valeur spécifique ou utiliser la " +"valeur par défaut du domaine. Laisser vide pour définir une valeur illimitée " +"(interdit pour les admins. de domaine)." + +#: admin/forms/account.py:182 admin/forms/domain.py:43 +#: admin/templates/admin/domains.html:26 +msgid "Alias(es)" +msgstr "Alias" + +#: admin/forms/account.py:185 +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 de cette boite. Indiquer seulement une adresse par champ, appuyer sur " +"ENTREE pour ajouter un nouveau champ. Utiliser le caractère '*' pour créer " +"une adresse 'attrape tout' (ex: *@domain.tld)." + +#: admin/forms/account.py:248 admin/forms/account.py:279 +#: admin/forms/alias.py:72 +msgid "Domain does not exist" +msgstr "Domaine inexistant" + +#: admin/forms/account.py:355 +msgid "Domain(s)" +msgstr "Domaine(s)" + +#: admin/forms/account.py:357 +msgid "Domain(s) that user administrates" +msgstr "Domaine(s) que cet utilisateur administre" + +#: admin/forms/account.py:401 admin/forms/account.py:487 +#: admin/forms/domain.py:300 admin/forms/domain.py:370 +msgid "General" +msgstr "Général" + +#: admin/forms/account.py:406 admin/forms/account.py:493 +msgid "Mail" +msgstr "Courrier" + +#: admin/forms/account.py:409 +msgid "Permissions" +msgstr "Permissions" + +#: admin/forms/account.py:476 +msgid "Account updated" +msgstr "Compte mis à jour" + +#: admin/forms/account.py:500 +msgid "New account" +msgstr "Nouveau compte" + +#: admin/forms/account.py:512 +msgid "Account created" +msgstr "Compte créé" + +#: admin/forms/alias.py:23 +msgid "Email address" +msgstr "Adresse email" + +#: admin/forms/alias.py:25 +msgid "" +"The alias address. To create a catchall alias, just enter the domain name " +"(@domain.tld)." +msgstr "" +"L'adresse de l'alias. Pour créer un alias \"fourre-tout\", saisissez " +"uniquement le nom du domaine (@domain.tld)." + +#: admin/forms/alias.py:31 +msgid "Recipients" +msgstr "Destinataires" + +#: admin/forms/alias.py:33 +msgid "" +"Addresses this alias will point to. Indicate only one address per input, " +"press ENTER to add a new input." +msgstr "" +"Les adresses vers lesquelles cet alias pointera. Indiquer une adresse par " +"champ, appuyer sur ENTREE pour ajouter un nouveau champ." + +#: admin/forms/alias.py:75 +msgid "You don't have access to this domain" +msgstr "Vous n'avez pas accès à ce domaine" + +#: admin/forms/alias.py:85 +msgid "No recipient defined" +msgstr "Aucune destinataire défini" + +#: admin/forms/domain.py:22 admin/models/domain.py:76 +#: admin/templates/admin/domain_statistics.html:49 +msgid "Domain" +msgstr "Domaine" + +#: admin/forms/domain.py:32 admin/templates/admin/domain_statistics.html:26 +msgid "Type" +msgstr "Type" + +#: admin/forms/domain.py:38 +msgid "" +"Default quota in MB applied to mailboxes. Leave empty to use the default " +"value." +msgstr "" +"Quota par défaut en Mo appliqué aux boites aux lettres. Laisser vide pour " +"utiliser la valeur par défaut." + +#: admin/forms/domain.py:46 +msgid "" +"Alias(es) of this domain. Indicate only one name per input, press ENTER to " +"add a new input." +msgstr "" +"Alias de ce domaine. Indiquer seulement un nom par champ, appuyer sur ENTREE " +"pour ajouter un nouveau champ." + +#: admin/forms/domain.py:97 admin/forms/domain.py:108 +#: admin/forms/domain.py:116 +#, python-format +msgid "A %s with this name already exists" +msgstr "Un %s de ce nom existe déjà" + +#: admin/forms/domain.py:108 admin/forms/domain.py:113 +msgid "domain" +msgstr "domaine" + +#: admin/forms/domain.py:200 +msgid "Create a domain administrator" +msgstr "Créer un administrateur de domaine" + +#: admin/forms/domain.py:203 +msgid "Automatically create an administrator for this domain" +msgstr "Créer automatiquement un administrateur pour ce domaine" + +#: admin/forms/domain.py:208 admin/templates/admin/domains.html:25 +#: admin/templates/admin/editdomainform.html:13 +#: admin/templates/admin/identity_headers.html:2 +msgid "Name" +msgstr "Nom" + +#: admin/forms/domain.py:211 +msgid "" +"The administrator's name. Don't include the domain's name here, it will be " +"automatically appended." +msgstr "" +"Le nom de l'administrateur. Ne pas include un nom de domaine ici, il sera " +"ajouté automatiquement." + +#: admin/forms/domain.py:218 +msgid "Create aliases" +msgstr "Créer des alias" + +#: admin/forms/domain.py:221 +msgid "Automatically create standard aliases for this domain" +msgstr "Créer automatiquement des alias standard pour ce domaine" + +#: admin/forms/domain.py:240 +msgid "Invalid format" +msgstr "Format invalide" + +#: admin/forms/domain.py:257 +#, python-format +msgid "User '%s' already exists" +msgstr "L'utilisateur '%s' existe déjà" + +#: admin/forms/domain.py:359 +msgid "Domain modified" +msgstr "Domaine modifié" + +#: admin/forms/domain.py:379 +msgid "Options" +msgstr "Options" + +#: admin/forms/domain.py:387 +msgid "New domain" +msgstr "Nouveau domaine" + +#: admin/forms/domain.py:405 +msgid "Domain created" +msgstr "Domaine créé" + +#: admin/forms/export.py:7 +msgid "File name" +msgstr "Nom du fichier" + +#: admin/forms/export.py:13 admin/forms/import_.py:13 +msgid "Separator" +msgstr "Séparateur" + +#: admin/forms/forward.py:14 +msgid "Recipient(s)" +msgstr "Destinataire(s)" + +#: admin/forms/forward.py:18 +msgid "Indicate one or more recipients separated by a ','" +msgstr "Indiquer un ou plusieurs destinataires séparés par une ','" + +#: admin/forms/forward.py:21 +msgid "Keep local copies" +msgstr "Conserver des copies locales" + +#: admin/forms/forward.py:24 +msgid "Forward messages and store copies into your local mailbox" +msgstr "Transférer les messages et conserver une copie dans votre boite locale" + +#: admin/forms/forward.py:42 +msgid "" +"You can't define a forward to a local destination. Please ask your " +"administrator to create an alias instead." +msgstr "" +"Vous ne pouvez pas définir un transfert vers une destination locale. " +"Veuillez plutôt demander à votre administrateur de créer un alias" + +#: admin/forms/import_.py:11 +msgid "Select a file" +msgstr "Sélectionner un fichier" + +#: admin/forms/import_.py:19 +msgid "Continue on error" +msgstr "Continuer en cas d'erreur" + +#: admin/forms/import_.py:20 +msgid "Don't treat duplicated objects as error" +msgstr "Ne pas traiter les objets dupliqués comme des erreurs" + +#: admin/forms/import_.py:38 +msgid "Crypt passwords" +msgstr "Chiffrer les mots de passe" + +#: admin/forms/import_.py:40 +msgid "Check this option if passwords contained in your file are not crypted" +msgstr "" +"Cocher cette option si les mots de passe contenus dans votre fichier ne sont " +"pas chiffrés" + +#: admin/lib.py:30 +msgid "A mailbox is required" +msgstr "Une boite aux lettres est requise" + +#: admin/lib.py:120 +msgid "You are not allowed to import domains" +msgstr "Vous n'êtes pas autorisé à importer des domaines" + +#: admin/lib.py:129 +msgid "You are not allowed to import domain aliases." +msgstr "Vous n'êtes pas autorisé à importer des alias de domaine." + +#: admin/models/alias.py:26 admin/models/mailbox.py:77 +msgid "address" +msgstr "Adresse" + +#: admin/models/alias.py:28 +msgid "The alias address." +msgstr "L'adresse de l'alias" + +#: admin/models/alias.py:33 admin/models/domain.py:37 +#: admin/models/domain_alias.py:24 +msgid "enabled" +msgstr "Activé" + +#: admin/models/alias.py:34 admin/models/domain_alias.py:25 +msgid "Check to activate this alias" +msgstr "Cocher pour activer cet alias" + +#: admin/models/alias.py:79 +msgid "distribution list" +msgstr "liste de distribution" + +#: admin/models/alias.py:80 +msgid "forward" +msgstr "transfert" + +#: admin/models/alias.py:81 +msgid "alias" +msgstr "alias" + +#: admin/models/alias.py:112 +msgid "Invalid address" +msgstr "Adresse invalide" + +#: admin/models/alias.py:133 +msgid "Local recipient {}@{} not found" +msgstr "Destinataire local {}@{} introuvable" + +#: admin/models/alias.py:162 +#, python-format +msgid "Invalid line: %s" +msgstr "Ligne invalide : %s" + +#: admin/models/alias.py:168 +#, python-format +msgid "Domain '%s' does not exist" +msgstr "Domaine '%s' inexistant" + +#: admin/models/domain.py:33 admin/models/domain_alias.py:17 +msgid "name" +msgstr "Nom" + +#: admin/models/domain.py:34 +msgid "The domain name" +msgstr "Le nom du domaine" + +#: admin/models/domain.py:38 +msgid "Check to activate this domain" +msgstr "Cocher pour activer ce domaine" + +#: admin/models/domain.py:160 admin/models/domain_alias.py:47 +msgid "Invalid line" +msgstr "Ligne invalide" + +#: admin/models/domain.py:168 +#, python-format +msgid "Invalid quota value for domain '%s'" +msgstr "Quota invalide pour le domaine '%s'" + +#: admin/models/domain_alias.py:18 +msgid "The alias name" +msgstr "Le nom de l'alias" + +#: admin/models/domain_alias.py:20 +msgid "target" +msgstr "cible" + +#: admin/models/domain_alias.py:21 +msgid "The domain this alias points to" +msgstr "Le domaine vers lequel cet alias pointe" + +#: admin/models/domain_alias.py:56 +#, python-format +msgid "Unknown domain %s" +msgstr "Domaine %s inconnu" + +#: admin/models/mailbox.py:79 +msgid "Mailbox address (without the @domain.tld part)" +msgstr "Adresse de la boite aux lettres (sans la partie @domain.tld)" + +#: admin/models/mailbox.py:144 +#, python-format +msgid "Failed to retrieve mailbox location (%s)" +msgstr "Impossible de récuperer l'emplacement de la boite (%s)" + +#: admin/models/mailbox.py:233 +#, python-format +msgid "Quota is greater than the allowed domain's limit (%dM)" +msgstr "Le quota est supérieur à la limité autorisée pour le domaine (%dM)" + +#: admin/models/mailbox.py:239 +msgid "A quota is required" +msgstr "Un quota est requis" + +#: admin/templates/admin/account_general_form.html:11 +#: admin/templates/admin/aliasform.html:8 +msgid "Domain is disabled" +msgstr "Le domaine est désactivé" + +#: admin/templates/admin/domain_general_form.html:4 +#: admin/templates/admin/mailform.html:3 +#: admin/templates/admin/newdomainform.html:3 +msgid "MB" +msgstr "Mo" + +#: admin/templates/admin/domain_options_form.html:7 +msgid "You can't create a new domain administrator" +msgstr "Vous ne pouvez pas créer de nouvel administrateur de domaine" + +#: admin/templates/admin/domain_statistics.html:15 +#: admin/templatetags/admin_tags.py:45 +msgid "Statistics" +msgstr "Statistiques" + +#: admin/templates/admin/domain_statistics.html:15 +msgid "Domains and identities" +msgstr "Domaines et identités" + +#: admin/templates/admin/domain_statistics.html:22 +msgid "Global counters" +msgstr "Compteurs globaux" + +#: admin/templates/admin/domain_statistics.html:27 +msgid "Quantity" +msgstr "Quantité" + +#: admin/templates/admin/domain_statistics.html:33 +#: admin/templates/admin/editdomainform.html:24 +msgid "Domain aliases" +msgstr "Alias de domaine" + +#: admin/templates/admin/domain_statistics.html:45 +msgid "Per-domain counters" +msgstr "Compteurs par domaine" + +#: admin/templates/admin/domains.html:27 +#: admin/templates/admin/identity_headers.html:4 +msgid "Tags" +msgstr "Tags" + +#: admin/templates/admin/domains.html:28 +#: admin/templates/admin/identity_headers.html:5 +msgid "Actions" +msgstr "Actions" + +#: admin/templates/admin/domains_table.html:4 +msgid "disabled" +msgstr "Desactivé" + +#: admin/templates/admin/editdomainform.html:4 +msgid "Administrators" +msgstr "Administrateurs" + +#: admin/templates/admin/editdomainform.html:7 +msgid "Miscellaneous" +msgstr "Divers" + +#: admin/templates/admin/editdomainform.html:19 +msgid "No domain administrator defined" +msgstr "Aucun administrateur de domaine défini" + +#: admin/templates/admin/editdomainform.html:22 +msgid "Creation date" +msgstr "Date de création" + +#: admin/templates/admin/editdomainform.html:23 +msgid "Last modification date" +msgstr "Date de dernière modification" + +#: admin/templates/admin/editdomainform.html:26 +msgid "Mailbox aliases" +msgstr "Alias de boite." + +#: admin/templates/admin/forward.html:2 +msgid "Automatically forward emails to another address" +msgstr "Transférer automatiquement les messages vers une autre adresse" + +#: admin/templates/admin/forward.html:9 admin/views/alias.py:113 +msgid "Update" +msgstr "Mettre à jour" + +#: admin/templates/admin/identity_headers.html:3 +msgid "Fullname/Recipient" +msgstr "Nom complet/Destinataire" + +#: admin/templates/admin/importform.html:6 +msgid "Help" +msgstr "Aide" + +#: admin/templates/admin/importform.html:13 +msgid "Importing..." +msgstr "Import en cours..." + +#: admin/templates/admin/mailboxes_raw.html:3 +msgid "Choose a value" +msgstr "Choisir une valeur" + +#: admin/templates/admin/mailform.html:3 +msgid "Use domain default value" +msgstr "Utiliser la valeur par défaut du domaine" + +#: admin/templates/admin/quota_headers.html:2 +msgid "Address" +msgstr "Adresse" + +#: admin/templates/admin/quota_headers.html:3 +msgid "Value" +msgstr "Valeur" + +#: admin/templates/admin/quota_headers.html:4 +msgid "Limit" +msgstr "Limite" + +#: admin/templates/admin/quota_headers.html:5 +#, python-format +msgid "Usage (%%)" +msgstr "Utilisation (%%)" + +#: admin/templates/admin/searchform.html:6 +msgid "Search" +msgstr "Chercher" + +#: admin/templatetags/admin_tags.py:19 +msgid "enabled_m" +msgstr "Activé" + +#: admin/templatetags/admin_tags.py:19 +msgid "enabled_f" +msgstr "Activée" + +#: admin/templatetags/admin_tags.py:40 +msgid "List domains" +msgstr "Lister les domaines" + +#: admin/templatetags/admin_tags.py:53 +msgid "Add domain" +msgstr "Ajouter un domaine" + +#: admin/templatetags/admin_tags.py:62 admin/templatetags/admin_tags.py:126 +#: admin/views/import_.py:99 admin/views/import_.py:133 +msgid "Import" +msgstr "Importer" + +#: admin/templatetags/admin_tags.py:68 admin/templatetags/admin_tags.py:132 +#: admin/views/export.py:41 admin/views/export.py:69 +msgid "Export" +msgstr "Exporter" + +#: admin/templatetags/admin_tags.py:92 +msgid "List identities" +msgstr "Lister les identités" + +#: admin/templatetags/admin_tags.py:97 +msgid "List quotas" +msgstr "Lister les quotas" + +#: admin/templatetags/admin_tags.py:102 +msgid "Add account" +msgstr "Ajouter un compte" + +#: admin/templatetags/admin_tags.py:108 +msgid "Add alias" +msgstr "Ajouter un alias" + +#: admin/templatetags/admin_tags.py:114 +msgid "Add forward" +msgstr "Ajouter un transfert" + +#: admin/templatetags/admin_tags.py:120 +msgid "Add distribution list" +msgstr "Ajouter une liste de distribution" + +#: admin/templatetags/admin_tags.py:152 +msgid "View the domain's identities" +msgstr "Voir les identités du domaine" + +#: admin/templatetags/admin_tags.py:159 admin/templatetags/admin_tags.py:182 +#: admin/templatetags/admin_tags.py:190 +#, python-format +msgid "Delete %s?" +msgstr "Supprimer %s ?" + +#: admin/templatetags/admin_tags.py:267 +msgid "Remove this permission" +msgstr "Supprimer cette permission" + +#: admin/views/alias.py:29 +msgid "Alias with this name already exists" +msgstr "Un alias de ce nom existe déjà" + +#: admin/views/alias.py:53 +msgid "Create" +msgstr "Créer" + +#: admin/views/alias.py:65 +msgid "New distribution list" +msgstr "Nouvelle liste de distribution" + +#: admin/views/alias.py:67 +msgid "Distribution list created" +msgstr "Liste de distribution créée" + +#: admin/views/alias.py:76 +msgid "New alias" +msgstr "Nouvel alias" + +#: admin/views/alias.py:77 +msgid "Alias created" +msgstr "Alias créé" + +#: admin/views/alias.py:86 +msgid "New forward" +msgstr "Nouveau transfert" + +#: admin/views/alias.py:87 +msgid "Forward created" +msgstr "Transfert créé" + +#: admin/views/alias.py:101 +msgid "Distribution list modified" +msgstr "Liste de distribution modifiée" + +#: admin/views/alias.py:103 +msgid "Forward modified" +msgstr "Transfert modifié" + +#: admin/views/alias.py:105 +msgid "Alias modified" +msgstr "Alias modifié" + +#: admin/views/domain.py:132 +msgid "You can't delete your own domain" +msgstr "Vous ne pouvez pas supprimer votre propre domaine" + +#: admin/views/domain.py:135 +msgid "Domain deleted" +msgid_plural "Domains deleted" +msgstr[0] "Domaine supprimé" +msgstr[1] "Domaines supprimés" + +#: admin/views/export.py:40 +msgid "Export identities" +msgstr "Export d'identités" + +#: admin/views/export.py:68 +msgid "Export domains" +msgstr "Export de domaines" + +#: admin/views/identity.py:108 admin/views/identity.py:200 +#: admin/views/identity.py:205 +msgid "Invalid request" +msgstr "Requête invalide" + +#: admin/views/identity.py:190 +msgid "Account deleted" +msgid_plural "Accounts deleted" +msgstr[0] "Compte supprimé" +msgstr[1] "Comptes supprimés" + +#: admin/views/import_.py:64 +#, python-format +msgid "Object already exists: %s" +msgstr "L'objet existe déjà : %s" + +#: admin/views/import_.py:68 +#, python-format +msgid "%d objects imported successfully" +msgstr "%d objets importés avec succès" + +#: admin/views/import_.py:86 +#, python-format +msgid "" +"Provide a CSV file where lines respect one of the following formats:\n" +"
      \n" +"
    • domain; name; quota; enabled
    • \n" +"
    • domainalias; name; targeted domain; enabled
    • \n" +" %s\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 "" +"Fournir un fichier CSV ou chaque ligne respècte un des formats suivants :\n" +"
      \n" +"
    • domain; nom; quota; activé
    • \n" +"
    • domainalias; nom; domaine cible; activé
    • \n" +" %s\n" +"
    \n" +"

    Le premier élément de chaque ligne est obligatoire et doit être égal à " +"l'une des valeurs précédentes.

    \n" +"

    Vous pouvez utiliser un caractère différent comme séparateur.

    \n" + +#: admin/views/import_.py:98 +msgid "Import domains" +msgstr "Importer des domaines" + +#: admin/views/import_.py:120 +msgid "" +"Provide a CSV file where lines respect one of the following formats:\n" +"
      \n" +"
    • account; loginname; password; first name; last name; enabled; group; " +"address; quota; [, domain, ...]
    • \n" +"
    • alias; address; enabled; internal recipient
    • \n" +"
    • forward; address; enabled; external recipient
    • \n" +"
    • dlist; address; enabled; recipient; recipient; ...
    • \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 "" +"Fournir un fichier CSV ou chaque ligne respècte un des formats suivants :\n" +"
      \n" +"
    • account; identifiant; mot de passe; prénom; nom; activé, groupe; " +"addresse; quota; [, domaine, ...]
    • \n" +"
    • alias; addresse; activé; destinataire interne
    • \n" +"
    • forward; addresse; activé; destinataire externe
    • \n" +"
    • dlist; addresse; activée; destinataire; destinataire; ...
    • \n" +"
    \n" +"

    Le premier élément de chaque ligne est obligatoire et doit être égal à " +"l'une des valeurs précédentes.

    \n" +"\n" +"

    Vous pouvez utiliser un caractère différent comme séparateur.

    \n" + +#: admin/views/import_.py:132 +msgid "Import identities" +msgstr "Importer des identités" + +#: admin/views/user.py:34 +msgid "Forward updated" +msgstr "Transfert mis à jour" diff --git a/modoboa/admin/locale/fr/LC_MESSAGES/djangojs.po b/modoboa/admin/locale/fr/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..c2605abc7 --- /dev/null +++ b/modoboa/admin/locale/fr/LC_MESSAGES/djangojs.po @@ -0,0 +1,54 @@ +# Modoboa french translation +# Copyright (C) 2009-2013 +# This file is distributed under the same license as the Modoboa package. +# Antoine Nguyen , 2009-2013. +# +msgid "" +msgstr "" +"Project-Id-Version: modoboa-1.0.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 08:20+0100\n" +"PO-Revision-Date: 2011-01-07 10:00+0100\n" +"Last-Translator: Antoine Nguyen \n" +"Language-Team: FR \n" +"Language: Français\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: static/admin/js/admin.js:126 +msgid "No more domain to show" +msgstr "Plus de domaine à afficher" + +#: static/admin/js/admin.js:145 +msgid "Do not delete domain directory" +msgstr "Ne pas supprimer le répertoire du domaine" + +#: static/admin/js/admin.js:147 +msgid "This operation will remove ALL data associated to this domain." +msgstr "Cette opération supprimera TOUTES les données associées à ce domaine." + +#: static/admin/js/admin.js:148 +msgid "" +"This operation will remove all data associated to this domain, excepting " +"accounts." +msgstr "" +"Cette opération supprimera toutes les données associées à ce domaine à " +"l'excéption des comptes." + +#: static/admin/js/admin.js:231 +msgid "No domain administrator defined" +msgstr "Aucun administrateur de domaine défini" + +#: static/admin/js/admin.js:286 static/admin/js/admin.js.c:354 +msgid "No more identity to show" +msgstr "Plus d'identité à afficher" + +#: static/admin/js/admin.js:328 +msgid "Do not delete mailbox directory" +msgstr "Ne pas supprimer le répertoire de la boite aux lettres" + +#: static/admin/js/admin.js:357 +msgid "No more quota to show" +msgstr "Plus de quota à afficher" diff --git a/modoboa/admin/locale/it/LC_MESSAGES/django.po b/modoboa/admin/locale/it/LC_MESSAGES/django.po new file mode 100644 index 000000000..018153132 --- /dev/null +++ b/modoboa/admin/locale/it/LC_MESSAGES/django.po @@ -0,0 +1,878 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Giancarlo Gaifas , 2013 +# Rocco , 2015 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: 2015-10-11 11:48+0000\n" +"Last-Translator: Rocco \n" +"Language-Team: Italian (http://www.transifex.com/tonio/modoboa/language/" +"it/)\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: admin/app_settings.py:54 admin/templates/admin/editdomainform.html:25 +msgid "Mailboxes" +msgstr "Caselle di posta" + +#: admin/app_settings.py:57 +msgid "Handle mailboxes on filesystem" +msgstr "Gestione delle caselle di posta nel filsystem" + +#: admin/app_settings.py:60 +msgid "" +"Rename or remove mailboxes on the filesystem when they get renamed or " +"removed within Modoboa" +msgstr "" +"Rinomina o rimuovi le caselle di posta nel filesystem quando vengono " +"rinominate o cancellate in Modoboa " + +#: admin/app_settings.py:66 +msgid "Mailboxes owner" +msgstr "Proprietario delle caselle postali" + +#: admin/app_settings.py:69 +msgid "The UNIX account who owns mailboxes on the filesystem" +msgstr "L'account UNIX a cui appartengono le caselle postali nel filsystem" + +#: admin/app_settings.py:74 +msgid "Default domain quota" +msgstr "Quota predefinita per il domini" + +#: admin/app_settings.py:77 +msgid "" +"Default quota (in MB) applied to freshly created domains with no value " +"specified. A value of 0 means no quota." +msgstr "" +"Quota predefinita (in MB) applicata ai nuovi domini che non la specificano. " +"Un valore 0 implica nessuna quota." + +#: admin/app_settings.py:83 +msgid "Automatic account removal" +msgstr "Rimozione automatica dell'account" + +#: admin/app_settings.py:86 +msgid "When a mailbox is removed, also remove the associated account" +msgstr "" +"Quando una casella postale viene rimossa rimuovi anche l'account associato" + +#: admin/app_settings.py:90 +msgid "Automatic domain/mailbox creation" +msgstr "Creazione automatica del domini/casella di posta elettronica" + +#: admin/app_settings.py:93 +msgid "Create a domain and a mailbox when an account is automatically created." +msgstr "" +"Crea un dominio ed una casella di posta elettronica quando si crea un " +"account utente" + +#: admin/app_settings.py:139 admin/forms/account.py:239 +#: admin/forms/domain.py:77 +msgid "Must be a positive integer" +msgstr "Dev'essere un intero positivo" + +#: admin/app_settings.py:149 +msgid "Administration" +msgstr "Amministrazione" + +#: admin/callbacks.py:46 +msgid "Domain administrator" +msgstr "Amministratore Dominio" + +#: admin/callbacks.py:75 admin/templates/admin/domain_statistics.html:30 +#: admin/templates/admin/domains.html:3 +msgid "Domains" +msgstr "Domini" + +#: admin/callbacks.py:85 admin/templates/admin/domain_statistics.html:36 +#: admin/templates/admin/domain_statistics.html:50 +#: admin/templates/admin/identities.html:5 +msgid "Identities" +msgstr "Identità" + +#: admin/callbacks.py:100 admin/templates/admin/forward.html:2 +msgid "Forward" +msgstr "Inoltra" + +#: admin/callbacks.py:163 +#, python-format +msgid "Account import failed (%s): domain does not exist" +msgstr "Importazione dell'account fallito (%s): il dominio non esiste" + +#: admin/callbacks.py:173 admin/forms/account.py:257 +#, python-format +msgid "Mailbox %s already exists" +msgstr "La casella di posta %s esiste già" + +#: admin/callbacks.py:181 +#, python-format +msgid "Account import failed (%s): wrong quota value" +msgstr "Importazione account fallito(%s): valore della quota errato" + +#: admin/callbacks.py:216 admin/forms/domain.py:94 +msgid "domain alias" +msgstr "alias del dominio" + +#: admin/forms/account.py:30 admin/templates/admin/editdomainform.html:13 +msgid "Username" +msgstr "Nome utente" + +#: admin/forms/account.py:32 +msgid "" +"The user's name. Must be a valid e-mail address for simple users or " +"administrators with a mailbox." +msgstr "" +"Il nome dell'utente. Dev'essere un indirizzo e-mail per gli utenti semplici " +"o gli amministratori che posseggono una casella" + +#: admin/forms/account.py:37 +msgid "Role" +msgstr "Ruolo" + +#: admin/forms/account.py:38 admin/forms/account.py:75 +msgid "Choose" +msgstr "Scegli" + +#: admin/forms/account.py:39 +msgid "What level of permission this user will have" +msgstr "Il livello di permessi che avrà questo utente" + +#: admin/forms/account.py:42 +msgid "Password" +msgstr "Password" + +#: admin/forms/account.py:45 +msgid "Confirmation" +msgstr "Conferma" + +#: admin/forms/account.py:48 +msgid "Enter the same password as above, for verification." +msgstr "" +"Inserisci la medesima password che hai inserito sopra, per la verifica." + +#: admin/forms/account.py:66 +msgid "Enabled" +msgstr "Abilitato" + +#: admin/forms/account.py:141 +msgid "Only super administrators are allowed for this mode" +msgstr "Solo i super amministratori possono operare in questa modalità" + +#: admin/forms/account.py:150 +msgid "The two password fields didn't match." +msgstr "Le password nei due campi non corrispondono" + +#: admin/forms/account.py:156 +msgid "You can't disable your own account" +msgstr "Tu non puoi disabilitare il tuo account" + +#: admin/forms/account.py:170 +msgid "E-mail" +msgstr "E-mail" + +#: admin/forms/account.py:172 admin/forms/domain.py:35 +msgid "Quota" +msgstr "Quota" + +#: admin/forms/account.py:174 +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 per questo account. Stabilire un valore personalizzato o usare " +"quello predefinito. Lasciare vuoto per non impostare limiti (non consentito " +"per gli amministratori di dominio)." + +#: admin/forms/account.py:182 admin/forms/domain.py:43 +#: admin/templates/admin/domains.html:26 +msgid "Alias(es)" +msgstr "Alias" + +#: admin/forms/account.py:185 +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 per questa casella di posta. Inserire un solo indirizzo alla volta, " +"premere INVIO per aggiungere un nuovo elemento. Utilizzare il carattere '*' " +"per creare un alias 'prenditutto' (p.es.: *@dominio.org)." + +#: admin/forms/account.py:248 admin/forms/account.py:279 +#: admin/forms/alias.py:72 +msgid "Domain does not exist" +msgstr "Il dominio non esiste" + +#: admin/forms/account.py:355 +msgid "Domain(s)" +msgstr "Dominio(i)" + +#: admin/forms/account.py:357 +msgid "Domain(s) that user administrates" +msgstr "Dominio(i) amministrato(i) dall'utente" + +#: admin/forms/account.py:401 admin/forms/account.py:487 +#: admin/forms/domain.py:300 admin/forms/domain.py:370 +msgid "General" +msgstr "Generale" + +#: admin/forms/account.py:406 admin/forms/account.py:493 +msgid "Mail" +msgstr "Posta" + +#: admin/forms/account.py:409 +msgid "Permissions" +msgstr "Permessi" + +#: admin/forms/account.py:476 +msgid "Account updated" +msgstr "Account aggiornato" + +#: admin/forms/account.py:500 +msgid "New account" +msgstr "Nuovo account" + +#: admin/forms/account.py:512 +msgid "Account created" +msgstr "Account creato" + +#: admin/forms/alias.py:23 +msgid "Email address" +msgstr "Indirizzo email" + +#: admin/forms/alias.py:25 +msgid "" +"The alias address. To create a catchall alias, just enter the domain name " +"(@domain.tld)." +msgstr "" +"Indirizzi email alias. Per creare un alias catchall specifica il nome del " +"dominio (@domain.tld)." + +#: admin/forms/alias.py:31 +msgid "Recipients" +msgstr "Destinatari" + +#: admin/forms/alias.py:33 +msgid "" +"Addresses this alias will point to. Indicate only one address per input, " +"press ENTER to add a new input." +msgstr "" +"Indirizzi verso il quale questo alias punterà . Indicare un solo indirizzo " +"per ogni ingresso , premere INVIO per aggiungere un nuovo input ." + +#: admin/forms/alias.py:75 +msgid "You don't have access to this domain" +msgstr "Non hai accesso a questo dominio" + +#: admin/forms/alias.py:85 +msgid "No recipient defined" +msgstr "Non è stato definito un destinatario" + +#: admin/forms/domain.py:22 admin/models/domain.py:76 +#: admin/templates/admin/domain_statistics.html:49 +msgid "Domain" +msgstr "Dominio" + +#: admin/forms/domain.py:32 admin/templates/admin/domain_statistics.html:26 +msgid "Type" +msgstr "Tipo" + +#: admin/forms/domain.py:38 +msgid "" +"Default quota in MB applied to mailboxes. Leave empty to use the default " +"value." +msgstr "" +"Quota in MB predefinita applicata alle caselle di posta. Lascialo vuoto se " +"vuoi applicare il valore predefinito." + +#: admin/forms/domain.py:46 +msgid "" +"Alias(es) of this domain. Indicate only one name per input, press ENTER to " +"add a new input." +msgstr "" +"Alias per questo dominio. Inserire un solo nome alla volta, premere INVIO " +"per aggiungere un nuovo elemento" + +#: admin/forms/domain.py:97 admin/forms/domain.py:108 +#: admin/forms/domain.py:116 +#, python-format +msgid "A %s with this name already exists" +msgstr "Un %s con questo nome esiste già" + +#: admin/forms/domain.py:108 admin/forms/domain.py:113 +msgid "domain" +msgstr "dominio" + +#: admin/forms/domain.py:200 +msgid "Create a domain administrator" +msgstr "Crea un amministratore di dominio" + +#: admin/forms/domain.py:203 +msgid "Automatically create an administrator for this domain" +msgstr "Crea automaticamente un amministratore per questo dominio" + +#: admin/forms/domain.py:208 admin/templates/admin/domains.html:25 +#: admin/templates/admin/editdomainform.html:13 +#: admin/templates/admin/identity_headers.html:2 +msgid "Name" +msgstr "Nome" + +#: admin/forms/domain.py:211 +msgid "" +"The administrator's name. Don't include the domain's name here, it will be " +"automatically appended." +msgstr "" +"Il nome dell'amministratore. Non includere il nome del dominio, verrà " +"aggiunto automaticamente." + +#: admin/forms/domain.py:218 +msgid "Create aliases" +msgstr "Crea alias" + +#: admin/forms/domain.py:221 +msgid "Automatically create standard aliases for this domain" +msgstr "Crea automaticamente gli alias standard per questo dominio" + +#: admin/forms/domain.py:240 +msgid "Invalid format" +msgstr "Formato non valido" + +#: admin/forms/domain.py:257 +#, python-format +msgid "User '%s' already exists" +msgstr "L'utente '%s' esiste già" + +#: admin/forms/domain.py:359 +msgid "Domain modified" +msgstr "Dominio modificato" + +#: admin/forms/domain.py:379 +msgid "Options" +msgstr "Opzioni" + +#: admin/forms/domain.py:387 +msgid "New domain" +msgstr "Nuovo dominio" + +#: admin/forms/domain.py:405 +msgid "Domain created" +msgstr "Dominio creato" + +#: admin/forms/export.py:7 +msgid "File name" +msgstr "Nome del file" + +#: admin/forms/export.py:13 admin/forms/import_.py:13 +msgid "Separator" +msgstr "Separatore" + +#: admin/forms/forward.py:14 +msgid "Recipient(s)" +msgstr "Destinatario(i)" + +#: admin/forms/forward.py:18 +msgid "Indicate one or more recipients separated by a ','" +msgstr "Indicare uno o più destinatari separati da ','" + +#: admin/forms/forward.py:21 +msgid "Keep local copies" +msgstr "Mantieni copie locali" + +#: admin/forms/forward.py:24 +msgid "Forward messages and store copies into your local mailbox" +msgstr "" +"Inoltra i messaggi e memorizza le copie nella tua casella di posta locale" + +#: admin/forms/forward.py:42 +msgid "" +"You can't define a forward to a local destination. Please ask your " +"administrator to create an alias instead." +msgstr "" +"Non puoi definire l'inoltro con destinazione una cartella locale. Chiedi " +"piuttosto all'amministratore di creare un alias." + +#: admin/forms/import_.py:11 +msgid "Select a file" +msgstr "Seleziona un file" + +#: admin/forms/import_.py:19 +msgid "Continue on error" +msgstr "Continuare in caso di errore" + +#: admin/forms/import_.py:20 +msgid "Don't treat duplicated objects as error" +msgstr "Non considerare errore gli oggetti duplicati" + +#: admin/forms/import_.py:38 +msgid "Crypt passwords" +msgstr "Cripta le password" + +#: admin/forms/import_.py:40 +msgid "Check this option if passwords contained in your file are not crypted" +msgstr "" +"Seleziona questa opzione se le password contenute nel file non sono criptate" + +#: admin/lib.py:30 +msgid "A mailbox is required" +msgstr "É necessaria una casella" + +#: admin/lib.py:120 +msgid "You are not allowed to import domains" +msgstr "Non hai i permessi di importare domini" + +#: admin/lib.py:129 +msgid "You are not allowed to import domain aliases." +msgstr "Non hai i permessi di importare gli alias dominio" + +#: admin/models/alias.py:26 admin/models/mailbox.py:77 +msgid "address" +msgstr "indirizzo" + +#: admin/models/alias.py:28 +msgid "The alias address." +msgstr "Indirizzi Alias." + +#: admin/models/alias.py:33 admin/models/domain.py:37 +#: admin/models/domain_alias.py:24 +msgid "enabled" +msgstr "Attivato" + +#: admin/models/alias.py:34 admin/models/domain_alias.py:25 +msgid "Check to activate this alias" +msgstr "Seleziona per attivare questo alias" + +#: admin/models/alias.py:79 +msgid "distribution list" +msgstr "elenco di smistamento" + +#: admin/models/alias.py:80 +msgid "forward" +msgstr "inoltra" + +#: admin/models/alias.py:81 +msgid "alias" +msgstr "alias" + +#: admin/models/alias.py:112 +msgid "Invalid address" +msgstr "Indirizzo non valido" + +#: admin/models/alias.py:133 +msgid "Local recipient {}@{} not found" +msgstr "Destinatario locale {}@{} non trovato" + +#: admin/models/alias.py:162 +#, python-format +msgid "Invalid line: %s" +msgstr "Riga non valida: %s" + +#: admin/models/alias.py:168 +#, python-format +msgid "Domain '%s' does not exist" +msgstr "Il dominio '%s' non esiste" + +#: admin/models/domain.py:33 admin/models/domain_alias.py:17 +msgid "name" +msgstr "nome" + +#: admin/models/domain.py:34 +msgid "The domain name" +msgstr "Il nome del dominio" + +#: admin/models/domain.py:38 +msgid "Check to activate this domain" +msgstr "Seleziona per attivare questo dominio" + +#: admin/models/domain.py:160 admin/models/domain_alias.py:47 +msgid "Invalid line" +msgstr "Riga non valida" + +#: admin/models/domain.py:168 +#, python-format +msgid "Invalid quota value for domain '%s'" +msgstr "Valore della quota non valido per il dominio '%s'" + +#: admin/models/domain_alias.py:18 +msgid "The alias name" +msgstr "Il nome dell'alias" + +#: admin/models/domain_alias.py:20 +msgid "target" +msgstr "destinazione" + +#: admin/models/domain_alias.py:21 +msgid "The domain this alias points to" +msgstr "Il dominio a cui punta questo alias" + +#: admin/models/domain_alias.py:56 +#, python-format +msgid "Unknown domain %s" +msgstr "Dominio sconosciuto %s" + +#: admin/models/mailbox.py:79 +msgid "Mailbox address (without the @domain.tld part)" +msgstr "Indirizzo della casella di posta (senza la parte @dominio.org)" + +#: admin/models/mailbox.py:144 +#, python-format +msgid "Failed to retrieve mailbox location (%s)" +msgstr "Impossibile recuperare la posizione della casella di posta (%s)" + +#: admin/models/mailbox.py:233 +#, python-format +msgid "Quota is greater than the allowed domain's limit (%dM)" +msgstr "La quota è maggiore del limite stabilito per il dominio (%dM)" + +#: admin/models/mailbox.py:239 +msgid "A quota is required" +msgstr "È richiesta una quota" + +#: admin/templates/admin/account_general_form.html:11 +#: admin/templates/admin/aliasform.html:8 +msgid "Domain is disabled" +msgstr "il Dominio è disabilitato" + +#: admin/templates/admin/domain_general_form.html:4 +#: admin/templates/admin/mailform.html:3 +#: admin/templates/admin/newdomainform.html:3 +msgid "MB" +msgstr "MB" + +#: admin/templates/admin/domain_options_form.html:7 +msgid "You can't create a new domain administrator" +msgstr "Non ti è possibile creare un amministratore di dominio" + +#: admin/templates/admin/domain_statistics.html:15 +#: admin/templatetags/admin_tags.py:45 +msgid "Statistics" +msgstr "Statistiche" + +#: admin/templates/admin/domain_statistics.html:15 +msgid "Domains and identities" +msgstr "Domini e identità" + +#: admin/templates/admin/domain_statistics.html:22 +msgid "Global counters" +msgstr "Contatori globali" + +#: admin/templates/admin/domain_statistics.html:27 +msgid "Quantity" +msgstr "Quantità" + +#: admin/templates/admin/domain_statistics.html:33 +#: admin/templates/admin/editdomainform.html:24 +msgid "Domain aliases" +msgstr "Alias del dominio" + +#: admin/templates/admin/domain_statistics.html:45 +msgid "Per-domain counters" +msgstr "Contatore per dominio" + +#: admin/templates/admin/domains.html:27 +#: admin/templates/admin/identity_headers.html:4 +msgid "Tags" +msgstr "Tag" + +#: admin/templates/admin/domains.html:28 +#: admin/templates/admin/identity_headers.html:5 +msgid "Actions" +msgstr "Azioni" + +#: admin/templates/admin/domains_table.html:4 +msgid "disabled" +msgstr "Disabilitato" + +#: admin/templates/admin/editdomainform.html:4 +msgid "Administrators" +msgstr "Amministratori" + +#: admin/templates/admin/editdomainform.html:7 +msgid "Miscellaneous" +msgstr "Miscellanea" + +#: admin/templates/admin/editdomainform.html:19 +msgid "No domain administrator defined" +msgstr "Non è stato definito alcun amministratore di dominio" + +#: admin/templates/admin/editdomainform.html:22 +msgid "Creation date" +msgstr "Data di creazione" + +#: admin/templates/admin/editdomainform.html:23 +msgid "Last modification date" +msgstr "Data dell'ultima modifica" + +#: admin/templates/admin/editdomainform.html:26 +msgid "Mailbox aliases" +msgstr "Alias delle caselle di posta" + +#: admin/templates/admin/forward.html:2 +msgid "Automatically forward emails to another address" +msgstr "Inoltra automaticamente le e-mail ad un altro indirizzo" + +#: admin/templates/admin/forward.html:9 admin/views/alias.py:113 +msgid "Update" +msgstr "Aggiorna" + +#: admin/templates/admin/identity_headers.html:3 +msgid "Fullname/Recipient" +msgstr "Nome completo/Destinatario" + +#: admin/templates/admin/importform.html:6 +msgid "Help" +msgstr "Aiuto" + +#: admin/templates/admin/importform.html:13 +msgid "Importing..." +msgstr "Importazione..." + +#: admin/templates/admin/mailboxes_raw.html:3 +msgid "Choose a value" +msgstr "Scegli un valore" + +#: admin/templates/admin/mailform.html:3 +msgid "Use domain default value" +msgstr "Usa il valore predefinito" + +#: admin/templates/admin/quota_headers.html:2 +msgid "Address" +msgstr "Indirizzo" + +#: admin/templates/admin/quota_headers.html:3 +msgid "Value" +msgstr "Valore" + +#: admin/templates/admin/quota_headers.html:4 +msgid "Limit" +msgstr "Limite" + +#: admin/templates/admin/quota_headers.html:5 +#, python-format +msgid "Usage (%%)" +msgstr "Utilizzo (%%)" + +#: admin/templates/admin/searchform.html:6 +msgid "Search" +msgstr "Ricerca" + +#: admin/templatetags/admin_tags.py:19 +msgid "enabled_m" +msgstr "Abilitato" + +#: admin/templatetags/admin_tags.py:19 +msgid "enabled_f" +msgstr "Abilitata" + +#: admin/templatetags/admin_tags.py:40 +msgid "List domains" +msgstr "Lista domini" + +#: admin/templatetags/admin_tags.py:53 +msgid "Add domain" +msgstr "Aggiungi dominio" + +#: admin/templatetags/admin_tags.py:62 admin/templatetags/admin_tags.py:126 +#: admin/views/import_.py:99 admin/views/import_.py:133 +msgid "Import" +msgstr "Importa" + +#: admin/templatetags/admin_tags.py:68 admin/templatetags/admin_tags.py:132 +#: admin/views/export.py:41 admin/views/export.py:69 +msgid "Export" +msgstr "Esporta" + +#: admin/templatetags/admin_tags.py:92 +msgid "List identities" +msgstr "Elenco identità" + +#: admin/templatetags/admin_tags.py:97 +msgid "List quotas" +msgstr "Elenco quote" + +#: admin/templatetags/admin_tags.py:102 +msgid "Add account" +msgstr "Aggiungi account" + +#: admin/templatetags/admin_tags.py:108 +msgid "Add alias" +msgstr "Aggiungi alias" + +#: admin/templatetags/admin_tags.py:114 +msgid "Add forward" +msgstr "Aggiungi inoltro" + +#: admin/templatetags/admin_tags.py:120 +msgid "Add distribution list" +msgstr "Aggiungi elenco di smistamento" + +#: admin/templatetags/admin_tags.py:152 +msgid "View the domain's identities" +msgstr "Visualizza le identita del dominio" + +#: admin/templatetags/admin_tags.py:159 admin/templatetags/admin_tags.py:182 +#: admin/templatetags/admin_tags.py:190 +#, python-format +msgid "Delete %s?" +msgstr "Eliminare %s?" + +#: admin/templatetags/admin_tags.py:267 +msgid "Remove this permission" +msgstr "Rimuovi questo permesso" + +#: admin/views/alias.py:29 +msgid "Alias with this name already exists" +msgstr "Un alias con questo nome esiste già" + +#: admin/views/alias.py:53 +msgid "Create" +msgstr "Crea" + +#: admin/views/alias.py:65 +msgid "New distribution list" +msgstr "Nuovo elenco di smistamento" + +#: admin/views/alias.py:67 +msgid "Distribution list created" +msgstr "Elenco di smistamento creato" + +#: admin/views/alias.py:76 +msgid "New alias" +msgstr "Nuovo alias" + +#: admin/views/alias.py:77 +msgid "Alias created" +msgstr "Alias creato" + +#: admin/views/alias.py:86 +msgid "New forward" +msgstr "Nuovo inoltro" + +#: admin/views/alias.py:87 +msgid "Forward created" +msgstr "Inoltro creato" + +#: admin/views/alias.py:101 +msgid "Distribution list modified" +msgstr "Elenco di smistamento modificato" + +#: admin/views/alias.py:103 +msgid "Forward modified" +msgstr "Inoltro modificato" + +#: admin/views/alias.py:105 +msgid "Alias modified" +msgstr "Alias modificato" + +#: admin/views/domain.py:132 +msgid "You can't delete your own domain" +msgstr "Non puoi cancellare il tuo dominio" + +#: admin/views/domain.py:135 +msgid "Domain deleted" +msgid_plural "Domains deleted" +msgstr[0] "Dominio cancellato" +msgstr[1] "Domini cancellati" + +#: admin/views/export.py:40 +msgid "Export identities" +msgstr "Identità di esportazione" + +#: admin/views/export.py:68 +msgid "Export domains" +msgstr "Esporta domini" + +#: admin/views/identity.py:108 admin/views/identity.py:200 +#: admin/views/identity.py:205 +msgid "Invalid request" +msgstr "Richiesta non valida" + +#: admin/views/identity.py:190 +msgid "Account deleted" +msgid_plural "Accounts deleted" +msgstr[0] "Account cancellato" +msgstr[1] "Account cancellati" + +#: admin/views/import_.py:64 +#, python-format +msgid "Object already exists: %s" +msgstr "L'oggetto esiste già: %s" + +#: admin/views/import_.py:68 +#, python-format +msgid "%d objects imported successfully" +msgstr "%d oggetti importati correttamente" + +#: admin/views/import_.py:86 +#, python-format +msgid "" +"Provide a CSV file where lines respect one of the following formats:\n" +"
      \n" +"
    • domain; name; quota; enabled
    • \n" +"
    • domainalias; name; targeted domain; enabled
    • \n" +" %s\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 "" +"Fornire un file CSV le cui righe rispettino uno dei seguenti formati:\n" +"
      \n" +"
    • dominio; nome; quota; abilitato
    • \n" +"
    • aliasdominio; nome; dominio di destinazione; abilitato
    • \n" +" %s\n" +"
    \n" +"

    Il primo elemento di ogni riga è obbligatorio e deve essere uguale a " +"quello dei valori precedenti.

    \n" +"\n" +"

    Come separatore puoi utilizzare un carattere diverso.

    \n" + +#: admin/views/import_.py:98 +msgid "Import domains" +msgstr "Importa domini" + +#: admin/views/import_.py:120 +msgid "" +"Provide a CSV file where lines respect one of the following formats:\n" +"
      \n" +"
    • account; loginname; password; first name; last name; enabled; group; " +"address; quota; [, domain, ...]
    • \n" +"
    • alias; address; enabled; internal recipient
    • \n" +"
    • forward; address; enabled; external recipient
    • \n" +"
    • dlist; address; enabled; recipient; recipient; ...
    • \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 "" +"Fornire un file CSV le cui righe rispettino uno dei seguenti formati:\n" +"
      \n" +"
    • account; id utente; password; nome; cognome; abilitato; gruppo; " +"indirizzo; quota; [, dominio, ...]
    • \n" +"
    • alias; indirizzo; abilitato; destinatario interno
    • \n" +"
    • forward; indirizzo; abilitato; destinatario esterno
    • \n" +"
    • dlist; indirizzo; abilitato; destinatario; destinatario; ...\n" +"
    \n" +"

    Il primo elemento di ogni riga è obbligatorio e deve essere uguale a " +"quello dei valori precedenti.

    \n" +"\n" +"

    Come separatore puoi utilizzare un carattere diverso.

    \n" + +#: admin/views/import_.py:132 +msgid "Import identities" +msgstr "Identità di importazione" + +#: admin/views/user.py:34 +msgid "Forward updated" +msgstr "Inoltro aggiornato" diff --git a/modoboa/admin/locale/it/LC_MESSAGES/djangojs.po b/modoboa/admin/locale/it/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..0fb671eae --- /dev/null +++ b/modoboa/admin/locale/it/LC_MESSAGES/djangojs.po @@ -0,0 +1,56 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Rocco , 2015 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 08:20+0100\n" +"PO-Revision-Date: 2015-10-11 11:19+0000\n" +"Last-Translator: Rocco \n" +"Language-Team: Italian (http://www.transifex.com/tonio/modoboa/language/" +"it/)\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: static/admin/js/admin.js:126 +msgid "No more domain to show" +msgstr "Non ci sono altri domini da mostrare" + +#: static/admin/js/admin.js:145 +msgid "Do not delete domain directory" +msgstr "Non eliminare la cartella dei domini" + +#: static/admin/js/admin.js:147 +msgid "This operation will remove ALL data associated to this domain." +msgstr "Questa operazione eliminerà TUTTI i dati associati a questo dominio." + +#: static/admin/js/admin.js:148 +msgid "" +"This operation will remove all data associated to this domain, excepting " +"accounts." +msgstr "" +"Questa operazione eliminerà tutti i dati associati a questo dominio tranne " +"gli account" + +#: static/admin/js/admin.js:231 +msgid "No domain administrator defined" +msgstr "Non è stato definito un amministratore di dominio" + +#: static/admin/js/admin.js:286 static/admin/js/admin.js.c:354 +msgid "No more identity to show" +msgstr "Non ci sono altre identità da mostrare" + +#: static/admin/js/admin.js:328 +msgid "Do not delete mailbox directory" +msgstr "Non eliminare la cartella della casella posta" + +#: static/admin/js/admin.js:357 +msgid "No more quota to show" +msgstr "Non ci sono altre quote da mostrare" diff --git a/modoboa/admin/locale/nl_NL/LC_MESSAGES/django.po b/modoboa/admin/locale/nl_NL/LC_MESSAGES/django.po new file mode 100644 index 000000000..fd7deacc5 --- /dev/null +++ b/modoboa/admin/locale/nl_NL/LC_MESSAGES/django.po @@ -0,0 +1,897 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Andrew , 2014 +# TuxBrother , 2014 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: 2014-12-31 11:37+0000\n" +"Last-Translator: TuxBrother \n" +"Language-Team: Dutch (Netherlands) (http://www.transifex.com/projects/p/" +"modoboa/language/nl_NL/)\n" +"Language: nl_NL\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: admin/app_settings.py:54 admin/templates/admin/editdomainform.html:25 +msgid "Mailboxes" +msgstr "Mailboxen" + +#: admin/app_settings.py:57 +msgid "Handle mailboxes on filesystem" +msgstr "Pas mailboxen op het bestandssysteem aan" + +#: admin/app_settings.py:60 +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." + +#: admin/app_settings.py:66 +msgid "Mailboxes owner" +msgstr "Eigenaar van de mailboxen" + +#: admin/app_settings.py:69 +msgid "The UNIX account who owns mailboxes on the filesystem" +msgstr "" +"De UNIX account die de rechten heeft tot de mailboxen op het bestandssysteem" + +#: admin/app_settings.py:74 +msgid "Default domain quota" +msgstr "Standaard domein quota" + +#: admin/app_settings.py:77 +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." + +#: admin/app_settings.py:83 +msgid "Automatic account removal" +msgstr "Automatish een account verwijderen" + +#: admin/app_settings.py:86 +msgid "When a mailbox is removed, also remove the associated account" +msgstr "" +"Als een mailbox word verwijderd, verwijder dan ook de bijbehorende account" + +#: admin/app_settings.py:90 +msgid "Automatic domain/mailbox creation" +msgstr "Domein en mailbox automatisch aanmaken" + +#: admin/app_settings.py:93 +msgid "Create a domain and a mailbox when an account is automatically created." +msgstr "" +"Maak een domein en mailbox aan wanneer een account automatisch is aangemaakt." + +#: admin/app_settings.py:139 admin/forms/account.py:239 +#: admin/forms/domain.py:77 +msgid "Must be a positive integer" +msgstr "Dit moet een positive integer zijn." + +#: admin/app_settings.py:149 +msgid "Administration" +msgstr "Administratie" + +#: admin/callbacks.py:46 +#, fuzzy +#| msgid "Create a domain administrator" +msgid "Domain administrator" +msgstr "Maak een domein administrator aan" + +#: admin/callbacks.py:75 admin/templates/admin/domain_statistics.html:30 +#: admin/templates/admin/domains.html:3 +msgid "Domains" +msgstr "Domeinen" + +#: admin/callbacks.py:85 admin/templates/admin/domain_statistics.html:36 +#: admin/templates/admin/domain_statistics.html:50 +#: admin/templates/admin/identities.html:5 +msgid "Identities" +msgstr "Identiteiten" + +#: admin/callbacks.py:100 admin/templates/admin/forward.html:2 +msgid "Forward" +msgstr "Doorsturen" + +#: admin/callbacks.py:163 +#, python-format +msgid "Account import failed (%s): domain does not exist" +msgstr "Accounts importeren mislukt (%s): domein bestaat niet" + +#: admin/callbacks.py:173 admin/forms/account.py:257 +#, python-format +msgid "Mailbox %s already exists" +msgstr "Mailbox %s bestaat al" + +#: admin/callbacks.py:181 +#, python-format +msgid "Account import failed (%s): wrong quota value" +msgstr "Account import is mislukt (%s): verkeerd quotum waarde" + +#: admin/callbacks.py:216 admin/forms/domain.py:94 +msgid "domain alias" +msgstr "domein alias" + +#: admin/forms/account.py:30 admin/templates/admin/editdomainform.html:13 +msgid "Username" +msgstr "Gebruikersnaam" + +#: admin/forms/account.py:32 +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." + +#: admin/forms/account.py:37 +msgid "Role" +msgstr "Regel" + +#: admin/forms/account.py:38 admin/forms/account.py:75 +msgid "Choose" +msgstr "Kiezen" + +#: admin/forms/account.py:39 +msgid "What level of permission this user will have" +msgstr "Welk niveau van rechten deze gebruiker zal hebben" + +#: admin/forms/account.py:42 +msgid "Password" +msgstr "Wachtwoord" + +#: admin/forms/account.py:45 +msgid "Confirmation" +msgstr "Bevestiging" + +#: admin/forms/account.py:48 +msgid "Enter the same password as above, for verification." +msgstr "Herhaal wachtwoord (voor verificatie)" + +#: admin/forms/account.py:66 +msgid "Enabled" +msgstr "Actief" + +#: admin/forms/account.py:141 +msgid "Only super administrators are allowed for this mode" +msgstr "" + +#: admin/forms/account.py:150 +msgid "The two password fields didn't match." +msgstr "De wachtwoorden komen niet overeen." + +#: admin/forms/account.py:156 +msgid "You can't disable your own account" +msgstr "U kunt uw eigen account niet uitschakelen" + +#: admin/forms/account.py:170 +msgid "E-mail" +msgstr "E-mail" + +#: admin/forms/account.py:172 admin/forms/domain.py:35 +msgid "Quota" +msgstr "Quota" + +#: admin/forms/account.py:174 +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)." + +#: admin/forms/account.py:182 admin/forms/domain.py:43 +#: admin/templates/admin/domains.html:26 +msgid "Alias(es)" +msgstr "Alias(sen)" + +#: admin/forms/account.py:185 +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)." + +#: admin/forms/account.py:248 admin/forms/account.py:279 +#: admin/forms/alias.py:72 +msgid "Domain does not exist" +msgstr "Domein bestaat niet" + +#: admin/forms/account.py:355 +msgid "Domain(s)" +msgstr "Domein(en)" + +#: admin/forms/account.py:357 +msgid "Domain(s) that user administrates" +msgstr "Domein(en) die gebruiker mag beheren" + +#: admin/forms/account.py:401 admin/forms/account.py:487 +#: admin/forms/domain.py:300 admin/forms/domain.py:370 +msgid "General" +msgstr "Algemeen" + +#: admin/forms/account.py:406 admin/forms/account.py:493 +msgid "Mail" +msgstr "Mail" + +#: admin/forms/account.py:409 +msgid "Permissions" +msgstr "Rechten" + +#: admin/forms/account.py:476 +msgid "Account updated" +msgstr "Account aangepast" + +#: admin/forms/account.py:500 +msgid "New account" +msgstr "Nieuwe account" + +#: admin/forms/account.py:512 +msgid "Account created" +msgstr "Account aangemaakt" + +#: admin/forms/alias.py:23 +msgid "Email address" +msgstr "Email adres" + +#: admin/forms/alias.py:25 +#, fuzzy +#| msgid "" +#| "The alias address (without the domain part). For a 'catch-all' address, " +#| "just enter an * character." +msgid "" +"The alias address. To create a catchall alias, just enter the domain name " +"(@domain.tld)." +msgstr "" +"Het alias adres (zonder het domein deel). Voor een 'catch-all' adres, vul " +"een * in." + +#: admin/forms/alias.py:31 +msgid "Recipients" +msgstr "Ontvangers" + +#: admin/forms/alias.py:33 +#, fuzzy +#| msgid "" +#| "Mailbox(es) this alias will point to. Indicate only one address per " +#| "input, press ENTER to add a new input." +msgid "" +"Addresses 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." + +#: admin/forms/alias.py:75 +msgid "You don't have access to this domain" +msgstr "U heeft geen toegang tot dit domein" + +#: admin/forms/alias.py:85 +msgid "No recipient defined" +msgstr "Geen ontvanger opgegeven" + +#: admin/forms/domain.py:22 admin/models/domain.py:76 +#: admin/templates/admin/domain_statistics.html:49 +msgid "Domain" +msgstr "Domein" + +#: admin/forms/domain.py:32 admin/templates/admin/domain_statistics.html:26 +msgid "Type" +msgstr "Type" + +#: admin/forms/domain.py:38 +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." + +#: admin/forms/domain.py:46 +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." + +#: admin/forms/domain.py:97 admin/forms/domain.py:108 +#: admin/forms/domain.py:116 +#, python-format +msgid "A %s with this name already exists" +msgstr "Een %s met deze naam bestaat al" + +#: admin/forms/domain.py:108 admin/forms/domain.py:113 +msgid "domain" +msgstr "domein" + +#: admin/forms/domain.py:200 +msgid "Create a domain administrator" +msgstr "Maak een domein administrator aan" + +#: admin/forms/domain.py:203 +msgid "Automatically create an administrator for this domain" +msgstr "Automatisch een administrator aanmaken voor dit domein" + +#: admin/forms/domain.py:208 admin/templates/admin/domains.html:25 +#: admin/templates/admin/editdomainform.html:13 +#: admin/templates/admin/identity_headers.html:2 +msgid "Name" +msgstr "Naam" + +#: admin/forms/domain.py:211 +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." + +#: admin/forms/domain.py:218 +msgid "Create aliases" +msgstr "Aliassen aanmaken" + +#: admin/forms/domain.py:221 +msgid "Automatically create standard aliases for this domain" +msgstr "Automatisch een standaard alias voor dit domein aanmaken" + +#: admin/forms/domain.py:240 +msgid "Invalid format" +msgstr "Verkeerd formaat" + +#: admin/forms/domain.py:257 +#, python-format +msgid "User '%s' already exists" +msgstr "Gebruiker '%s' bestaat al" + +#: admin/forms/domain.py:359 +msgid "Domain modified" +msgstr "Domein gewijzigd" + +#: admin/forms/domain.py:379 +msgid "Options" +msgstr "Opties" + +#: admin/forms/domain.py:387 +msgid "New domain" +msgstr "Nieuw domein" + +#: admin/forms/domain.py:405 +msgid "Domain created" +msgstr "Domein aangemaakt" + +#: admin/forms/export.py:7 +msgid "File name" +msgstr "Bestandsnaam" + +#: admin/forms/export.py:13 admin/forms/import_.py:13 +msgid "Separator" +msgstr "Scheidingsteken" + +#: admin/forms/forward.py:14 +msgid "Recipient(s)" +msgstr "Ontvanger(s)" + +#: admin/forms/forward.py:18 +msgid "Indicate one or more recipients separated by a ','" +msgstr "Geef een of meer ontvangers op, gescheiden door een komma" + +#: admin/forms/forward.py:21 +msgid "Keep local copies" +msgstr "Lokale kopie behouden" + +#: admin/forms/forward.py:24 +msgid "Forward messages and store copies into your local mailbox" +msgstr "Stuur berichten door en sla een kopie op in uw mailbox" + +#: admin/forms/forward.py:42 +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." + +#: admin/forms/import_.py:11 +msgid "Select a file" +msgstr "Bestand selecteren" + +#: admin/forms/import_.py:19 +msgid "Continue on error" +msgstr "Doorgaan als er een fout optreed" + +#: admin/forms/import_.py:20 +msgid "Don't treat duplicated objects as error" +msgstr "Zie duplicaten niet als fout" + +#: admin/forms/import_.py:38 +msgid "Crypt passwords" +msgstr "Versleutel wachtwoorden" + +#: admin/forms/import_.py:40 +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" + +#: admin/lib.py:30 +msgid "A mailbox is required" +msgstr "Een mailbox is benodigd" + +#: admin/lib.py:120 +#, fuzzy +#| msgid "You can't delete your own domain" +msgid "You are not allowed to import domains" +msgstr "U kunt uw eigen domein niet verwijderen" + +#: admin/lib.py:129 +msgid "You are not allowed to import domain aliases." +msgstr "" + +#: admin/models/alias.py:26 admin/models/mailbox.py:77 +msgid "address" +msgstr "adres" + +#: admin/models/alias.py:28 +#, fuzzy +#| msgid "The alias name" +msgid "The alias address." +msgstr "De alias naam" + +#: admin/models/alias.py:33 admin/models/domain.py:37 +#: admin/models/domain_alias.py:24 +msgid "enabled" +msgstr "actief" + +#: admin/models/alias.py:34 admin/models/domain_alias.py:25 +msgid "Check to activate this alias" +msgstr "Aanvinken om deze alias te activeren" + +#: admin/models/alias.py:79 +msgid "distribution list" +msgstr "distributie lijst" + +#: admin/models/alias.py:80 +msgid "forward" +msgstr "doorsturen" + +#: admin/models/alias.py:81 +msgid "alias" +msgstr "alias" + +#: admin/models/alias.py:112 +#, fuzzy +#| msgid "Invalid request" +msgid "Invalid address" +msgstr "Ongeldig verzoek" + +#: admin/models/alias.py:133 +#, fuzzy +#| msgid "Local recipient %s not found" +msgid "Local recipient {}@{} not found" +msgstr "Lokale ontvanger %s niet gevonden" + +#: admin/models/alias.py:162 +#, python-format +msgid "Invalid line: %s" +msgstr "Ongeldige regel: %s" + +#: admin/models/alias.py:168 +#, python-format +msgid "Domain '%s' does not exist" +msgstr "Domein '%s' bestaat niet" + +#: admin/models/domain.py:33 admin/models/domain_alias.py:17 +msgid "name" +msgstr "naam" + +#: admin/models/domain.py:34 +msgid "The domain name" +msgstr "De domeinnaam" + +#: admin/models/domain.py:38 +msgid "Check to activate this domain" +msgstr "Aanvinken om dit domein te activeren" + +#: admin/models/domain.py:160 admin/models/domain_alias.py:47 +msgid "Invalid line" +msgstr "Verkeerde regel" + +#: admin/models/domain.py:168 +#, python-format +msgid "Invalid quota value for domain '%s'" +msgstr "Verkeerde quota waarde voor domein '%s'" + +#: admin/models/domain_alias.py:18 +msgid "The alias name" +msgstr "De alias naam" + +#: admin/models/domain_alias.py:20 +msgid "target" +msgstr "doel" + +#: admin/models/domain_alias.py:21 +msgid "The domain this alias points to" +msgstr "De domein alias verwijst naar" + +#: admin/models/domain_alias.py:56 +#, python-format +msgid "Unknown domain %s" +msgstr "Onbekend domein %s" + +#: admin/models/mailbox.py:79 +msgid "Mailbox address (without the @domain.tld part)" +msgstr "Mailbox adres (zonder het @domein.naam deel)" + +#: admin/models/mailbox.py:144 +#, python-format +msgid "Failed to retrieve mailbox location (%s)" +msgstr "Mislukt om de mailbox locatie te verkijgen (%s)" + +#: admin/models/mailbox.py:233 +#, python-format +msgid "Quota is greater than the allowed domain's limit (%dM)" +msgstr "Quota is groter dan de maximaal toegestane domein limiet (%dM)" + +#: admin/models/mailbox.py:239 +msgid "A quota is required" +msgstr "Een quota is vereist" + +#: admin/templates/admin/account_general_form.html:11 +#: admin/templates/admin/aliasform.html:8 +msgid "Domain is disabled" +msgstr "Domein is gedeactiveerd" + +#: admin/templates/admin/domain_general_form.html:4 +#: admin/templates/admin/mailform.html:3 +#: admin/templates/admin/newdomainform.html:3 +msgid "MB" +msgstr "MB" + +#: admin/templates/admin/domain_options_form.html:7 +msgid "You can't create a new domain administrator" +msgstr "U kunt geen nieuwe domein administrator aanmaken" + +#: admin/templates/admin/domain_statistics.html:15 +#: admin/templatetags/admin_tags.py:45 +msgid "Statistics" +msgstr "Statistieken" + +#: admin/templates/admin/domain_statistics.html:15 +msgid "Domains and identities" +msgstr "Domeinen en identiteiten" + +#: admin/templates/admin/domain_statistics.html:22 +msgid "Global counters" +msgstr "Globale statistieken" + +#: admin/templates/admin/domain_statistics.html:27 +msgid "Quantity" +msgstr "Hoeveelheid" + +#: admin/templates/admin/domain_statistics.html:33 +#: admin/templates/admin/editdomainform.html:24 +msgid "Domain aliases" +msgstr "Domein aliassen" + +#: admin/templates/admin/domain_statistics.html:45 +msgid "Per-domain counters" +msgstr "Statistieken per domein" + +#: admin/templates/admin/domains.html:27 +#: admin/templates/admin/identity_headers.html:4 +msgid "Tags" +msgstr "Labels" + +#: admin/templates/admin/domains.html:28 +#: admin/templates/admin/identity_headers.html:5 +msgid "Actions" +msgstr "Acties" + +#: admin/templates/admin/domains_table.html:4 +msgid "disabled" +msgstr "uitgeschakeld" + +#: admin/templates/admin/editdomainform.html:4 +msgid "Administrators" +msgstr "Administrators" + +#: admin/templates/admin/editdomainform.html:7 +msgid "Miscellaneous" +msgstr "Diversen" + +#: admin/templates/admin/editdomainform.html:19 +msgid "No domain administrator defined" +msgstr "Geen domein administrator toegewezen" + +#: admin/templates/admin/editdomainform.html:22 +msgid "Creation date" +msgstr "Datum van aanmaken" + +#: admin/templates/admin/editdomainform.html:23 +msgid "Last modification date" +msgstr "Laatst gewijzigd" + +#: admin/templates/admin/editdomainform.html:26 +msgid "Mailbox aliases" +msgstr "Mailbox aliassen" + +#: admin/templates/admin/forward.html:2 +msgid "Automatically forward emails to another address" +msgstr "Automatisch mails doorsturen naar een ander adres" + +#: admin/templates/admin/forward.html:9 admin/views/alias.py:113 +msgid "Update" +msgstr "Toepassen" + +#: admin/templates/admin/identity_headers.html:3 +msgid "Fullname/Recipient" +msgstr "Volledige naam/Ontvanger" + +#: admin/templates/admin/importform.html:6 +msgid "Help" +msgstr "Help" + +#: admin/templates/admin/importform.html:13 +msgid "Importing..." +msgstr "Importeren..." + +#: admin/templates/admin/mailboxes_raw.html:3 +msgid "Choose a value" +msgstr "Kies een waarde" + +#: admin/templates/admin/mailform.html:3 +msgid "Use domain default value" +msgstr "Gebruik de standaard waarde van dit domein" + +#: admin/templates/admin/quota_headers.html:2 +msgid "Address" +msgstr "Adres" + +#: admin/templates/admin/quota_headers.html:3 +msgid "Value" +msgstr "Waarde" + +#: admin/templates/admin/quota_headers.html:4 +msgid "Limit" +msgstr "Limiet" + +#: admin/templates/admin/quota_headers.html:5 +#, python-format +msgid "Usage (%%)" +msgstr "Gebruik (%%)" + +#: admin/templates/admin/searchform.html:6 +msgid "Search" +msgstr "Zoeken" + +#: admin/templatetags/admin_tags.py:19 +msgid "enabled_m" +msgstr "geactiveerd_m" + +#: admin/templatetags/admin_tags.py:19 +msgid "enabled_f" +msgstr "geactiveerd_f" + +#: admin/templatetags/admin_tags.py:40 +msgid "List domains" +msgstr "Weergeef domeinen" + +#: admin/templatetags/admin_tags.py:53 +msgid "Add domain" +msgstr "Domein toevoegen" + +#: admin/templatetags/admin_tags.py:62 admin/templatetags/admin_tags.py:126 +#: admin/views/import_.py:99 admin/views/import_.py:133 +msgid "Import" +msgstr "Importeren" + +#: admin/templatetags/admin_tags.py:68 admin/templatetags/admin_tags.py:132 +#: admin/views/export.py:41 admin/views/export.py:69 +msgid "Export" +msgstr "Exporteren" + +#: admin/templatetags/admin_tags.py:92 +msgid "List identities" +msgstr "Weergeef identiteiten" + +#: admin/templatetags/admin_tags.py:97 +msgid "List quotas" +msgstr "Weergeef quota's" + +#: admin/templatetags/admin_tags.py:102 +msgid "Add account" +msgstr "Account aanmaken" + +#: admin/templatetags/admin_tags.py:108 +msgid "Add alias" +msgstr "Alias toevoegen" + +#: admin/templatetags/admin_tags.py:114 +msgid "Add forward" +msgstr "Doorsuurregel toevoegen" + +#: admin/templatetags/admin_tags.py:120 +msgid "Add distribution list" +msgstr "Distributielijst toevoegen" + +#: admin/templatetags/admin_tags.py:152 +msgid "View the domain's identities" +msgstr "Weergeef de identiteiten van dit domein" + +#: admin/templatetags/admin_tags.py:159 admin/templatetags/admin_tags.py:182 +#: admin/templatetags/admin_tags.py:190 +#, python-format +msgid "Delete %s?" +msgstr "Verwijder %s?" + +#: admin/templatetags/admin_tags.py:267 +msgid "Remove this permission" +msgstr "Deze bevoegdheid verwijderen" + +#: admin/views/alias.py:29 +msgid "Alias with this name already exists" +msgstr "Een alias met deze naam bestaat al" + +#: admin/views/alias.py:53 +msgid "Create" +msgstr "Aanmaken" + +#: admin/views/alias.py:65 +msgid "New distribution list" +msgstr "Nieuwe distributie lijst" + +#: admin/views/alias.py:67 +msgid "Distribution list created" +msgstr "Distributielijst aangemaakt" + +#: admin/views/alias.py:76 +msgid "New alias" +msgstr "Nieuwe alias" + +#: admin/views/alias.py:77 +msgid "Alias created" +msgstr "Alias aangemaakt" + +#: admin/views/alias.py:86 +msgid "New forward" +msgstr "Nieuwe doorstuurregel" + +#: admin/views/alias.py:87 +msgid "Forward created" +msgstr "Doorstuurregel aangemaakt" + +#: admin/views/alias.py:101 +msgid "Distribution list modified" +msgstr "Distributielijst aangepast" + +#: admin/views/alias.py:103 +msgid "Forward modified" +msgstr "Doorstuurregel aangepast" + +#: admin/views/alias.py:105 +msgid "Alias modified" +msgstr "Alias aangepast" + +#: admin/views/domain.py:132 +msgid "You can't delete your own domain" +msgstr "U kunt uw eigen domein niet verwijderen" + +#: admin/views/domain.py:135 +msgid "Domain deleted" +msgid_plural "Domains deleted" +msgstr[0] "Domeinen verwijderd" +msgstr[1] "Domeinen verwijderd" + +#: admin/views/export.py:40 +msgid "Export identities" +msgstr "Identiteiten exporteren" + +#: admin/views/export.py:68 +msgid "Export domains" +msgstr "Exporteer domeinen" + +#: admin/views/identity.py:108 admin/views/identity.py:200 +#: admin/views/identity.py:205 +msgid "Invalid request" +msgstr "Ongeldig verzoek" + +#: admin/views/identity.py:190 +msgid "Account deleted" +msgid_plural "Accounts deleted" +msgstr[0] "Account verwijderd" +msgstr[1] "Accounts verwijderd" + +#: admin/views/import_.py:64 +#, python-format +msgid "Object already exists: %s" +msgstr "Object bestaat al: %s" + +#: admin/views/import_.py:68 +#, python-format +msgid "%d objects imported successfully" +msgstr "%d objecten successvol geimporteerd" + +#: admin/views/import_.py:86 +#, python-format +msgid "" +"Provide a CSV file where lines respect one of the following formats:\n" +"
      \n" +"
    • domain; name; quota; enabled
    • \n" +"
    • domainalias; name; targeted domain; enabled
    • \n" +" %s\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" +"
    • domain; name; quota; enabled
    • \n" +"
    • domainalias; name; targeted domain; enabled
    • \n" +"%s\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" + +#: admin/views/import_.py:98 +msgid "Import domains" +msgstr "Importeer domeinen" + +#: admin/views/import_.py:120 +msgid "" +"Provide a CSV file where lines respect one of the following formats:\n" +"
      \n" +"
    • account; loginname; password; first name; last name; enabled; group; " +"address; quota; [, domain, ...]
    • \n" +"
    • alias; address; enabled; internal recipient
    • \n" +"
    • forward; address; enabled; external recipient
    • \n" +"
    • dlist; address; enabled; recipient; recipient; ...
    • \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" +"
    • account; loginname; password; first name; last name; enabled; group; " +"address; quota; [, domain, ...]
    • \n" +"
    • alias; address; enabled; internal recipient
    • \n" +"
    • forward; address; enabled; external recipient
    • \n" +"
    • dlist; address; enabled; recipient; recipient; ...
    • \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" + +#: admin/views/import_.py:132 +msgid "Import identities" +msgstr "Identiteiten importeren" + +#: admin/views/user.py:34 +msgid "Forward updated" +msgstr "Doorstuuradres aangepast" diff --git a/modoboa/admin/locale/nl_NL/LC_MESSAGES/djangojs.po b/modoboa/admin/locale/nl_NL/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..ecc37122e --- /dev/null +++ b/modoboa/admin/locale/nl_NL/LC_MESSAGES/djangojs.po @@ -0,0 +1,59 @@ +# 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 +# TuxBrother , 2014 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 08:20+0100\n" +"PO-Revision-Date: 2014-12-31 11:42+0000\n" +"Last-Translator: TuxBrother \n" +"Language-Team: Dutch (Netherlands) (http://www.transifex.com/projects/p/" +"modoboa/language/nl_NL/)\n" +"Language: nl_NL\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: static/admin/js/admin.js:126 +msgid "No more domain to show" +msgstr "Geen domein meer om te weergeven" + +#: static/admin/js/admin.js:145 +msgid "Do not delete domain directory" +msgstr "De map van dit domein niet verwijderen (Zal de data ervan behouden)" + +#: static/admin/js/admin.js:147 +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?" + +#: static/admin/js/admin.js:148 +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." + +#: static/admin/js/admin.js:231 +msgid "No domain administrator defined" +msgstr "Geen administrator toegewezen" + +#: static/admin/js/admin.js:286 static/admin/js/admin.js.c:354 +msgid "No more identity to show" +msgstr "Geen identiteiten meer om te weergeven" + +#: static/admin/js/admin.js:328 +msgid "Do not delete mailbox directory" +msgstr "Verwijder de map van deze mailbox niet (zal de data ervan behouden)" + +#: static/admin/js/admin.js:357 +msgid "No more quota to show" +msgstr "Geen quota meer om te weergeven" diff --git a/modoboa/admin/locale/pt_BR/LC_MESSAGES/django.po b/modoboa/admin/locale/pt_BR/LC_MESSAGES/django.po new file mode 100644 index 000000000..7ed786671 --- /dev/null +++ b/modoboa/admin/locale/pt_BR/LC_MESSAGES/django.po @@ -0,0 +1,871 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# csmk, 2014 +# Paulino Michelazzo , 2014 +# Rafael Barretto Alves , 2013,2015 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: 2015-09-09 08:30+0000\n" +"Last-Translator: Antoine Nguyen \n" +"Language-Team: Portuguese (Brazil) (http://www.transifex.com/tonio/modoboa/" +"language/pt_BR/)\n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: admin/app_settings.py:54 admin/templates/admin/editdomainform.html:25 +msgid "Mailboxes" +msgstr "Caixas de correios" + +#: admin/app_settings.py:57 +msgid "Handle mailboxes on filesystem" +msgstr "Tratar caixas de mensagens no sistema de arquivos." + +#: admin/app_settings.py:60 +msgid "" +"Rename or remove mailboxes on the filesystem when they get renamed or " +"removed within Modoboa" +msgstr "" +"Renomeia ou remove caixas de mensagens do sistema de arquivos quando elas " +"são renomeadas ou removidas dentro do Modoboa" + +#: admin/app_settings.py:66 +msgid "Mailboxes owner" +msgstr "Proprietário de caixas de mensagens" + +#: admin/app_settings.py:69 +msgid "The UNIX account who owns mailboxes on the filesystem" +msgstr "A conta UNIX que possui caixas de mensagens no sistema de arquivos" + +#: admin/app_settings.py:74 +msgid "Default domain quota" +msgstr "Cota padrão do domínio" + +#: admin/app_settings.py:77 +msgid "" +"Default quota (in MB) applied to freshly created domains with no value " +"specified. A value of 0 means no quota." +msgstr "" +"Cota padrão (em MB) aplicada para novos domínios sem nenhum valor " +"especificado. Um valor de 0 indica que não existe cota." + +#: admin/app_settings.py:83 +msgid "Automatic account removal" +msgstr "Remoção automática da conta" + +#: admin/app_settings.py:86 +msgid "When a mailbox is removed, also remove the associated account" +msgstr "" +"Quando uma caixa de correio é removida, também remove a conta associada" + +#: admin/app_settings.py:90 +msgid "Automatic domain/mailbox creation" +msgstr "Criação automática de domínio/caixa de correio" + +#: admin/app_settings.py:93 +msgid "Create a domain and a mailbox when an account is automatically created." +msgstr "" +"Criar um domínio e uma caixa de correio quando uma conta é automaticamente " +"criada." + +#: admin/app_settings.py:139 admin/forms/account.py:239 +#: admin/forms/domain.py:77 +msgid "Must be a positive integer" +msgstr "Deve ser um inteiro positivo" + +#: admin/app_settings.py:149 +msgid "Administration" +msgstr "Administração" + +#: admin/callbacks.py:46 +msgid "Domain administrator" +msgstr "Administrador de domínio" + +#: admin/callbacks.py:75 admin/templates/admin/domain_statistics.html:30 +#: admin/templates/admin/domains.html:3 +msgid "Domains" +msgstr "Domínios" + +#: admin/callbacks.py:85 admin/templates/admin/domain_statistics.html:36 +#: admin/templates/admin/domain_statistics.html:50 +#: admin/templates/admin/identities.html:5 +msgid "Identities" +msgstr "Identidades" + +#: admin/callbacks.py:100 admin/templates/admin/forward.html:2 +msgid "Forward" +msgstr "Encaminhar" + +#: admin/callbacks.py:163 +#, python-format +msgid "Account import failed (%s): domain does not exist" +msgstr "Falha ao importar a conta (%s): domínio não existe" + +#: admin/callbacks.py:173 admin/forms/account.py:257 +#, python-format +msgid "Mailbox %s already exists" +msgstr "A caixa %s já existe" + +#: admin/callbacks.py:181 +#, python-format +msgid "Account import failed (%s): wrong quota value" +msgstr "Falha na importação de conta (%s): valor de cota errado" + +#: admin/callbacks.py:216 admin/forms/domain.py:94 +msgid "domain alias" +msgstr "alias de domínio" + +#: admin/forms/account.py:30 admin/templates/admin/editdomainform.html:13 +msgid "Username" +msgstr "Nome de usuário" + +#: admin/forms/account.py:32 +msgid "" +"The user's name. Must be a valid e-mail address for simple users or " +"administrators with a mailbox." +msgstr "" +"O nome de usuário. Deve ser um endereço de e-mail válido para usuários " +"simples ou administradores com uma caixa de mensagens." + +#: admin/forms/account.py:37 +msgid "Role" +msgstr "Papel" + +#: admin/forms/account.py:38 admin/forms/account.py:75 +msgid "Choose" +msgstr "Escolher" + +#: admin/forms/account.py:39 +msgid "What level of permission this user will have" +msgstr "Qual nível de permissão terá esse usuário" + +#: admin/forms/account.py:42 +msgid "Password" +msgstr "Senha" + +#: admin/forms/account.py:45 +msgid "Confirmation" +msgstr "Confirmação" + +#: admin/forms/account.py:48 +msgid "Enter the same password as above, for verification." +msgstr "Para verificação, digite a mesma senha que a anterior." + +#: admin/forms/account.py:66 +msgid "Enabled" +msgstr "Ativado" + +#: admin/forms/account.py:141 +msgid "Only super administrators are allowed for this mode" +msgstr "Apenas super administradores estão liberados para este modo" + +#: admin/forms/account.py:150 +msgid "The two password fields didn't match." +msgstr "Os dois campos de senha não coincidiram." + +#: admin/forms/account.py:156 +msgid "You can't disable your own account" +msgstr "Você não pode desabilitar seu própria conta" + +#: admin/forms/account.py:170 +msgid "E-mail" +msgstr "E-mail" + +#: admin/forms/account.py:172 admin/forms/domain.py:35 +msgid "Quota" +msgstr "Cota" + +#: admin/forms/account.py:174 +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 "" +"Cota em MB para esta caixa de mensagens. Defina um valor customizado ou use " +"o padrão do domínio. Deixe em branco para definir um valor ilimitado (não " +"permitido para administradores de domínios)." + +#: admin/forms/account.py:182 admin/forms/domain.py:43 +#: admin/templates/admin/domains.html:26 +msgid "Alias(es)" +msgstr "Alias(es)" + +#: admin/forms/account.py:185 +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(es) para essa caixa de correio. Indique apenas um endereço por " +"entrada, pressione ENTER para adicionar uma nova entrada. Use o caractere " +"'*' para criar um alias 'pega-todos' (ex: *@exemplo.com)." + +#: admin/forms/account.py:248 admin/forms/account.py:279 +#: admin/forms/alias.py:72 +msgid "Domain does not exist" +msgstr "Domínio não existe" + +#: admin/forms/account.py:355 +msgid "Domain(s)" +msgstr "Domínio(s)" + +#: admin/forms/account.py:357 +msgid "Domain(s) that user administrates" +msgstr "Domínio(s) que o usuário administra" + +#: admin/forms/account.py:401 admin/forms/account.py:487 +#: admin/forms/domain.py:300 admin/forms/domain.py:370 +msgid "General" +msgstr "Geral" + +#: admin/forms/account.py:406 admin/forms/account.py:493 +msgid "Mail" +msgstr "Correio" + +#: admin/forms/account.py:409 +msgid "Permissions" +msgstr "Permissões" + +#: admin/forms/account.py:476 +msgid "Account updated" +msgstr "Conta atualizada" + +#: admin/forms/account.py:500 +msgid "New account" +msgstr "Nova conta" + +#: admin/forms/account.py:512 +msgid "Account created" +msgstr "Conta criada" + +#: admin/forms/alias.py:23 +msgid "Email address" +msgstr "Endereço de e-mail" + +#: admin/forms/alias.py:25 +msgid "" +"The alias address. To create a catchall alias, just enter the domain name " +"(@domain.tld)." +msgstr "" + +#: admin/forms/alias.py:31 +msgid "Recipients" +msgstr "Destinatários" + +#: admin/forms/alias.py:33 +msgid "" +"Addresses this alias will point to. Indicate only one address per input, " +"press ENTER to add a new input." +msgstr "" + +#: admin/forms/alias.py:75 +msgid "You don't have access to this domain" +msgstr "Você não tem acesso a esse domínio" + +#: admin/forms/alias.py:85 +msgid "No recipient defined" +msgstr "Nenhum destinatário definido" + +#: admin/forms/domain.py:22 admin/models/domain.py:76 +#: admin/templates/admin/domain_statistics.html:49 +msgid "Domain" +msgstr "Domínio" + +#: admin/forms/domain.py:32 admin/templates/admin/domain_statistics.html:26 +msgid "Type" +msgstr "Tipo" + +#: admin/forms/domain.py:38 +msgid "" +"Default quota in MB applied to mailboxes. Leave empty to use the default " +"value." +msgstr "" +"Cota padrão em MB aplicada para caixas de mensagens. Deixe em branco para " +"usar o valor padrão." + +#: admin/forms/domain.py:46 +msgid "" +"Alias(es) of this domain. Indicate only one name per input, press ENTER to " +"add a new input." +msgstr "" +"Alias(es) para esse domínio. Indique apenas um nome por entrada, pressione " +"ENTER para adicionar uma nova entrada." + +#: admin/forms/domain.py:97 admin/forms/domain.py:108 +#: admin/forms/domain.py:116 +#, python-format +msgid "A %s with this name already exists" +msgstr "Já existe um %s com este nome" + +#: admin/forms/domain.py:108 admin/forms/domain.py:113 +msgid "domain" +msgstr "domínio" + +#: admin/forms/domain.py:200 +msgid "Create a domain administrator" +msgstr "Criar um administrador de domínio" + +#: admin/forms/domain.py:203 +msgid "Automatically create an administrator for this domain" +msgstr "Criar automaticamente um administrador para este domínio" + +#: admin/forms/domain.py:208 admin/templates/admin/domains.html:25 +#: admin/templates/admin/editdomainform.html:13 +#: admin/templates/admin/identity_headers.html:2 +msgid "Name" +msgstr "Nome" + +#: admin/forms/domain.py:211 +msgid "" +"The administrator's name. Don't include the domain's name here, it will be " +"automatically appended." +msgstr "" +"O nome de administrador. Não inclua aqui o nome de domínio. Ele será " +"automaticamente adicionado." + +#: admin/forms/domain.py:218 +msgid "Create aliases" +msgstr "Criar alias" + +#: admin/forms/domain.py:221 +msgid "Automatically create standard aliases for this domain" +msgstr "Criar automaticamente aliases padrão para este domínio" + +#: admin/forms/domain.py:240 +msgid "Invalid format" +msgstr "Formato inválido" + +#: admin/forms/domain.py:257 +#, python-format +msgid "User '%s' already exists" +msgstr "O usuário '%s' já existe" + +#: admin/forms/domain.py:359 +msgid "Domain modified" +msgstr "Domínio alterado" + +#: admin/forms/domain.py:379 +msgid "Options" +msgstr "Opções" + +#: admin/forms/domain.py:387 +msgid "New domain" +msgstr "Novo domínio" + +#: admin/forms/domain.py:405 +msgid "Domain created" +msgstr "Domínio criado" + +#: admin/forms/export.py:7 +msgid "File name" +msgstr "Nome do arquivo" + +#: admin/forms/export.py:13 admin/forms/import_.py:13 +msgid "Separator" +msgstr "Separador" + +#: admin/forms/forward.py:14 +msgid "Recipient(s)" +msgstr "Destinatário(s)" + +#: admin/forms/forward.py:18 +msgid "Indicate one or more recipients separated by a ','" +msgstr "Indique um ou mais destinatários separados por uma ','" + +#: admin/forms/forward.py:21 +msgid "Keep local copies" +msgstr "Manter cópias locais" + +#: admin/forms/forward.py:24 +msgid "Forward messages and store copies into your local mailbox" +msgstr "Encaminhar mensagens e armazenar cópias em sua caixa de correio local" + +#: admin/forms/forward.py:42 +msgid "" +"You can't define a forward to a local destination. Please ask your " +"administrator to create an alias instead." +msgstr "" +"Você não pode definir um encaminhamento para um local de destino. Por favor, " +"ao invés peça ao seu administrador para criar um alias." + +#: admin/forms/import_.py:11 +msgid "Select a file" +msgstr "Selecione um arquivo" + +#: admin/forms/import_.py:19 +msgid "Continue on error" +msgstr "Continuar em caso de erro" + +#: admin/forms/import_.py:20 +msgid "Don't treat duplicated objects as error" +msgstr "Não tratar como erro objetos duplicados" + +#: admin/forms/import_.py:38 +msgid "Crypt passwords" +msgstr "Criptografar senhas" + +#: admin/forms/import_.py:40 +msgid "Check this option if passwords contained in your file are not crypted" +msgstr "" +"Marcar essa opção se as senhas contidas no seu arquivo estão criptografadas" + +#: admin/lib.py:30 +msgid "A mailbox is required" +msgstr "Um caixa de mensagens é obrigatória" + +#: admin/lib.py:120 +msgid "You are not allowed to import domains" +msgstr "Você não tem permissão para importar domínios" + +#: admin/lib.py:129 +msgid "You are not allowed to import domain aliases." +msgstr "Você não tem permissão para importar aliases de domínio." + +#: admin/models/alias.py:26 admin/models/mailbox.py:77 +msgid "address" +msgstr "endereço" + +#: admin/models/alias.py:28 +msgid "The alias address." +msgstr "" + +#: admin/models/alias.py:33 admin/models/domain.py:37 +#: admin/models/domain_alias.py:24 +msgid "enabled" +msgstr "ativado" + +#: admin/models/alias.py:34 admin/models/domain_alias.py:25 +msgid "Check to activate this alias" +msgstr "Verificar para ativar o alias" + +#: admin/models/alias.py:79 +msgid "distribution list" +msgstr "lista de distribuição" + +#: admin/models/alias.py:80 +msgid "forward" +msgstr "encaminhar" + +#: admin/models/alias.py:81 +msgid "alias" +msgstr "alias" + +#: admin/models/alias.py:112 +msgid "Invalid address" +msgstr "" + +#: admin/models/alias.py:133 +msgid "Local recipient {}@{} not found" +msgstr "Destinatário local {}@{} não encontrado" + +#: admin/models/alias.py:162 +#, python-format +msgid "Invalid line: %s" +msgstr "Linha inválida: %s" + +#: admin/models/alias.py:168 +#, python-format +msgid "Domain '%s' does not exist" +msgstr "O domínio '%s' não existe" + +#: admin/models/domain.py:33 admin/models/domain_alias.py:17 +msgid "name" +msgstr "nome" + +#: admin/models/domain.py:34 +msgid "The domain name" +msgstr "Nome do domínio" + +#: admin/models/domain.py:38 +msgid "Check to activate this domain" +msgstr "Marque para ativar esse domínio" + +#: admin/models/domain.py:160 admin/models/domain_alias.py:47 +msgid "Invalid line" +msgstr "Linha inválida" + +#: admin/models/domain.py:168 +#, python-format +msgid "Invalid quota value for domain '%s'" +msgstr "Valor de cota inválido para o domínio '%s'" + +#: admin/models/domain_alias.py:18 +msgid "The alias name" +msgstr "Nome de alias" + +#: admin/models/domain_alias.py:20 +msgid "target" +msgstr "alvo" + +#: admin/models/domain_alias.py:21 +msgid "The domain this alias points to" +msgstr "Esse alias aponta para o domínio" + +#: admin/models/domain_alias.py:56 +#, python-format +msgid "Unknown domain %s" +msgstr "Domínio desconhecido %s" + +#: admin/models/mailbox.py:79 +msgid "Mailbox address (without the @domain.tld part)" +msgstr "Endereço da caixa de correio (sem a parte @meu.dominio)" + +#: admin/models/mailbox.py:144 +#, python-format +msgid "Failed to retrieve mailbox location (%s)" +msgstr "Falha ao recuperar a localização da caixa de mensagens (%s)" + +#: admin/models/mailbox.py:233 +#, python-format +msgid "Quota is greater than the allowed domain's limit (%dM)" +msgstr "A cota é maior que o permitido pelo limite do domínio (%dM)" + +#: admin/models/mailbox.py:239 +msgid "A quota is required" +msgstr "Uma cota é requerida" + +#: admin/templates/admin/account_general_form.html:11 +#: admin/templates/admin/aliasform.html:8 +msgid "Domain is disabled" +msgstr "Domínio está desabilitado" + +#: admin/templates/admin/domain_general_form.html:4 +#: admin/templates/admin/mailform.html:3 +#: admin/templates/admin/newdomainform.html:3 +msgid "MB" +msgstr "MB" + +#: admin/templates/admin/domain_options_form.html:7 +msgid "You can't create a new domain administrator" +msgstr "Você não pode criar um novo administrador de domínio" + +#: admin/templates/admin/domain_statistics.html:15 +#: admin/templatetags/admin_tags.py:45 +msgid "Statistics" +msgstr "Estatísticas" + +#: admin/templates/admin/domain_statistics.html:15 +msgid "Domains and identities" +msgstr "Domínios e identidades" + +#: admin/templates/admin/domain_statistics.html:22 +msgid "Global counters" +msgstr "Contadores globais" + +#: admin/templates/admin/domain_statistics.html:27 +msgid "Quantity" +msgstr "Quantidade" + +#: admin/templates/admin/domain_statistics.html:33 +#: admin/templates/admin/editdomainform.html:24 +msgid "Domain aliases" +msgstr "Alias de domínios" + +#: admin/templates/admin/domain_statistics.html:45 +msgid "Per-domain counters" +msgstr "Contadores por domínios" + +#: admin/templates/admin/domains.html:27 +#: admin/templates/admin/identity_headers.html:4 +msgid "Tags" +msgstr "Tags" + +#: admin/templates/admin/domains.html:28 +#: admin/templates/admin/identity_headers.html:5 +msgid "Actions" +msgstr "Ações" + +#: admin/templates/admin/domains_table.html:4 +msgid "disabled" +msgstr "desabilitado" + +#: admin/templates/admin/editdomainform.html:4 +msgid "Administrators" +msgstr "Administradores" + +#: admin/templates/admin/editdomainform.html:7 +msgid "Miscellaneous" +msgstr "Diversos" + +#: admin/templates/admin/editdomainform.html:19 +msgid "No domain administrator defined" +msgstr "Nenhum administrador de domínio definido" + +#: admin/templates/admin/editdomainform.html:22 +msgid "Creation date" +msgstr "Data de criação" + +#: admin/templates/admin/editdomainform.html:23 +msgid "Last modification date" +msgstr "Data da última modificação" + +#: admin/templates/admin/editdomainform.html:26 +msgid "Mailbox aliases" +msgstr "Alias de caixas de correio" + +#: admin/templates/admin/forward.html:2 +msgid "Automatically forward emails to another address" +msgstr "Encaminhar e-mails automaticamente para outro endereço" + +#: admin/templates/admin/forward.html:9 admin/views/alias.py:113 +msgid "Update" +msgstr "Atualizar" + +#: admin/templates/admin/identity_headers.html:3 +msgid "Fullname/Recipient" +msgstr "Nome completo/Destinatário" + +#: admin/templates/admin/importform.html:6 +msgid "Help" +msgstr "Ajuda" + +#: admin/templates/admin/importform.html:13 +msgid "Importing..." +msgstr "Importando..." + +#: admin/templates/admin/mailboxes_raw.html:3 +msgid "Choose a value" +msgstr "Escolher um valor" + +#: admin/templates/admin/mailform.html:3 +msgid "Use domain default value" +msgstr "Usar valor padrão do domínio" + +#: admin/templates/admin/quota_headers.html:2 +msgid "Address" +msgstr "Endereço" + +#: admin/templates/admin/quota_headers.html:3 +msgid "Value" +msgstr "Valor" + +#: admin/templates/admin/quota_headers.html:4 +msgid "Limit" +msgstr "Limite" + +#: admin/templates/admin/quota_headers.html:5 +#, python-format +msgid "Usage (%%)" +msgstr "Uso (%%)" + +#: admin/templates/admin/searchform.html:6 +msgid "Search" +msgstr "Busca" + +#: admin/templatetags/admin_tags.py:19 +msgid "enabled_m" +msgstr "Ativado" + +#: admin/templatetags/admin_tags.py:19 +msgid "enabled_f" +msgstr "Ativada" + +#: admin/templatetags/admin_tags.py:40 +msgid "List domains" +msgstr "Listardomínios" + +#: admin/templatetags/admin_tags.py:53 +msgid "Add domain" +msgstr "Adicionar domínio" + +#: admin/templatetags/admin_tags.py:62 admin/templatetags/admin_tags.py:126 +#: admin/views/import_.py:99 admin/views/import_.py:133 +msgid "Import" +msgstr "Importar" + +#: admin/templatetags/admin_tags.py:68 admin/templatetags/admin_tags.py:132 +#: admin/views/export.py:41 admin/views/export.py:69 +msgid "Export" +msgstr "Exportar" + +#: admin/templatetags/admin_tags.py:92 +msgid "List identities" +msgstr "Listar identidades" + +#: admin/templatetags/admin_tags.py:97 +msgid "List quotas" +msgstr "Listar quotas" + +#: admin/templatetags/admin_tags.py:102 +msgid "Add account" +msgstr "Adicionar conta" + +#: admin/templatetags/admin_tags.py:108 +msgid "Add alias" +msgstr "Adicionar alias" + +#: admin/templatetags/admin_tags.py:114 +msgid "Add forward" +msgstr "Adicionar encaminhamento" + +#: admin/templatetags/admin_tags.py:120 +msgid "Add distribution list" +msgstr "Adicionar lista de distribuição" + +#: admin/templatetags/admin_tags.py:152 +msgid "View the domain's identities" +msgstr "Ver as identidades de domínios" + +#: admin/templatetags/admin_tags.py:159 admin/templatetags/admin_tags.py:182 +#: admin/templatetags/admin_tags.py:190 +#, python-format +msgid "Delete %s?" +msgstr "Remover %s?" + +#: admin/templatetags/admin_tags.py:267 +msgid "Remove this permission" +msgstr "Remover essa persmissão" + +#: admin/views/alias.py:29 +msgid "Alias with this name already exists" +msgstr "Um alias para esse nome já existe" + +#: admin/views/alias.py:53 +msgid "Create" +msgstr "Criar" + +#: admin/views/alias.py:65 +msgid "New distribution list" +msgstr "Nova lista de distribuição" + +#: admin/views/alias.py:67 +msgid "Distribution list created" +msgstr "Lista de distribuição criada" + +#: admin/views/alias.py:76 +msgid "New alias" +msgstr "Novo alias" + +#: admin/views/alias.py:77 +msgid "Alias created" +msgstr "Alias criado" + +#: admin/views/alias.py:86 +msgid "New forward" +msgstr "Novo encaminhamento" + +#: admin/views/alias.py:87 +msgid "Forward created" +msgstr "Encaminhamento criado" + +#: admin/views/alias.py:101 +msgid "Distribution list modified" +msgstr "Lista de distribuição modificada" + +#: admin/views/alias.py:103 +msgid "Forward modified" +msgstr "Encaminhamento modificado" + +#: admin/views/alias.py:105 +msgid "Alias modified" +msgstr "Alias modificado" + +#: admin/views/domain.py:132 +msgid "You can't delete your own domain" +msgstr "Você não pode deletar seu próprio domínio" + +#: admin/views/domain.py:135 +msgid "Domain deleted" +msgid_plural "Domains deleted" +msgstr[0] "Domínio deletado" +msgstr[1] "Domínios deletados" + +#: admin/views/export.py:40 +msgid "Export identities" +msgstr "Exportar identidades" + +#: admin/views/export.py:68 +msgid "Export domains" +msgstr "Exportar domínios" + +#: admin/views/identity.py:108 admin/views/identity.py:200 +#: admin/views/identity.py:205 +msgid "Invalid request" +msgstr "Solicitação inválida" + +#: admin/views/identity.py:190 +msgid "Account deleted" +msgid_plural "Accounts deleted" +msgstr[0] "Conta deletada" +msgstr[1] "Contas deletadas" + +#: admin/views/import_.py:64 +#, python-format +msgid "Object already exists: %s" +msgstr "O objeto já existe: %s" + +#: admin/views/import_.py:68 +#, python-format +msgid "%d objects imported successfully" +msgstr "%d objetos importados com sucesso" + +#: admin/views/import_.py:86 +#, python-format +msgid "" +"Provide a CSV file where lines respect one of the following formats:\n" +"
      \n" +"
    • domain; name; quota; enabled
    • \n" +"
    • domainalias; name; targeted domain; enabled
    • \n" +" %s\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 "" +"Forneça um arquivo CSV onde as linhas respeitam um dos seguintes formatos:\n" +"
      \n" +"
    • domain; name; cota; enabled
    • \n" +"
    • domainalias; name; targeted domain; enabled
    • \n" +" %s\n" +"
    \n" +"

    O primeiro elemento de cada linha é mandatório e deve ser igual a um dos " +"valores indicados.

    \n" +"

    Você pode usar um caractere diferente como separador.

    \n" + +#: admin/views/import_.py:98 +msgid "Import domains" +msgstr "Importar domínios" + +#: admin/views/import_.py:120 +msgid "" +"Provide a CSV file where lines respect one of the following formats:\n" +"
      \n" +"
    • account; loginname; password; first name; last name; enabled; group; " +"address; quota; [, domain, ...]
    • \n" +"
    • alias; address; enabled; internal recipient
    • \n" +"
    • forward; address; enabled; external recipient
    • \n" +"
    • dlist; address; enabled; recipient; recipient; ...
    • \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 "" +"Forneça um arquivo CSV onde as linhas respeitam um dos seguintes formatos:\n" +"
      \n" +"
    • account; loginname; password; first name; last name; enabled; group; " +"address; cota; [, domain, ...]
    • \n" +"
    • alias; address; enabled; internal recipient
    • \n" +"
    • forward; address; enabled; external recipient
    • \n" +"
    • dlist; address; enabled; recipient; recipient; ...
    • \n" +"
    \n" +"

    O primeiro elemento de cada linha é mandatório e deve ser igual a um dos " +"valores indicados.

    \n" +"\n" +"

    Você pode usar um caractere diferente como separador.

    \n" + +#: admin/views/import_.py:132 +msgid "Import identities" +msgstr "Importar identidades" + +#: admin/views/user.py:34 +msgid "Forward updated" +msgstr "Encaminhamento atualizado" diff --git a/modoboa/admin/locale/pt_BR/LC_MESSAGES/djangojs.po b/modoboa/admin/locale/pt_BR/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..a201a4fdb --- /dev/null +++ b/modoboa/admin/locale/pt_BR/LC_MESSAGES/djangojs.po @@ -0,0 +1,56 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Paulino Michelazzo , 2014 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 08:20+0100\n" +"PO-Revision-Date: 2015-08-10 20:19+0000\n" +"Last-Translator: Rafael Barretto Alves \n" +"Language-Team: Portuguese (Brazil) (http://www.transifex.com/tonio/modoboa/" +"language/pt_BR/)\n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: static/admin/js/admin.js:126 +msgid "No more domain to show" +msgstr "Nenhum domínio a mais para mostrar" + +#: static/admin/js/admin.js:145 +msgid "Do not delete domain directory" +msgstr "Não remova o diretório de domínio" + +#: static/admin/js/admin.js:147 +msgid "This operation will remove ALL data associated to this domain." +msgstr "Esta operação removerá TODOS os dados associados com este domínio." + +#: static/admin/js/admin.js:148 +msgid "" +"This operation will remove all data associated to this domain, excepting " +"accounts." +msgstr "" +"Esta operação removerá todos os dados associados com este domínio, exceto " +"contas." + +#: static/admin/js/admin.js:231 +msgid "No domain administrator defined" +msgstr "Nenhum administrador de domínio definido" + +#: static/admin/js/admin.js:286 static/admin/js/admin.js.c:354 +msgid "No more identity to show" +msgstr "Nenhuma identidade a mais para mostrar" + +#: static/admin/js/admin.js:328 +msgid "Do not delete mailbox directory" +msgstr "Não remova o diretório de caixas de mensagens" + +#: static/admin/js/admin.js:357 +msgid "No more quota to show" +msgstr "Nenhuma quota a mais para mostrar" diff --git a/modoboa/admin/locale/pt_PT/LC_MESSAGES/django.po b/modoboa/admin/locale/pt_PT/LC_MESSAGES/django.po new file mode 100644 index 000000000..477b0df60 --- /dev/null +++ b/modoboa/admin/locale/pt_PT/LC_MESSAGES/django.po @@ -0,0 +1,873 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Damien Duponchelle , 2013 +# lusitan , 2013 +# Mike C. , 2012 +# Mike C. , 2013-2014 +# Sandra Ribeiro , 2012-2013 +# Sandra Ribeiro , 2012 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: 2014-12-29 12:51+0000\n" +"Last-Translator: Mike C. \n" +"Language-Team: Portuguese (Portugal) (http://www.transifex.com/projects/p/" +"modoboa/language/pt_PT/)\n" +"Language: pt_PT\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: admin/app_settings.py:54 admin/templates/admin/editdomainform.html:25 +msgid "Mailboxes" +msgstr "Caixas de Correio" + +#: admin/app_settings.py:57 +msgid "Handle mailboxes on filesystem" +msgstr "Lidar com caixas de correio no sistema de ficheiros" + +#: admin/app_settings.py:60 +msgid "" +"Rename or remove mailboxes on the filesystem when they get renamed or " +"removed within Modoboa" +msgstr "" +"Renomear ou remover as caixas de correio no sistema de ficheiros quando " +"estas são renomeadas no Modoboa" + +#: admin/app_settings.py:66 +msgid "Mailboxes owner" +msgstr "Dono das caixas de correio" + +#: admin/app_settings.py:69 +msgid "The UNIX account who owns mailboxes on the filesystem" +msgstr "A conta UNIX que detém as caixas de correio no sistema de ficheiros" + +#: admin/app_settings.py:74 +msgid "Default domain quota" +msgstr "Quota pré-definida para o domínio" + +#: admin/app_settings.py:77 +msgid "" +"Default quota (in MB) applied to freshly created domains with no value " +"specified. A value of 0 means no quota." +msgstr "" +"Quota (em MB) a aplicar a domínios recentemente criados sem valor " +"especificado. Um valor de 0 implica sem quota." + +#: admin/app_settings.py:83 +msgid "Automatic account removal" +msgstr "Remoção automática de conta" + +#: admin/app_settings.py:86 +msgid "When a mailbox is removed, also remove the associated account" +msgstr "" +"Ao remover uma caixa de correio estará também a remover a conta associada" + +#: admin/app_settings.py:90 +msgid "Automatic domain/mailbox creation" +msgstr "Criação automatica de domino/caixa de correio" + +#: admin/app_settings.py:93 +msgid "Create a domain and a mailbox when an account is automatically created." +msgstr "" +"Criar um dominio e uma caixa de correio quando uma conta é criada " +"automátiamente." + +#: admin/app_settings.py:139 admin/forms/account.py:239 +#: admin/forms/domain.py:77 +msgid "Must be a positive integer" +msgstr "Tem de ser um inteiro, positivo" + +#: admin/app_settings.py:149 +msgid "Administration" +msgstr "Administração" + +#: admin/callbacks.py:46 +#, fuzzy +#| msgid "Create a domain administrator" +msgid "Domain administrator" +msgstr "Criar um administrador de domínio" + +#: admin/callbacks.py:75 admin/templates/admin/domain_statistics.html:30 +#: admin/templates/admin/domains.html:3 +msgid "Domains" +msgstr "Domínios" + +#: admin/callbacks.py:85 admin/templates/admin/domain_statistics.html:36 +#: admin/templates/admin/domain_statistics.html:50 +#: admin/templates/admin/identities.html:5 +msgid "Identities" +msgstr "Identidades" + +#: admin/callbacks.py:100 admin/templates/admin/forward.html:2 +msgid "Forward" +msgstr "Reencaminhar" + +#: admin/callbacks.py:163 +#, python-format +msgid "Account import failed (%s): domain does not exist" +msgstr "Importação da conta falhou (%s): domínio não existe" + +#: admin/callbacks.py:173 admin/forms/account.py:257 +#, python-format +msgid "Mailbox %s already exists" +msgstr "Caixa de correio %s já existe" + +#: admin/callbacks.py:181 +#, python-format +msgid "Account import failed (%s): wrong quota value" +msgstr "Importação da conta falhou (%s): valor para quota inválido" + +#: admin/callbacks.py:216 admin/forms/domain.py:94 +msgid "domain alias" +msgstr "'Alias' de domínio" + +#: admin/forms/account.py:30 admin/templates/admin/editdomainform.html:13 +msgid "Username" +msgstr "Nome de Utilizador" + +#: admin/forms/account.py:32 +msgid "" +"The user's name. Must be a valid e-mail address for simple users or " +"administrators with a mailbox." +msgstr "" + +#: admin/forms/account.py:37 +msgid "Role" +msgstr "Papel" + +#: admin/forms/account.py:38 admin/forms/account.py:75 +msgid "Choose" +msgstr "Escolher" + +#: admin/forms/account.py:39 +msgid "What level of permission this user will have" +msgstr "Que nível de permissões terá o utilizador" + +#: admin/forms/account.py:42 +msgid "Password" +msgstr "Password" + +#: admin/forms/account.py:45 +msgid "Confirmation" +msgstr "Confirmação" + +#: admin/forms/account.py:48 +msgid "Enter the same password as above, for verification." +msgstr "Repita a password acima, para verificação." + +#: admin/forms/account.py:66 +msgid "Enabled" +msgstr "Ativo" + +#: admin/forms/account.py:141 +msgid "Only super administrators are allowed for this mode" +msgstr "" + +#: admin/forms/account.py:150 +msgid "The two password fields didn't match." +msgstr "Os dois campos de password não são idênticos." + +#: admin/forms/account.py:156 +msgid "You can't disable your own account" +msgstr "Não pode desativar a sua própria conta" + +#: admin/forms/account.py:170 +msgid "E-mail" +msgstr "E-mail" + +#: admin/forms/account.py:172 admin/forms/domain.py:35 +msgid "Quota" +msgstr "Quota" + +#: admin/forms/account.py:174 +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 em MB para esta caixa de correio. Definir uma valor personalizado ou " +"usar o valor predefinido pelo domínio. Deixar em branco para definir uma " +"valor ilimitado (não permitido para administradores do domínio)." + +#: admin/forms/account.py:182 admin/forms/domain.py:43 +#: admin/templates/admin/domains.html:26 +msgid "Alias(es)" +msgstr "Alias(es)" + +#: admin/forms/account.py:185 +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(es) desta caixa de correio. Indique apenas um endereço por entrada, " +"clique ENTER para adicionar uma nova entrada. Use o '*' para criar um alias " +"'catchall' (ex: *@domain.tld)." + +#: admin/forms/account.py:248 admin/forms/account.py:279 +#: admin/forms/alias.py:72 +msgid "Domain does not exist" +msgstr "O domínio não existe" + +#: admin/forms/account.py:355 +msgid "Domain(s)" +msgstr "Domínio(s)" + +#: admin/forms/account.py:357 +msgid "Domain(s) that user administrates" +msgstr "Domínio(s) administrado pelo utilizador" + +#: admin/forms/account.py:401 admin/forms/account.py:487 +#: admin/forms/domain.py:300 admin/forms/domain.py:370 +msgid "General" +msgstr "Geral" + +#: admin/forms/account.py:406 admin/forms/account.py:493 +msgid "Mail" +msgstr "Correio" + +#: admin/forms/account.py:409 +msgid "Permissions" +msgstr "Permissões" + +#: admin/forms/account.py:476 +msgid "Account updated" +msgstr "Conta actualizada" + +#: admin/forms/account.py:500 +msgid "New account" +msgstr "Nova conta" + +#: admin/forms/account.py:512 +msgid "Account created" +msgstr "Conta criada" + +#: admin/forms/alias.py:23 +msgid "Email address" +msgstr "Endereço de e-mail" + +#: admin/forms/alias.py:25 +#, fuzzy +#| msgid "" +#| "The alias address (without the domain part). For a 'catch-all' address, " +#| "just enter an * character." +msgid "" +"The alias address. To create a catchall alias, just enter the domain name " +"(@domain.tld)." +msgstr "" +"O endereço do alias (sem a parte do domínio). Para um endereço 'cath-all', " +"digite apenas o caractere *" + +#: admin/forms/alias.py:31 +msgid "Recipients" +msgstr "Destinatários" + +#: admin/forms/alias.py:33 +#, fuzzy +#| msgid "" +#| "Mailbox(es) this alias will point to. Indicate only one address per " +#| "input, press ENTER to add a new input." +msgid "" +"Addresses this alias will point to. Indicate only one address per input, " +"press ENTER to add a new input." +msgstr "" +"Caixa(s) de correio para o qual este 'alias' aponta. Indicar apenas um " +"endereço por entrada, pressionar ENTER para acrescentar uma nova entrada." + +#: admin/forms/alias.py:75 +msgid "You don't have access to this domain" +msgstr "Não tem acesso a este domínio" + +#: admin/forms/alias.py:85 +msgid "No recipient defined" +msgstr "Sem destinatário definido" + +#: admin/forms/domain.py:22 admin/models/domain.py:76 +#: admin/templates/admin/domain_statistics.html:49 +msgid "Domain" +msgstr "Domínio" + +#: admin/forms/domain.py:32 admin/templates/admin/domain_statistics.html:26 +msgid "Type" +msgstr "Tipo" + +#: admin/forms/domain.py:38 +msgid "" +"Default quota in MB applied to mailboxes. Leave empty to use the default " +"value." +msgstr "" +"Quota (em MB) aplicada às caixas de correio. Deixe em branco para manter o " +"valor pré-definido." + +#: admin/forms/domain.py:46 +msgid "" +"Alias(es) of this domain. Indicate only one name per input, press ENTER to " +"add a new input." +msgstr "" +"Alias(es) deste domínio. Indicar apenas um nome por entrada, pressionar " +"ENTER para acrescentar uma nova entrada." + +#: admin/forms/domain.py:97 admin/forms/domain.py:108 +#: admin/forms/domain.py:116 +#, python-format +msgid "A %s with this name already exists" +msgstr "Já existe um(a) %s com este nome. " + +#: admin/forms/domain.py:108 admin/forms/domain.py:113 +msgid "domain" +msgstr "domínio" + +#: admin/forms/domain.py:200 +msgid "Create a domain administrator" +msgstr "Criar um administrador de domínio" + +#: admin/forms/domain.py:203 +msgid "Automatically create an administrator for this domain" +msgstr "Criar automaticamente um administrador para este domínio" + +#: admin/forms/domain.py:208 admin/templates/admin/domains.html:25 +#: admin/templates/admin/editdomainform.html:13 +#: admin/templates/admin/identity_headers.html:2 +msgid "Name" +msgstr "Nome" + +#: admin/forms/domain.py:211 +msgid "" +"The administrator's name. Don't include the domain's name here, it will be " +"automatically appended." +msgstr "" +"O nome do administrador. Não inclua o nome do domínio já que este será " +"automaticamente anexado." + +#: admin/forms/domain.py:218 +msgid "Create aliases" +msgstr " Criar aliases" + +#: admin/forms/domain.py:221 +msgid "Automatically create standard aliases for this domain" +msgstr "Criar automaticamente aliases para este domínio" + +#: admin/forms/domain.py:240 +msgid "Invalid format" +msgstr "Formato inválido" + +#: admin/forms/domain.py:257 +#, python-format +msgid "User '%s' already exists" +msgstr "O utilizador '%s' já existe" + +#: admin/forms/domain.py:359 +msgid "Domain modified" +msgstr "Domínio alterado" + +#: admin/forms/domain.py:379 +msgid "Options" +msgstr "Opções" + +#: admin/forms/domain.py:387 +msgid "New domain" +msgstr "Novo domínio" + +#: admin/forms/domain.py:405 +msgid "Domain created" +msgstr "Domínio criado" + +#: admin/forms/export.py:7 +msgid "File name" +msgstr "Nome do ficheiro" + +#: admin/forms/export.py:13 admin/forms/import_.py:13 +msgid "Separator" +msgstr "Separador" + +#: admin/forms/forward.py:14 +msgid "Recipient(s)" +msgstr "Destinatário(s)" + +#: admin/forms/forward.py:18 +msgid "Indicate one or more recipients separated by a ','" +msgstr "Indicar um ou mais destinatários separados por ','" + +#: admin/forms/forward.py:21 +msgid "Keep local copies" +msgstr "Conservar cópias locais" + +#: admin/forms/forward.py:24 +msgid "Forward messages and store copies into your local mailbox" +msgstr "" +"Reencaminhar mensagens e armazenar cópias na sua caixa de correio local" + +#: admin/forms/forward.py:42 +msgid "" +"You can't define a forward to a local destination. Please ask your " +"administrator to create an alias instead." +msgstr "" +"Não é possível definir um reencaminhamento para um destino local. Por favor " +"informe-se junto de um administrador para criar um alias." + +#: admin/forms/import_.py:11 +msgid "Select a file" +msgstr "Escolha um ficheiro" + +#: admin/forms/import_.py:19 +msgid "Continue on error" +msgstr "Continua com erro" + +#: admin/forms/import_.py:20 +msgid "Don't treat duplicated objects as error" +msgstr "Não tratar objetos duplicados como erro" + +#: admin/forms/import_.py:38 +msgid "Crypt passwords" +msgstr "Passwords encriptadas" + +#: admin/forms/import_.py:40 +msgid "Check this option if passwords contained in your file are not crypted" +msgstr "Marque esta opção se as senhas contidas no arquivo não são encriptadas" + +#: admin/lib.py:30 +msgid "A mailbox is required" +msgstr "É necessária uma caixa de correio" + +#: admin/lib.py:120 +#, fuzzy +#| msgid "You can't delete your own domain" +msgid "You are not allowed to import domains" +msgstr "Não é possível eliminar o seu próprio domínio" + +#: admin/lib.py:129 +msgid "You are not allowed to import domain aliases." +msgstr "" + +#: admin/models/alias.py:26 admin/models/mailbox.py:77 +msgid "address" +msgstr "endereços" + +#: admin/models/alias.py:28 +#, fuzzy +#| msgid "The alias name" +msgid "The alias address." +msgstr "O nome do 'alias'" + +#: admin/models/alias.py:33 admin/models/domain.py:37 +#: admin/models/domain_alias.py:24 +msgid "enabled" +msgstr "ativo" + +#: admin/models/alias.py:34 admin/models/domain_alias.py:25 +msgid "Check to activate this alias" +msgstr "Marcar para ativar este alias" + +#: admin/models/alias.py:79 +msgid "distribution list" +msgstr "lista de distribuição" + +#: admin/models/alias.py:80 +msgid "forward" +msgstr "reencaminhar" + +#: admin/models/alias.py:81 +msgid "alias" +msgstr "alias" + +#: admin/models/alias.py:112 +#, fuzzy +#| msgid "Invalid request" +msgid "Invalid address" +msgstr "Pedido inválido" + +#: admin/models/alias.py:133 +#, fuzzy +#| msgid "Local recipient %s not found" +msgid "Local recipient {}@{} not found" +msgstr "O destinatário local %s não foi encontrado" + +#: admin/models/alias.py:162 +#, python-format +msgid "Invalid line: %s" +msgstr "Linha inválida: %s" + +#: admin/models/alias.py:168 +#, python-format +msgid "Domain '%s' does not exist" +msgstr "O domínio '%s' não existe" + +#: admin/models/domain.py:33 admin/models/domain_alias.py:17 +msgid "name" +msgstr "nome" + +#: admin/models/domain.py:34 +msgid "The domain name" +msgstr "O nome do domínio" + +#: admin/models/domain.py:38 +msgid "Check to activate this domain" +msgstr "Marcar para ativar este domínio" + +#: admin/models/domain.py:160 admin/models/domain_alias.py:47 +msgid "Invalid line" +msgstr "Linha inválida" + +#: admin/models/domain.py:168 +#, python-format +msgid "Invalid quota value for domain '%s'" +msgstr "Valor de quota inválido para o dominio '%s'" + +#: admin/models/domain_alias.py:18 +msgid "The alias name" +msgstr "O nome do 'alias'" + +#: admin/models/domain_alias.py:20 +msgid "target" +msgstr "alvo" + +#: admin/models/domain_alias.py:21 +msgid "The domain this alias points to" +msgstr "O domínio para o qual este alias irá apontar" + +#: admin/models/domain_alias.py:56 +#, python-format +msgid "Unknown domain %s" +msgstr "Domínio desconhecido: %s" + +#: admin/models/mailbox.py:79 +msgid "Mailbox address (without the @domain.tld part)" +msgstr "Endereços das caixas de correio (sem a parte @domain.tld)" + +#: admin/models/mailbox.py:144 +#, python-format +msgid "Failed to retrieve mailbox location (%s)" +msgstr "Falha ao recuperar a localização da caixa de correio (%s)" + +#: admin/models/mailbox.py:233 +#, python-format +msgid "Quota is greater than the allowed domain's limit (%dM)" +msgstr "A quota excede o limite permitido pelo domínio (%dM)" + +#: admin/models/mailbox.py:239 +msgid "A quota is required" +msgstr "É necessário uma quota" + +#: admin/templates/admin/account_general_form.html:11 +#: admin/templates/admin/aliasform.html:8 +msgid "Domain is disabled" +msgstr "O dominio está inativo" + +#: admin/templates/admin/domain_general_form.html:4 +#: admin/templates/admin/mailform.html:3 +#: admin/templates/admin/newdomainform.html:3 +msgid "MB" +msgstr "MB" + +#: admin/templates/admin/domain_options_form.html:7 +msgid "You can't create a new domain administrator" +msgstr "Não pode criar administradores de domínio" + +#: admin/templates/admin/domain_statistics.html:15 +#: admin/templatetags/admin_tags.py:45 +msgid "Statistics" +msgstr "Estatísticas" + +#: admin/templates/admin/domain_statistics.html:15 +msgid "Domains and identities" +msgstr "Domínios e identidades" + +#: admin/templates/admin/domain_statistics.html:22 +msgid "Global counters" +msgstr "Contadores globais" + +#: admin/templates/admin/domain_statistics.html:27 +msgid "Quantity" +msgstr "Quantidade" + +#: admin/templates/admin/domain_statistics.html:33 +#: admin/templates/admin/editdomainform.html:24 +msgid "Domain aliases" +msgstr "Aliases de domínio" + +#: admin/templates/admin/domain_statistics.html:45 +msgid "Per-domain counters" +msgstr "Contadores por-domínio" + +#: admin/templates/admin/domains.html:27 +#: admin/templates/admin/identity_headers.html:4 +msgid "Tags" +msgstr "Tags" + +#: admin/templates/admin/domains.html:28 +#: admin/templates/admin/identity_headers.html:5 +msgid "Actions" +msgstr "Ações" + +#: admin/templates/admin/domains_table.html:4 +msgid "disabled" +msgstr "inativo" + +#: admin/templates/admin/editdomainform.html:4 +msgid "Administrators" +msgstr "Administradores" + +#: admin/templates/admin/editdomainform.html:7 +msgid "Miscellaneous" +msgstr "Vários" + +#: admin/templates/admin/editdomainform.html:19 +msgid "No domain administrator defined" +msgstr "Nenhum administrador de domínio definido" + +#: admin/templates/admin/editdomainform.html:22 +msgid "Creation date" +msgstr "Data de criação" + +#: admin/templates/admin/editdomainform.html:23 +msgid "Last modification date" +msgstr "Data da última modificação" + +#: admin/templates/admin/editdomainform.html:26 +msgid "Mailbox aliases" +msgstr "Aliases para caixas de correio" + +#: admin/templates/admin/forward.html:2 +msgid "Automatically forward emails to another address" +msgstr "Reenviar automaticamente e-mails para outro endereço" + +#: admin/templates/admin/forward.html:9 admin/views/alias.py:113 +msgid "Update" +msgstr "Actualizar" + +#: admin/templates/admin/identity_headers.html:3 +msgid "Fullname/Recipient" +msgstr "Nome Completo / Destinatário" + +#: admin/templates/admin/importform.html:6 +msgid "Help" +msgstr "Ajuda" + +#: admin/templates/admin/importform.html:13 +msgid "Importing..." +msgstr "A importar..." + +#: admin/templates/admin/mailboxes_raw.html:3 +msgid "Choose a value" +msgstr "Escolha um valor" + +#: admin/templates/admin/mailform.html:3 +msgid "Use domain default value" +msgstr "Usar um valor padrão de domínio" + +#: admin/templates/admin/quota_headers.html:2 +msgid "Address" +msgstr "Endereço" + +#: admin/templates/admin/quota_headers.html:3 +msgid "Value" +msgstr "Valor" + +#: admin/templates/admin/quota_headers.html:4 +msgid "Limit" +msgstr "Limite" + +#: admin/templates/admin/quota_headers.html:5 +#, python-format +msgid "Usage (%%)" +msgstr "Utilização (%%)" + +#: admin/templates/admin/searchform.html:6 +msgid "Search" +msgstr "Procurar" + +#: admin/templatetags/admin_tags.py:19 +msgid "enabled_m" +msgstr "enabled_m" + +#: admin/templatetags/admin_tags.py:19 +msgid "enabled_f" +msgstr "enabled_f" + +#: admin/templatetags/admin_tags.py:40 +msgid "List domains" +msgstr "Listar domínios" + +#: admin/templatetags/admin_tags.py:53 +msgid "Add domain" +msgstr "Adicionar domínio" + +#: admin/templatetags/admin_tags.py:62 admin/templatetags/admin_tags.py:126 +#: admin/views/import_.py:99 admin/views/import_.py:133 +msgid "Import" +msgstr "Importar" + +#: admin/templatetags/admin_tags.py:68 admin/templatetags/admin_tags.py:132 +#: admin/views/export.py:41 admin/views/export.py:69 +msgid "Export" +msgstr "Exportar" + +#: admin/templatetags/admin_tags.py:92 +msgid "List identities" +msgstr "Listar identidades" + +#: admin/templatetags/admin_tags.py:97 +msgid "List quotas" +msgstr "Listar as quotas" + +#: admin/templatetags/admin_tags.py:102 +msgid "Add account" +msgstr "Adicionar conta" + +#: admin/templatetags/admin_tags.py:108 +msgid "Add alias" +msgstr "Adicionar 'alias'" + +#: admin/templatetags/admin_tags.py:114 +msgid "Add forward" +msgstr "Adicionar reencaminhamento" + +#: admin/templatetags/admin_tags.py:120 +msgid "Add distribution list" +msgstr "Adicionar lista de distribuição" + +#: admin/templatetags/admin_tags.py:152 +msgid "View the domain's identities" +msgstr "Visualizar as identidades do domínio" + +#: admin/templatetags/admin_tags.py:159 admin/templatetags/admin_tags.py:182 +#: admin/templatetags/admin_tags.py:190 +#, python-format +msgid "Delete %s?" +msgstr "Eliminar %s?" + +#: admin/templatetags/admin_tags.py:267 +msgid "Remove this permission" +msgstr "Remover esta permissão" + +#: admin/views/alias.py:29 +msgid "Alias with this name already exists" +msgstr "Já existe um 'alias' com este nome" + +#: admin/views/alias.py:53 +msgid "Create" +msgstr "Criar" + +#: admin/views/alias.py:65 +msgid "New distribution list" +msgstr "Nova lista de distribuição" + +#: admin/views/alias.py:67 +msgid "Distribution list created" +msgstr "A lista de distribuição foi criada" + +#: admin/views/alias.py:76 +msgid "New alias" +msgstr "Novo 'alias'" + +#: admin/views/alias.py:77 +msgid "Alias created" +msgstr "Alias criado" + +#: admin/views/alias.py:86 +msgid "New forward" +msgstr "Novo reencaminhamento" + +#: admin/views/alias.py:87 +msgid "Forward created" +msgstr "Reencaminhamento criado" + +#: admin/views/alias.py:101 +msgid "Distribution list modified" +msgstr "Lista de distribuição modificada" + +#: admin/views/alias.py:103 +msgid "Forward modified" +msgstr "Reencaminhamento modificado" + +#: admin/views/alias.py:105 +msgid "Alias modified" +msgstr "'Alias' modificado" + +#: admin/views/domain.py:132 +msgid "You can't delete your own domain" +msgstr "Não é possível eliminar o seu próprio domínio" + +#: admin/views/domain.py:135 +msgid "Domain deleted" +msgid_plural "Domains deleted" +msgstr[0] "Domínio criado" +msgstr[1] "Domínios eliminados" + +#: admin/views/export.py:40 +msgid "Export identities" +msgstr "Exportar identidades" + +#: admin/views/export.py:68 +msgid "Export domains" +msgstr "Exportar domínios" + +#: admin/views/identity.py:108 admin/views/identity.py:200 +#: admin/views/identity.py:205 +msgid "Invalid request" +msgstr "Pedido inválido" + +#: admin/views/identity.py:190 +msgid "Account deleted" +msgid_plural "Accounts deleted" +msgstr[0] "Conta eliminada" +msgstr[1] "Contas eliminadas" + +#: admin/views/import_.py:64 +#, python-format +msgid "Object already exists: %s" +msgstr "Este objeto já existe: %s" + +#: admin/views/import_.py:68 +#, python-format +msgid "%d objects imported successfully" +msgstr "%d objetos importados com sucesso" + +#: admin/views/import_.py:86 +#, python-format +msgid "" +"Provide a CSV file where lines respect one of the following formats:\n" +"
      \n" +"
    • domain; name; quota; enabled
    • \n" +"
    • domainalias; name; targeted domain; enabled
    • \n" +" %s\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 "" + +#: admin/views/import_.py:98 +msgid "Import domains" +msgstr "Importar domínios" + +#: admin/views/import_.py:120 +msgid "" +"Provide a CSV file where lines respect one of the following formats:\n" +"
      \n" +"
    • account; loginname; password; first name; last name; enabled; group; " +"address; quota; [, domain, ...]
    • \n" +"
    • alias; address; enabled; internal recipient
    • \n" +"
    • forward; address; enabled; external recipient
    • \n" +"
    • dlist; address; enabled; recipient; recipient; ...
    • \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 "" + +#: admin/views/import_.py:132 +msgid "Import identities" +msgstr "Importar identidades" + +#: admin/views/user.py:34 +msgid "Forward updated" +msgstr "Reencaminhamento atualizado" diff --git a/modoboa/admin/locale/pt_PT/LC_MESSAGES/djangojs.po b/modoboa/admin/locale/pt_PT/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..c18072d3d --- /dev/null +++ b/modoboa/admin/locale/pt_PT/LC_MESSAGES/djangojs.po @@ -0,0 +1,61 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# lusitan , 2013 +# lusitan , 2013 +# Mike C. , 2012 +# Mike C. , 2012 +# Mike C. , 2014 +# Sandra Ribeiro , 2013 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 08:20+0100\n" +"PO-Revision-Date: 2014-12-29 12:08+0000\n" +"Last-Translator: Mike C. \n" +"Language-Team: Portuguese (Portugal) (http://www.transifex.com/projects/p/" +"modoboa/language/pt_PT/)\n" +"Language: pt_PT\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: static/admin/js/admin.js:126 +msgid "No more domain to show" +msgstr "Não existem mais dominios para mostrar" + +#: static/admin/js/admin.js:145 +msgid "Do not delete domain directory" +msgstr "Não excluir o diretório do domínio" + +#: static/admin/js/admin.js:147 +msgid "This operation will remove ALL data associated to this domain." +msgstr "Esta operação irá remover TODOS os dados associados a este domínio." + +#: static/admin/js/admin.js:148 +msgid "" +"This operation will remove all data associated to this domain, excepting " +"accounts." +msgstr "" +"Esta operação irá remover TODOS os dados associados a este domínio, excepto " +"as contas." + +#: static/admin/js/admin.js:231 +msgid "No domain administrator defined" +msgstr "Não está definido nenhum administrador para o domínio" + +#: static/admin/js/admin.js:286 static/admin/js/admin.js.c:354 +msgid "No more identity to show" +msgstr "Não existem mais identidades para mostrar" + +#: static/admin/js/admin.js:328 +msgid "Do not delete mailbox directory" +msgstr "Não excluir o diretório da caixa de correio" + +#: static/admin/js/admin.js:357 +msgid "No more quota to show" +msgstr "Não existem mais quotas para mostrar" diff --git a/modoboa/admin/locale/ru/LC_MESSAGES/django.po b/modoboa/admin/locale/ru/LC_MESSAGES/django.po new file mode 100644 index 000000000..58dbd9645 --- /dev/null +++ b/modoboa/admin/locale/ru/LC_MESSAGES/django.po @@ -0,0 +1,871 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: 2013-12-03 09:35+0000\n" +"Last-Translator: Antoine Nguyen \n" +"Language-Team: Russian (http://www.transifex.com/projects/p/modoboa/language/" +"ru/)\n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#: admin/app_settings.py:54 admin/templates/admin/editdomainform.html:25 +msgid "Mailboxes" +msgstr "Почтовые ящики" + +#: admin/app_settings.py:57 +msgid "Handle mailboxes on filesystem" +msgstr "Обрабатывать почтовые ящики на диске" + +#: admin/app_settings.py:60 +#, fuzzy +msgid "" +"Rename or remove mailboxes on the filesystem when they get renamed or " +"removed within Modoboa" +msgstr "" +"Удалять или переименовывать почтовые ящики на диске, если они удалены или " +"переименованы в Modoboa" + +#: admin/app_settings.py:66 +msgid "Mailboxes owner" +msgstr "Почтовые ящики закончились" + +#: admin/app_settings.py:69 +msgid "The UNIX account who owns mailboxes on the filesystem" +msgstr "UNIX аккаунт владельца почтового ящика на диске" + +#: admin/app_settings.py:74 +msgid "Default domain quota" +msgstr "" + +#: admin/app_settings.py:77 +msgid "" +"Default quota (in MB) applied to freshly created domains with no value " +"specified. A value of 0 means no quota." +msgstr "" + +#: admin/app_settings.py:83 +msgid "Automatic account removal" +msgstr "Автоматическое удаление аккаунта" + +#: admin/app_settings.py:86 +msgid "When a mailbox is removed, also remove the associated account" +msgstr "При удалении почтового ящика, так же удалять и аккаунт владельца" + +#: admin/app_settings.py:90 +#, fuzzy +msgid "Automatic domain/mailbox creation" +msgstr "Домен создан" + +#: admin/app_settings.py:93 +msgid "Create a domain and a mailbox when an account is automatically created." +msgstr "" + +#: admin/app_settings.py:139 admin/forms/account.py:239 +#: admin/forms/domain.py:77 +msgid "Must be a positive integer" +msgstr "" + +#: admin/app_settings.py:149 +msgid "Administration" +msgstr "" + +#: admin/callbacks.py:46 +#, fuzzy +#| msgid "Create a domain administrator" +msgid "Domain administrator" +msgstr "Создание администратора домена" + +#: admin/callbacks.py:75 admin/templates/admin/domain_statistics.html:30 +#: admin/templates/admin/domains.html:3 +msgid "Domains" +msgstr "Домены" + +#: admin/callbacks.py:85 admin/templates/admin/domain_statistics.html:36 +#: admin/templates/admin/domain_statistics.html:50 +#: admin/templates/admin/identities.html:5 +msgid "Identities" +msgstr "Пользователи" + +#: admin/callbacks.py:100 admin/templates/admin/forward.html:2 +msgid "Forward" +msgstr "Перенаправленые" + +#: admin/callbacks.py:163 +#, python-format +msgid "Account import failed (%s): domain does not exist" +msgstr "Ошибка импорта аккаунта (%s): домен должен существовать " + +#: admin/callbacks.py:173 admin/forms/account.py:257 +#, python-format +msgid "Mailbox %s already exists" +msgstr "Почтовый ящик %s существует" + +#: admin/callbacks.py:181 +#, fuzzy, python-format +msgid "Account import failed (%s): wrong quota value" +msgstr "Ошибка импорта аккаунта (%s): домен должен существовать " + +#: admin/callbacks.py:216 admin/forms/domain.py:94 +msgid "domain alias" +msgstr "" + +#: admin/forms/account.py:30 admin/templates/admin/editdomainform.html:13 +msgid "Username" +msgstr "Логин пользователя" + +#: admin/forms/account.py:32 +msgid "" +"The user's name. Must be a valid e-mail address for simple users or " +"administrators with a mailbox." +msgstr "" + +#: admin/forms/account.py:37 +msgid "Role" +msgstr "Роль" + +#: admin/forms/account.py:38 admin/forms/account.py:75 +msgid "Choose" +msgstr "Выбрать" + +#: admin/forms/account.py:39 +msgid "What level of permission this user will have" +msgstr "Разрешения для этого пользователя" + +#: admin/forms/account.py:42 +msgid "Password" +msgstr "Пароль" + +#: admin/forms/account.py:45 +msgid "Confirmation" +msgstr "Подтверждение" + +#: admin/forms/account.py:48 +msgid "Enter the same password as above, for verification." +msgstr "Введите такой же пароль как выше. Для проверки." + +#: admin/forms/account.py:66 +msgid "Enabled" +msgstr "Разрешено" + +#: admin/forms/account.py:141 +msgid "Only super administrators are allowed for this mode" +msgstr "" + +#: admin/forms/account.py:150 +msgid "The two password fields didn't match." +msgstr "Пароли не совпадают" + +#: admin/forms/account.py:156 +msgid "You can't disable your own account" +msgstr "Вы не можете запретить собственный аккаунт" + +#: admin/forms/account.py:170 +msgid "E-mail" +msgstr "Е-майл" + +#: admin/forms/account.py:172 admin/forms/domain.py:35 +msgid "Quota" +msgstr "Квота" + +#: admin/forms/account.py:174 +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 "" +"Квота в мегабайтах для этого почтового ящика. Определите собственное " +"значение или используйте значение по умолчанию для родительского домена. " +"Оставьте пустым для неограниченного значения. (Не разрешено администраторам " +"домена)" + +#: admin/forms/account.py:182 admin/forms/domain.py:43 +#: admin/templates/admin/domains.html:26 +msgid "Alias(es)" +msgstr "Синоним(ы)" + +#: admin/forms/account.py:185 +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 "" +"Синонимы этого почтового ящика. Вводите один адрес за раз. Нажмите ENTER для " +"добавления. Используйте '*' для создания синонима 'для всех' (*@domain.tld)" + +#: admin/forms/account.py:248 admin/forms/account.py:279 +#: admin/forms/alias.py:72 +msgid "Domain does not exist" +msgstr "Домен не существует" + +#: admin/forms/account.py:355 +msgid "Domain(s)" +msgstr "Домен(ы)" + +#: admin/forms/account.py:357 +msgid "Domain(s) that user administrates" +msgstr "Домен(ы) администрируемые пользователем" + +#: admin/forms/account.py:401 admin/forms/account.py:487 +#: admin/forms/domain.py:300 admin/forms/domain.py:370 +msgid "General" +msgstr "Основные" + +#: admin/forms/account.py:406 admin/forms/account.py:493 +msgid "Mail" +msgstr "Почта" + +#: admin/forms/account.py:409 +msgid "Permissions" +msgstr "Права" + +#: admin/forms/account.py:476 +msgid "Account updated" +msgstr "аккаунт изменен" + +#: admin/forms/account.py:500 +msgid "New account" +msgstr "Новый аккаунт" + +#: admin/forms/account.py:512 +msgid "Account created" +msgstr "Аккаунт создан" + +#: admin/forms/alias.py:23 +msgid "Email address" +msgstr "Е-майл адрес" + +#: admin/forms/alias.py:25 +#, fuzzy +#| msgid "" +#| "The alias address (without the domain part). For a 'catch-all' address, " +#| "just enter an * character." +msgid "" +"The alias address. To create a catchall alias, just enter the domain name " +"(@domain.tld)." +msgstr "" +"Адреса синонимов (без доменной части). Для указания адреса 'для всех' " +"введите символ *" + +#: admin/forms/alias.py:31 +msgid "Recipients" +msgstr "Получатели" + +#: admin/forms/alias.py:33 +#, fuzzy +#| msgid "" +#| "Mailbox(es) this alias will point to. Indicate only one address per " +#| "input, press ENTER to add a new input." +msgid "" +"Addresses this alias will point to. Indicate only one address per input, " +"press ENTER to add a new input." +msgstr "" +"Этот синоним почтового ящика. Вводите один адрес за раз. Нажмите ENTER для " +"добавления." + +#: admin/forms/alias.py:75 +msgid "You don't have access to this domain" +msgstr "Вы не имеете права доступа к этому домену" + +#: admin/forms/alias.py:85 +msgid "No recipient defined" +msgstr "Получатель не определен" + +#: admin/forms/domain.py:22 admin/models/domain.py:76 +#: admin/templates/admin/domain_statistics.html:49 +msgid "Domain" +msgstr "" + +#: admin/forms/domain.py:32 admin/templates/admin/domain_statistics.html:26 +msgid "Type" +msgstr "" + +#: admin/forms/domain.py:38 +msgid "" +"Default quota in MB applied to mailboxes. Leave empty to use the default " +"value." +msgstr "" + +#: admin/forms/domain.py:46 +msgid "" +"Alias(es) of this domain. Indicate only one name per input, press ENTER to " +"add a new input." +msgstr "" +"Синоним(ы) этого домена. Вводите одно имя за раз. Нажмите ENTER для " +"добавления." + +#: admin/forms/domain.py:97 admin/forms/domain.py:108 +#: admin/forms/domain.py:116 +#, python-format +msgid "A %s with this name already exists" +msgstr "" + +#: admin/forms/domain.py:108 admin/forms/domain.py:113 +msgid "domain" +msgstr "" + +#: admin/forms/domain.py:200 +msgid "Create a domain administrator" +msgstr "Создание администратора домена" + +#: admin/forms/domain.py:203 +msgid "Automatically create an administrator for this domain" +msgstr "Автоматически создаст администратора для этого домена" + +#: admin/forms/domain.py:208 admin/templates/admin/domains.html:25 +#: admin/templates/admin/editdomainform.html:13 +#: admin/templates/admin/identity_headers.html:2 +msgid "Name" +msgstr "Логин" + +#: admin/forms/domain.py:211 +msgid "" +"The administrator's name. Don't include the domain's name here, it will be " +"automatically appended." +msgstr "" +"Имя администратора. Не включайте имя домена оно будет добавлено " +"автоматически." + +#: admin/forms/domain.py:218 +msgid "Create aliases" +msgstr "Создание синонимов" + +#: admin/forms/domain.py:221 +msgid "Automatically create standard aliases for this domain" +msgstr "Автоматически создает стандартные синонимы для этого домена" + +#: admin/forms/domain.py:240 +msgid "Invalid format" +msgstr "Неверный формат" + +#: admin/forms/domain.py:257 +#, python-format +msgid "User '%s' already exists" +msgstr "Пользователь '%s' уже существует" + +#: admin/forms/domain.py:359 +msgid "Domain modified" +msgstr "Домен изменен" + +#: admin/forms/domain.py:379 +msgid "Options" +msgstr "Опции" + +#: admin/forms/domain.py:387 +msgid "New domain" +msgstr "Новый домен" + +#: admin/forms/domain.py:405 +msgid "Domain created" +msgstr "Домен создан" + +#: admin/forms/export.py:7 +msgid "File name" +msgstr "Имя файла" + +#: admin/forms/export.py:13 admin/forms/import_.py:13 +msgid "Separator" +msgstr "Разделитель" + +#: admin/forms/forward.py:14 +msgid "Recipient(s)" +msgstr "Получатели" + +#: admin/forms/forward.py:18 +msgid "Indicate one or more recipients separated by a ','" +msgstr "Укажите одного или нескольких получателей, разделенных запятой ','" + +#: admin/forms/forward.py:21 +msgid "Keep local copies" +msgstr "Сохранить локальную копию" + +#: admin/forms/forward.py:24 +msgid "Forward messages and store copies into your local mailbox" +msgstr "" +"Перенаправление сообщения, и сохранение копии в вашем локальном почтовом " +"ящике" + +#: admin/forms/forward.py:42 +msgid "" +"You can't define a forward to a local destination. Please ask your " +"administrator to create an alias instead." +msgstr "" +"Вы не можете определить перенаправление на локальный адрес. Пожалуйста, " +"попросите администратора создать синоним" + +#: admin/forms/import_.py:11 +msgid "Select a file" +msgstr "Выберите файл" + +#: admin/forms/import_.py:19 +msgid "Continue on error" +msgstr "Продолжать при ошибке" + +#: admin/forms/import_.py:20 +msgid "Don't treat duplicated objects as error" +msgstr "Не исправлять дублирующийся объект при ошибке" + +#: admin/forms/import_.py:38 +msgid "Crypt passwords" +msgstr "Шифровать пароль" + +#: admin/forms/import_.py:40 +msgid "Check this option if passwords contained in your file are not crypted" +msgstr "Выберите эту опцию если пароль в вашем файле не зашифрован " + +#: admin/lib.py:30 +msgid "A mailbox is required" +msgstr "" + +#: admin/lib.py:120 +#, fuzzy +#| msgid "You can't delete your own domain" +msgid "You are not allowed to import domains" +msgstr "Вы не можете удалить собственный домен" + +#: admin/lib.py:129 +msgid "You are not allowed to import domain aliases." +msgstr "" + +#: admin/models/alias.py:26 admin/models/mailbox.py:77 +msgid "address" +msgstr "адрес" + +#: admin/models/alias.py:28 +#, fuzzy +#| msgid "The alias name" +msgid "The alias address." +msgstr "Имя синонима" + +#: admin/models/alias.py:33 admin/models/domain.py:37 +#: admin/models/domain_alias.py:24 +msgid "enabled" +msgstr "Разрешен" + +#: admin/models/alias.py:34 admin/models/domain_alias.py:25 +msgid "Check to activate this alias" +msgstr "Отметить для активации синонима" + +#: admin/models/alias.py:79 +msgid "distribution list" +msgstr "Список рассылки" + +#: admin/models/alias.py:80 +msgid "forward" +msgstr "Перенаправление" + +#: admin/models/alias.py:81 +msgid "alias" +msgstr "Синоним" + +#: admin/models/alias.py:112 +#, fuzzy +#| msgid "Invalid request" +msgid "Invalid address" +msgstr "Ошибочный запрос" + +#: admin/models/alias.py:133 +#, fuzzy +#| msgid "Local recipient %s not found" +msgid "Local recipient {}@{} not found" +msgstr "Локальный получатель %s не найден" + +#: admin/models/alias.py:162 +#, python-format +msgid "Invalid line: %s" +msgstr "Неверная строка %s" + +#: admin/models/alias.py:168 +#, python-format +msgid "Domain '%s' does not exist" +msgstr "Домен '%s' не существует" + +#: admin/models/domain.py:33 admin/models/domain_alias.py:17 +msgid "name" +msgstr "имя" + +#: admin/models/domain.py:34 +msgid "The domain name" +msgstr "Имя домена" + +#: admin/models/domain.py:38 +msgid "Check to activate this domain" +msgstr "Отметить для активации этого домена" + +#: admin/models/domain.py:160 admin/models/domain_alias.py:47 +msgid "Invalid line" +msgstr "Ошибочная строка" + +#: admin/models/domain.py:168 +#, python-format +msgid "Invalid quota value for domain '%s'" +msgstr "" + +#: admin/models/domain_alias.py:18 +msgid "The alias name" +msgstr "Имя синонима" + +#: admin/models/domain_alias.py:20 +msgid "target" +msgstr "адресат" + +#: admin/models/domain_alias.py:21 +msgid "The domain this alias points to" +msgstr "Домен этого синонима указывает на" + +#: admin/models/domain_alias.py:56 +#, python-format +msgid "Unknown domain %s" +msgstr "Неизвестный домен %s" + +#: admin/models/mailbox.py:79 +msgid "Mailbox address (without the @domain.tld part)" +msgstr "Адрес почтового ящика (без @domain.tld)" + +#: admin/models/mailbox.py:144 +#, python-format +msgid "Failed to retrieve mailbox location (%s)" +msgstr "Ошибка получения места находжения почтового ящика (%s)" + +#: admin/models/mailbox.py:233 +#, python-format +msgid "Quota is greater than the allowed domain's limit (%dM)" +msgstr "Квота больше чем разрешено на уровне домена (%dM)" + +#: admin/models/mailbox.py:239 +msgid "A quota is required" +msgstr "Требуется квота" + +#: admin/templates/admin/account_general_form.html:11 +#: admin/templates/admin/aliasform.html:8 +#, fuzzy +msgid "Domain is disabled" +msgstr "Домен удален" + +#: admin/templates/admin/domain_general_form.html:4 +#: admin/templates/admin/mailform.html:3 +#: admin/templates/admin/newdomainform.html:3 +msgid "MB" +msgstr "Мб" + +#: admin/templates/admin/domain_options_form.html:7 +msgid "You can't create a new domain administrator" +msgstr "" + +#: admin/templates/admin/domain_statistics.html:15 +#: admin/templatetags/admin_tags.py:45 +msgid "Statistics" +msgstr "Статистика" + +#: admin/templates/admin/domain_statistics.html:15 +#, fuzzy +msgid "Domains and identities" +msgstr "Список пользователей" + +#: admin/templates/admin/domain_statistics.html:22 +msgid "Global counters" +msgstr "" + +#: admin/templates/admin/domain_statistics.html:27 +#, fuzzy +msgid "Quantity" +msgstr "Карантин" + +#: admin/templates/admin/domain_statistics.html:33 +#: admin/templates/admin/editdomainform.html:24 +msgid "Domain aliases" +msgstr "Синонимы доменов" + +#: admin/templates/admin/domain_statistics.html:45 +msgid "Per-domain counters" +msgstr "" + +#: admin/templates/admin/domains.html:27 +#: admin/templates/admin/identity_headers.html:4 +msgid "Tags" +msgstr "Теги" + +#: admin/templates/admin/domains.html:28 +#: admin/templates/admin/identity_headers.html:5 +msgid "Actions" +msgstr "Действие" + +#: admin/templates/admin/domains_table.html:4 +#, fuzzy +msgid "disabled" +msgstr "Разрешен" + +#: admin/templates/admin/editdomainform.html:4 +msgid "Administrators" +msgstr "Администраторы" + +#: admin/templates/admin/editdomainform.html:7 +msgid "Miscellaneous" +msgstr "Прочее" + +#: admin/templates/admin/editdomainform.html:19 +msgid "No domain administrator defined" +msgstr "Администратор домена не определен" + +#: admin/templates/admin/editdomainform.html:22 +msgid "Creation date" +msgstr "Дата создания" + +#: admin/templates/admin/editdomainform.html:23 +msgid "Last modification date" +msgstr "Дата последней модификации" + +#: admin/templates/admin/editdomainform.html:26 +msgid "Mailbox aliases" +msgstr "Синонимы почтового ящика" + +#: admin/templates/admin/forward.html:2 +msgid "Automatically forward emails to another address" +msgstr "Автоматически перенаправлять письма на другой адрес" + +#: admin/templates/admin/forward.html:9 admin/views/alias.py:113 +msgid "Update" +msgstr "Обновление" + +#: admin/templates/admin/identity_headers.html:3 +msgid "Fullname/Recipient" +msgstr "Полное имя/получатель" + +#: admin/templates/admin/importform.html:6 +msgid "Help" +msgstr "Подсказка" + +#: admin/templates/admin/importform.html:13 +msgid "Importing..." +msgstr "Импорт..." + +#: admin/templates/admin/mailboxes_raw.html:3 +msgid "Choose a value" +msgstr "Выбрать значение" + +#: admin/templates/admin/mailform.html:3 +msgid "Use domain default value" +msgstr "Используются значения домена по умолчанию " + +#: admin/templates/admin/quota_headers.html:2 +msgid "Address" +msgstr "Адрес" + +#: admin/templates/admin/quota_headers.html:3 +msgid "Value" +msgstr "Значение" + +#: admin/templates/admin/quota_headers.html:4 +msgid "Limit" +msgstr "Лимит" + +#: admin/templates/admin/quota_headers.html:5 +#, python-format +msgid "Usage (%%)" +msgstr "использование (%%)" + +#: admin/templates/admin/searchform.html:6 +msgid "Search" +msgstr "Поиск" + +#: admin/templatetags/admin_tags.py:19 +msgid "enabled_m" +msgstr "enabled_m" + +#: admin/templatetags/admin_tags.py:19 +msgid "enabled_f" +msgstr "enabled_f" + +#: admin/templatetags/admin_tags.py:40 +#, fuzzy +msgid "List domains" +msgstr "Список квот" + +#: admin/templatetags/admin_tags.py:53 +msgid "Add domain" +msgstr "Добавить домен" + +#: admin/templatetags/admin_tags.py:62 admin/templatetags/admin_tags.py:126 +#: admin/views/import_.py:99 admin/views/import_.py:133 +msgid "Import" +msgstr "Импорт" + +#: admin/templatetags/admin_tags.py:68 admin/templatetags/admin_tags.py:132 +#: admin/views/export.py:41 admin/views/export.py:69 +msgid "Export" +msgstr "Экспорт" + +#: admin/templatetags/admin_tags.py:92 +msgid "List identities" +msgstr "Список пользователей" + +#: admin/templatetags/admin_tags.py:97 +msgid "List quotas" +msgstr "Список квот" + +#: admin/templatetags/admin_tags.py:102 +msgid "Add account" +msgstr "Добавить аккаунт" + +#: admin/templatetags/admin_tags.py:108 +msgid "Add alias" +msgstr "Добавить синоним" + +#: admin/templatetags/admin_tags.py:114 +msgid "Add forward" +msgstr "Добавить пересылку" + +#: admin/templatetags/admin_tags.py:120 +msgid "Add distribution list" +msgstr "Добавить список рассылки" + +#: admin/templatetags/admin_tags.py:152 +msgid "View the domain's identities" +msgstr "Информация о домене" + +#: admin/templatetags/admin_tags.py:159 admin/templatetags/admin_tags.py:182 +#: admin/templatetags/admin_tags.py:190 +#, python-format +msgid "Delete %s?" +msgstr "Удалить %s?" + +#: admin/templatetags/admin_tags.py:267 +msgid "Remove this permission" +msgstr "Удалить это разрешение" + +#: admin/views/alias.py:29 +msgid "Alias with this name already exists" +msgstr "Синоним с таким именем уже есть" + +#: admin/views/alias.py:53 +msgid "Create" +msgstr "Создать" + +#: admin/views/alias.py:65 +msgid "New distribution list" +msgstr "Новый список рассылки" + +#: admin/views/alias.py:67 +msgid "Distribution list created" +msgstr "Список рассылки создан" + +#: admin/views/alias.py:76 +msgid "New alias" +msgstr "Новый синоним" + +#: admin/views/alias.py:77 +msgid "Alias created" +msgstr "Синоним создан" + +#: admin/views/alias.py:86 +msgid "New forward" +msgstr "Новая пересылка" + +#: admin/views/alias.py:87 +msgid "Forward created" +msgstr "Пересылка создана" + +#: admin/views/alias.py:101 +msgid "Distribution list modified" +msgstr "Список рассылки изменен" + +#: admin/views/alias.py:103 +msgid "Forward modified" +msgstr "Пересылка изменена" + +#: admin/views/alias.py:105 +msgid "Alias modified" +msgstr "Синоним изменен" + +#: admin/views/domain.py:132 +msgid "You can't delete your own domain" +msgstr "Вы не можете удалить собственный домен" + +#: admin/views/domain.py:135 +msgid "Domain deleted" +msgid_plural "Domains deleted" +msgstr[0] "Домен удален" +msgstr[1] "Домена удалено" +msgstr[2] "Доменов удалено" + +#: admin/views/export.py:40 +msgid "Export identities" +msgstr "Экспорт пользователя" + +#: admin/views/export.py:68 +msgid "Export domains" +msgstr "Экспорт доменов" + +#: admin/views/identity.py:108 admin/views/identity.py:200 +#: admin/views/identity.py:205 +msgid "Invalid request" +msgstr "Ошибочный запрос" + +#: admin/views/identity.py:190 +msgid "Account deleted" +msgid_plural "Accounts deleted" +msgstr[0] "Аккаунт удален" +msgstr[1] "Аккаунта удалено" +msgstr[2] "Аккаунтов удалено" + +#: admin/views/import_.py:64 +#, python-format +msgid "Object already exists: %s" +msgstr "Объект уже существует: %s" + +#: admin/views/import_.py:68 +#, python-format +msgid "%d objects imported successfully" +msgstr "%d объектов импортировано" + +#: admin/views/import_.py:86 +#, python-format +msgid "" +"Provide a CSV file where lines respect one of the following formats:\n" +"
      \n" +"
    • domain; name; quota; enabled
    • \n" +"
    • domainalias; name; targeted domain; enabled
    • \n" +" %s\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 "" + +#: admin/views/import_.py:98 +msgid "Import domains" +msgstr "Импорт доменов" + +#: admin/views/import_.py:120 +msgid "" +"Provide a CSV file where lines respect one of the following formats:\n" +"
      \n" +"
    • account; loginname; password; first name; last name; enabled; group; " +"address; quota; [, domain, ...]
    • \n" +"
    • alias; address; enabled; internal recipient
    • \n" +"
    • forward; address; enabled; external recipient
    • \n" +"
    • dlist; address; enabled; recipient; recipient; ...
    • \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 "" + +#: admin/views/import_.py:132 +msgid "Import identities" +msgstr "Импорт информации о пользователях" + +#: admin/views/user.py:34 +msgid "Forward updated" +msgstr "Перенаправление обновлено" diff --git a/modoboa/admin/locale/ru/LC_MESSAGES/djangojs.po b/modoboa/admin/locale/ru/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..c2f2dbb77 --- /dev/null +++ b/modoboa/admin/locale/ru/LC_MESSAGES/djangojs.po @@ -0,0 +1,56 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 08:20+0100\n" +"PO-Revision-Date: 2013-12-03 09:35+0000\n" +"Last-Translator: Antoine Nguyen \n" +"Language-Team: Russian (http://www.transifex.com/projects/p/modoboa/language/" +"ru/)\n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#: static/admin/js/admin.js:126 +msgid "No more domain to show" +msgstr "" + +#: static/admin/js/admin.js:145 +msgid "Do not delete domain directory" +msgstr "Не удаляйте каталог домена" + +#: static/admin/js/admin.js:147 +msgid "This operation will remove ALL data associated to this domain." +msgstr "Эта операция удалит ВСЕ данные, связанные с этим доменом." + +#: static/admin/js/admin.js:148 +msgid "" +"This operation will remove all data associated to this domain, excepting " +"accounts." +msgstr "" +"Эта операция удалит ВСЕ данные, связанные с этим доменом, за исключением " +"аккаунтов." + +#: static/admin/js/admin.js:231 +msgid "No domain administrator defined" +msgstr "Не указан администратор домена" + +#: static/admin/js/admin.js:286 static/admin/js/admin.js.c:354 +msgid "No more identity to show" +msgstr "" + +#: static/admin/js/admin.js:328 +msgid "Do not delete mailbox directory" +msgstr "Не удаляйте каталог почтовых ящиков" + +#: static/admin/js/admin.js:357 +msgid "No more quota to show" +msgstr "" diff --git a/modoboa/admin/locale/sv/LC_MESSAGES/django.po b/modoboa/admin/locale/sv/LC_MESSAGES/django.po new file mode 100644 index 000000000..3526aa7de --- /dev/null +++ b/modoboa/admin/locale/sv/LC_MESSAGES/django.po @@ -0,0 +1,889 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Martin Persson , 2014 +# Olle Gustafsson , 2013 +# Olle Gustafsson , 2013,2015 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: 2015-01-13 15:18+0000\n" +"Last-Translator: Olle Gustafsson \n" +"Language-Team: Swedish (http://www.transifex.com/projects/p/modoboa/language/" +"sv/)\n" +"Language: sv\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: admin/app_settings.py:54 admin/templates/admin/editdomainform.html:25 +msgid "Mailboxes" +msgstr "Brevlådor" + +#: admin/app_settings.py:57 +msgid "Handle mailboxes on filesystem" +msgstr "Hantera brevlådor på filsystem" + +#: admin/app_settings.py:60 +msgid "" +"Rename or remove mailboxes on the filesystem when they get renamed or " +"removed within Modoboa" +msgstr "" +"Byt namn på eller ta bort brevlådor på filsystemet när de kommer byta namn " +"eller tas bort inom Modoboa" + +#: admin/app_settings.py:66 +msgid "Mailboxes owner" +msgstr "Brevlådans ägare" + +#: admin/app_settings.py:69 +msgid "The UNIX account who owns mailboxes on the filesystem" +msgstr "Den UNIX-konto som äger brevlådor på filsystemet" + +#: admin/app_settings.py:74 +msgid "Default domain quota" +msgstr "Standard domän kvota" + +#: admin/app_settings.py:77 +msgid "" +"Default quota (in MB) applied to freshly created domains with no value " +"specified. A value of 0 means no quota." +msgstr "" +"Standard kvota (i MB) applicerad på nyskapade domäner som inte har något " +"värde specificerat. Ett värde på 0 betyder ingen kvota." + +#: admin/app_settings.py:83 +msgid "Automatic account removal" +msgstr "Automatisk konto borttagning" + +#: admin/app_settings.py:86 +msgid "When a mailbox is removed, also remove the associated account" +msgstr "När en brevlåda raderas, radera också tillhörande konto" + +#: admin/app_settings.py:90 +msgid "Automatic domain/mailbox creation" +msgstr "Automatisk skapande av domän/brevlåda" + +#: admin/app_settings.py:93 +msgid "Create a domain and a mailbox when an account is automatically created." +msgstr "Skapa en domän och en brevlåda när ett konto skapas automatiskt." + +#: admin/app_settings.py:139 admin/forms/account.py:239 +#: admin/forms/domain.py:77 +msgid "Must be a positive integer" +msgstr "Måste vara ett positivt heltal" + +#: admin/app_settings.py:149 +msgid "Administration" +msgstr "Administration" + +#: admin/callbacks.py:46 +#, fuzzy +#| msgid "Create a domain administrator" +msgid "Domain administrator" +msgstr "Skapa en domän administratör" + +#: admin/callbacks.py:75 admin/templates/admin/domain_statistics.html:30 +#: admin/templates/admin/domains.html:3 +msgid "Domains" +msgstr "Domäner" + +#: admin/callbacks.py:85 admin/templates/admin/domain_statistics.html:36 +#: admin/templates/admin/domain_statistics.html:50 +#: admin/templates/admin/identities.html:5 +msgid "Identities" +msgstr "Konton" + +#: admin/callbacks.py:100 admin/templates/admin/forward.html:2 +msgid "Forward" +msgstr "Vidarebefordra" + +#: admin/callbacks.py:163 +#, python-format +msgid "Account import failed (%s): domain does not exist" +msgstr "Konto import misslyckades (%s): domänen existerar inte" + +#: admin/callbacks.py:173 admin/forms/account.py:257 +#, python-format +msgid "Mailbox %s already exists" +msgstr "Brevlåda %s finns redan" + +#: admin/callbacks.py:181 +#, python-format +msgid "Account import failed (%s): wrong quota value" +msgstr "Importering av konto misslyckades (%s): Fel kvota värde" + +#: admin/callbacks.py:216 admin/forms/domain.py:94 +msgid "domain alias" +msgstr "domän alias" + +#: admin/forms/account.py:30 admin/templates/admin/editdomainform.html:13 +msgid "Username" +msgstr "Användarnamn" + +#: admin/forms/account.py:32 +msgid "" +"The user's name. Must be a valid e-mail address for simple users or " +"administrators with a mailbox." +msgstr "" +"Användarens namn. Måste vara en giltig e-postadress för vanliga användare " +"eller för administratörer med brevlåda." + +#: admin/forms/account.py:37 +msgid "Role" +msgstr "Roll" + +#: admin/forms/account.py:38 admin/forms/account.py:75 +msgid "Choose" +msgstr "Välj" + +#: admin/forms/account.py:39 +msgid "What level of permission this user will have" +msgstr "Vilken nivå av behörighet den här användaren har" + +#: admin/forms/account.py:42 +msgid "Password" +msgstr "Lösenord" + +#: admin/forms/account.py:45 +msgid "Confirmation" +msgstr "Bekräftelse" + +#: admin/forms/account.py:48 +msgid "Enter the same password as above, for verification." +msgstr "Ange samma lösenord som ovan för verifiering." + +#: admin/forms/account.py:66 +msgid "Enabled" +msgstr "Aktiverad" + +#: admin/forms/account.py:141 +msgid "Only super administrators are allowed for this mode" +msgstr "" + +#: admin/forms/account.py:150 +msgid "The two password fields didn't match." +msgstr "Dessa två lösnordsfält matchar inte." + +#: admin/forms/account.py:156 +msgid "You can't disable your own account" +msgstr "Du kan inte inaktivera ditt eget konto" + +#: admin/forms/account.py:170 +msgid "E-mail" +msgstr "E-post" + +#: admin/forms/account.py:172 admin/forms/domain.py:35 +msgid "Quota" +msgstr "Kvot" + +#: admin/forms/account.py:174 +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 "" +"Kvot i MB för denna brevlåda. Definiera ett anpassat värde eller använda " +"domänens förvalda. Lämna tomt för att definiera ett obegränsat värde (ej " +"tillåtet för domänadministratörer)." + +#: admin/forms/account.py:182 admin/forms/domain.py:43 +#: admin/templates/admin/domains.html:26 +msgid "Alias(es)" +msgstr "Alias" + +#: admin/forms/account.py:185 +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 för denna brevlåda. Ange endast en adress per rad, tryck ENTER för att " +"lägga till ny rad. Använd '*' för att skapa ett 'catchall' alias (ex: " +"*@domain.tld)." + +#: admin/forms/account.py:248 admin/forms/account.py:279 +#: admin/forms/alias.py:72 +msgid "Domain does not exist" +msgstr "Domän existerar inte" + +#: admin/forms/account.py:355 +msgid "Domain(s)" +msgstr "Domän(er)" + +#: admin/forms/account.py:357 +msgid "Domain(s) that user administrates" +msgstr "Domän(er) användaren administrerar" + +#: admin/forms/account.py:401 admin/forms/account.py:487 +#: admin/forms/domain.py:300 admin/forms/domain.py:370 +msgid "General" +msgstr "Generellt" + +#: admin/forms/account.py:406 admin/forms/account.py:493 +msgid "Mail" +msgstr "Post" + +#: admin/forms/account.py:409 +msgid "Permissions" +msgstr "Rättigheter" + +#: admin/forms/account.py:476 +msgid "Account updated" +msgstr "Konto uppdaterat" + +#: admin/forms/account.py:500 +msgid "New account" +msgstr "Nytt konto" + +#: admin/forms/account.py:512 +msgid "Account created" +msgstr "Konto skapat" + +#: admin/forms/alias.py:23 +msgid "Email address" +msgstr "E-post adress" + +#: admin/forms/alias.py:25 +#, fuzzy +#| msgid "" +#| "The alias address (without the domain part). For a 'catch-all' address, " +#| "just enter an * character." +msgid "" +"The alias address. To create a catchall alias, just enter the domain name " +"(@domain.tld)." +msgstr "" +"Alias adress (utan domän delen). För en \"catch-all\"-adress, ange bara " +"asterisk *." + +#: admin/forms/alias.py:31 +msgid "Recipients" +msgstr "Mottagare" + +#: admin/forms/alias.py:33 +#, fuzzy +#| msgid "" +#| "Mailbox(es) this alias will point to. Indicate only one address per " +#| "input, press ENTER to add a new input." +msgid "" +"Addresses this alias will point to. Indicate only one address per input, " +"press ENTER to add a new input." +msgstr "" +"Brevlåda/or detta alias kommer att peka på. Ange endast en adress per " +"inlägg, tryck ENTER för att lägga till en ny inlägg." + +#: admin/forms/alias.py:75 +msgid "You don't have access to this domain" +msgstr "Du har inte åtkomst till denna domän" + +#: admin/forms/alias.py:85 +msgid "No recipient defined" +msgstr "Ingen mottagare definierad" + +#: admin/forms/domain.py:22 admin/models/domain.py:76 +#: admin/templates/admin/domain_statistics.html:49 +msgid "Domain" +msgstr "Domän" + +#: admin/forms/domain.py:32 admin/templates/admin/domain_statistics.html:26 +msgid "Type" +msgstr "Typ" + +#: admin/forms/domain.py:38 +msgid "" +"Default quota in MB applied to mailboxes. Leave empty to use the default " +"value." +msgstr "" +"Standard kvota i MB tillämpad på nya brevlådor. Lämna tomt för att använda " +"standardvärdet." + +#: admin/forms/domain.py:46 +msgid "" +"Alias(es) of this domain. Indicate only one name per input, press ENTER to " +"add a new input." +msgstr "" +"Alias för denna domän. Ange endast ett namn per rad, tryck ENTER för att " +"lägga till en rad." + +#: admin/forms/domain.py:97 admin/forms/domain.py:108 +#: admin/forms/domain.py:116 +#, python-format +msgid "A %s with this name already exists" +msgstr "En %s med detta namn finns redan" + +#: admin/forms/domain.py:108 admin/forms/domain.py:113 +msgid "domain" +msgstr "domän" + +#: admin/forms/domain.py:200 +msgid "Create a domain administrator" +msgstr "Skapa en domän administratör" + +#: admin/forms/domain.py:203 +msgid "Automatically create an administrator for this domain" +msgstr "Skapa en domän administratör automatiskt för denna domän" + +#: admin/forms/domain.py:208 admin/templates/admin/domains.html:25 +#: admin/templates/admin/editdomainform.html:13 +#: admin/templates/admin/identity_headers.html:2 +msgid "Name" +msgstr "Namn" + +#: admin/forms/domain.py:211 +msgid "" +"The administrator's name. Don't include the domain's name here, it will be " +"automatically appended." +msgstr "" +"Administratörens namn. Inkludera inte domänens namn här, den kommer att " +"läggas till automatiskt." + +#: admin/forms/domain.py:218 +msgid "Create aliases" +msgstr "Skapa alias." + +#: admin/forms/domain.py:221 +msgid "Automatically create standard aliases for this domain" +msgstr "Skapa automatiskt standard alias för den här domänen" + +#: admin/forms/domain.py:240 +msgid "Invalid format" +msgstr "Ogiltigt format" + +#: admin/forms/domain.py:257 +#, python-format +msgid "User '%s' already exists" +msgstr "Användare '%s' finns redan" + +#: admin/forms/domain.py:359 +msgid "Domain modified" +msgstr "Domän ändrad" + +#: admin/forms/domain.py:379 +msgid "Options" +msgstr "Inställningar" + +#: admin/forms/domain.py:387 +msgid "New domain" +msgstr "Ny domän" + +#: admin/forms/domain.py:405 +msgid "Domain created" +msgstr "Domän skapad" + +#: admin/forms/export.py:7 +msgid "File name" +msgstr "Filnamn" + +#: admin/forms/export.py:13 admin/forms/import_.py:13 +msgid "Separator" +msgstr "Separator" + +#: admin/forms/forward.py:14 +msgid "Recipient(s)" +msgstr "Mottagare" + +#: admin/forms/forward.py:18 +msgid "Indicate one or more recipients separated by a ','" +msgstr "Ange en eller flera mottagare separerade med ett ','" + +#: admin/forms/forward.py:21 +msgid "Keep local copies" +msgstr "Behåll lokal kopia" + +#: admin/forms/forward.py:24 +msgid "Forward messages and store copies into your local mailbox" +msgstr "Vidarebefordra meddelande och spara kopia i din lokala brevlåda" + +#: admin/forms/forward.py:42 +msgid "" +"You can't define a forward to a local destination. Please ask your " +"administrator to create an alias instead." +msgstr "" +"Du kan inte definiera en vidarebefordring till en lokal destination. Be din " +"administratör att skapa ett alias istället." + +#: admin/forms/import_.py:11 +msgid "Select a file" +msgstr "Välj en fil" + +#: admin/forms/import_.py:19 +msgid "Continue on error" +msgstr "Fortsätt vid fel" + +#: admin/forms/import_.py:20 +msgid "Don't treat duplicated objects as error" +msgstr "Behandla inte dubletter som fel" + +#: admin/forms/import_.py:38 +msgid "Crypt passwords" +msgstr "Krypterade lösenord" + +#: admin/forms/import_.py:40 +msgid "Check this option if passwords contained in your file are not crypted" +msgstr "Markera det här alternativet om lösenord i filen inte är krypterad" + +#: admin/lib.py:30 +msgid "A mailbox is required" +msgstr "En brevlåda krävs" + +#: admin/lib.py:120 +#, fuzzy +#| msgid "You can't delete your own domain" +msgid "You are not allowed to import domains" +msgstr "Du kan inte radera din egen domän" + +#: admin/lib.py:129 +msgid "You are not allowed to import domain aliases." +msgstr "" + +#: admin/models/alias.py:26 admin/models/mailbox.py:77 +msgid "address" +msgstr "adress" + +#: admin/models/alias.py:28 +#, fuzzy +#| msgid "The alias name" +msgid "The alias address." +msgstr "Alias namn" + +#: admin/models/alias.py:33 admin/models/domain.py:37 +#: admin/models/domain_alias.py:24 +msgid "enabled" +msgstr "aktiverad" + +#: admin/models/alias.py:34 admin/models/domain_alias.py:25 +msgid "Check to activate this alias" +msgstr "Markera för aktivera detta alias" + +#: admin/models/alias.py:79 +msgid "distribution list" +msgstr "distributionslista" + +#: admin/models/alias.py:80 +msgid "forward" +msgstr "vidarebefordra" + +#: admin/models/alias.py:81 +msgid "alias" +msgstr "alias" + +#: admin/models/alias.py:112 +#, fuzzy +#| msgid "Invalid request" +msgid "Invalid address" +msgstr "Ogiltig förfrågan" + +#: admin/models/alias.py:133 +#, fuzzy +#| msgid "Local recipient %s not found" +msgid "Local recipient {}@{} not found" +msgstr "Lokal mottagare %s kunde inte hittas" + +#: admin/models/alias.py:162 +#, python-format +msgid "Invalid line: %s" +msgstr "Ogiltig rad: %s" + +#: admin/models/alias.py:168 +#, python-format +msgid "Domain '%s' does not exist" +msgstr "Domänen '%s' existerar inte" + +#: admin/models/domain.py:33 admin/models/domain_alias.py:17 +msgid "name" +msgstr "namn" + +#: admin/models/domain.py:34 +msgid "The domain name" +msgstr "Domännamnet" + +#: admin/models/domain.py:38 +msgid "Check to activate this domain" +msgstr "Markera för att aktivera denna domän" + +#: admin/models/domain.py:160 admin/models/domain_alias.py:47 +msgid "Invalid line" +msgstr "Ogiltig rad" + +#: admin/models/domain.py:168 +#, python-format +msgid "Invalid quota value for domain '%s'" +msgstr "Ogiltig kvota värde för domänen '%s'" + +#: admin/models/domain_alias.py:18 +msgid "The alias name" +msgstr "Alias namn" + +#: admin/models/domain_alias.py:20 +msgid "target" +msgstr "mål" + +#: admin/models/domain_alias.py:21 +msgid "The domain this alias points to" +msgstr "Domän dit detta alias pekar mot" + +#: admin/models/domain_alias.py:56 +#, python-format +msgid "Unknown domain %s" +msgstr "Okänt kommando %s" + +#: admin/models/mailbox.py:79 +msgid "Mailbox address (without the @domain.tld part)" +msgstr "Brevlåde adress (utan @domain.tld)" + +#: admin/models/mailbox.py:144 +#, python-format +msgid "Failed to retrieve mailbox location (%s)" +msgstr "Kunde inte hitta brevlådans sökväg (%s)" + +#: admin/models/mailbox.py:233 +#, python-format +msgid "Quota is greater than the allowed domain's limit (%dM)" +msgstr "Kvotan är större än domänens gräns (%dM)" + +#: admin/models/mailbox.py:239 +msgid "A quota is required" +msgstr "En kvota krävs" + +#: admin/templates/admin/account_general_form.html:11 +#: admin/templates/admin/aliasform.html:8 +msgid "Domain is disabled" +msgstr "Domänen är inaktiverad" + +#: admin/templates/admin/domain_general_form.html:4 +#: admin/templates/admin/mailform.html:3 +#: admin/templates/admin/newdomainform.html:3 +msgid "MB" +msgstr "MB" + +#: admin/templates/admin/domain_options_form.html:7 +msgid "You can't create a new domain administrator" +msgstr "Du kan inte skapa en ny domänadministratör" + +#: admin/templates/admin/domain_statistics.html:15 +#: admin/templatetags/admin_tags.py:45 +msgid "Statistics" +msgstr "Statistik" + +#: admin/templates/admin/domain_statistics.html:15 +msgid "Domains and identities" +msgstr "Domäner och konton" + +#: admin/templates/admin/domain_statistics.html:22 +msgid "Global counters" +msgstr "Globala räknare" + +#: admin/templates/admin/domain_statistics.html:27 +msgid "Quantity" +msgstr "Mängd" + +#: admin/templates/admin/domain_statistics.html:33 +#: admin/templates/admin/editdomainform.html:24 +msgid "Domain aliases" +msgstr "Domän alias" + +#: admin/templates/admin/domain_statistics.html:45 +msgid "Per-domain counters" +msgstr "Per domän räknare" + +#: admin/templates/admin/domains.html:27 +#: admin/templates/admin/identity_headers.html:4 +msgid "Tags" +msgstr "Tagg" + +#: admin/templates/admin/domains.html:28 +#: admin/templates/admin/identity_headers.html:5 +msgid "Actions" +msgstr "Åtgärder" + +#: admin/templates/admin/domains_table.html:4 +msgid "disabled" +msgstr "inaktiverad" + +#: admin/templates/admin/editdomainform.html:4 +msgid "Administrators" +msgstr "Administratörer" + +#: admin/templates/admin/editdomainform.html:7 +msgid "Miscellaneous" +msgstr "Övrigt" + +#: admin/templates/admin/editdomainform.html:19 +msgid "No domain administrator defined" +msgstr "Ingen domän administratör definierad" + +#: admin/templates/admin/editdomainform.html:22 +msgid "Creation date" +msgstr "Skapelsedatum" + +#: admin/templates/admin/editdomainform.html:23 +msgid "Last modification date" +msgstr "Senast ändrad" + +#: admin/templates/admin/editdomainform.html:26 +msgid "Mailbox aliases" +msgstr "Brevlåde alias" + +#: admin/templates/admin/forward.html:2 +msgid "Automatically forward emails to another address" +msgstr "Vidarebefordra automatiskt e-post till en annan adress" + +#: admin/templates/admin/forward.html:9 admin/views/alias.py:113 +msgid "Update" +msgstr "Uppdatera" + +#: admin/templates/admin/identity_headers.html:3 +msgid "Fullname/Recipient" +msgstr "Fullständigt namn/Mottagare" + +#: admin/templates/admin/importform.html:6 +msgid "Help" +msgstr "Hjälp" + +#: admin/templates/admin/importform.html:13 +msgid "Importing..." +msgstr "Importerar..." + +#: admin/templates/admin/mailboxes_raw.html:3 +msgid "Choose a value" +msgstr "Välj ett värde" + +#: admin/templates/admin/mailform.html:3 +msgid "Use domain default value" +msgstr "Använd domänens standardvärde" + +#: admin/templates/admin/quota_headers.html:2 +msgid "Address" +msgstr "Adress" + +#: admin/templates/admin/quota_headers.html:3 +msgid "Value" +msgstr "Värde" + +#: admin/templates/admin/quota_headers.html:4 +msgid "Limit" +msgstr "Gräns" + +#: admin/templates/admin/quota_headers.html:5 +#, python-format +msgid "Usage (%%)" +msgstr "Användning (%%)" + +#: admin/templates/admin/searchform.html:6 +msgid "Search" +msgstr "Sök" + +#: admin/templatetags/admin_tags.py:19 +msgid "enabled_m" +msgstr "enabled_m" + +#: admin/templatetags/admin_tags.py:19 +msgid "enabled_f" +msgstr "enabled_f" + +#: admin/templatetags/admin_tags.py:40 +msgid "List domains" +msgstr "Lista domäner" + +#: admin/templatetags/admin_tags.py:53 +msgid "Add domain" +msgstr "Lägg till domän" + +#: admin/templatetags/admin_tags.py:62 admin/templatetags/admin_tags.py:126 +#: admin/views/import_.py:99 admin/views/import_.py:133 +msgid "Import" +msgstr "Importera" + +#: admin/templatetags/admin_tags.py:68 admin/templatetags/admin_tags.py:132 +#: admin/views/export.py:41 admin/views/export.py:69 +msgid "Export" +msgstr "Exportera" + +#: admin/templatetags/admin_tags.py:92 +msgid "List identities" +msgstr "Lista konton" + +#: admin/templatetags/admin_tags.py:97 +msgid "List quotas" +msgstr "Lista kvoter" + +#: admin/templatetags/admin_tags.py:102 +msgid "Add account" +msgstr "Lägg till konto" + +#: admin/templatetags/admin_tags.py:108 +msgid "Add alias" +msgstr "Lägg till alias" + +#: admin/templatetags/admin_tags.py:114 +msgid "Add forward" +msgstr "Lägg till vidarebefodring" + +#: admin/templatetags/admin_tags.py:120 +msgid "Add distribution list" +msgstr "Lägg till distributionslista" + +#: admin/templatetags/admin_tags.py:152 +msgid "View the domain's identities" +msgstr "Visa domänens konton" + +#: admin/templatetags/admin_tags.py:159 admin/templatetags/admin_tags.py:182 +#: admin/templatetags/admin_tags.py:190 +#, python-format +msgid "Delete %s?" +msgstr "Radera %s?" + +#: admin/templatetags/admin_tags.py:267 +msgid "Remove this permission" +msgstr "Radera detta tillstånd" + +#: admin/views/alias.py:29 +msgid "Alias with this name already exists" +msgstr "Alias med detta namn existerar redan" + +#: admin/views/alias.py:53 +msgid "Create" +msgstr "Skapa" + +#: admin/views/alias.py:65 +msgid "New distribution list" +msgstr "Ny distributionslista" + +#: admin/views/alias.py:67 +msgid "Distribution list created" +msgstr "Distributionslista skapad" + +#: admin/views/alias.py:76 +msgid "New alias" +msgstr "Nytt alias" + +#: admin/views/alias.py:77 +msgid "Alias created" +msgstr "Alias skapat" + +#: admin/views/alias.py:86 +msgid "New forward" +msgstr "Ny vidarebefodring" + +#: admin/views/alias.py:87 +msgid "Forward created" +msgstr "Vidarebefodring skapad" + +#: admin/views/alias.py:101 +msgid "Distribution list modified" +msgstr "Distributionslista ändrad" + +#: admin/views/alias.py:103 +msgid "Forward modified" +msgstr "Vidarebefodring ändrad" + +#: admin/views/alias.py:105 +msgid "Alias modified" +msgstr "Alias ändrat" + +#: admin/views/domain.py:132 +msgid "You can't delete your own domain" +msgstr "Du kan inte radera din egen domän" + +#: admin/views/domain.py:135 +msgid "Domain deleted" +msgid_plural "Domains deleted" +msgstr[0] "Domän raderad" +msgstr[1] "Domäner raderade" + +#: admin/views/export.py:40 +msgid "Export identities" +msgstr "Exportera konton" + +#: admin/views/export.py:68 +msgid "Export domains" +msgstr "Exportera domäner" + +#: admin/views/identity.py:108 admin/views/identity.py:200 +#: admin/views/identity.py:205 +msgid "Invalid request" +msgstr "Ogiltig förfrågan" + +#: admin/views/identity.py:190 +msgid "Account deleted" +msgid_plural "Accounts deleted" +msgstr[0] "Konto raderat" +msgstr[1] "Konton raderade" + +#: admin/views/import_.py:64 +#, python-format +msgid "Object already exists: %s" +msgstr "Objektet existerar redan: %s" + +#: admin/views/import_.py:68 +#, python-format +msgid "%d objects imported successfully" +msgstr "%d objekt importerade" + +#: admin/views/import_.py:86 +#, python-format +msgid "" +"Provide a CSV file where lines respect one of the following formats:\n" +"
      \n" +"
    • domain; name; quota; enabled
    • \n" +"
    • domainalias; name; targeted domain; enabled
    • \n" +" %s\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 "" +"Tillhandahåll en CSV fil där raderna har följande format:\n" +"
      \n" +"
    • domain; name; quota; enabled
    • \n" +"
    • domainalias; name; targeted domain; enabled
    • \n" +"%s\n" +"
    \n" +"

    det första elementet i varje rad är obligatoriskt och måste vara lika med " +"en av de tidigare värdena.\n" +"

    Du kan använda ett annat tecken som separator.\n" + +#: admin/views/import_.py:98 +msgid "Import domains" +msgstr "Importera domäner" + +#: admin/views/import_.py:120 +msgid "" +"Provide a CSV file where lines respect one of the following formats:\n" +"

      \n" +"
    • account; loginname; password; first name; last name; enabled; group; " +"address; quota; [, domain, ...]
    • \n" +"
    • alias; address; enabled; internal recipient
    • \n" +"
    • forward; address; enabled; external recipient
    • \n" +"
    • dlist; address; enabled; recipient; recipient; ...
    • \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 "" +"Tillhandahåll en CSV-fil där raderna har följande format:\n" +"
      \n" +"
    • account; loginname; password; first name; last name; enabled; group; " +"address; quota; [, domain, ...]
    • \n" +"
    • alias; address; enabled; internal recipient
    • \n" +"
    • forward; address; enabled; external recipient
    • \n" +"
    • dlist; address; enabled; recipient; recipient; ...
    • \n" +"
    \n" +"

    Det första elementet i varje rad är obligatorisk och måste vara lika med " +"en av de tidigare värdena.

    \n" +"\n" +"

    Du kan använda ett annat tecken separator.

    \n" + +#: admin/views/import_.py:132 +msgid "Import identities" +msgstr "Importera konton" + +#: admin/views/user.py:34 +msgid "Forward updated" +msgstr "Vidarebefordring uppdaterad" diff --git a/modoboa/admin/locale/sv/LC_MESSAGES/djangojs.po b/modoboa/admin/locale/sv/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..c3c55396f --- /dev/null +++ b/modoboa/admin/locale/sv/LC_MESSAGES/djangojs.po @@ -0,0 +1,57 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Olle Gustafsson , 2013 +# Olle Gustafsson , 2013,2015 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 08:20+0100\n" +"PO-Revision-Date: 2015-01-13 15:21+0000\n" +"Last-Translator: Olle Gustafsson \n" +"Language-Team: Swedish (http://www.transifex.com/projects/p/modoboa/language/" +"sv/)\n" +"Language: sv\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: static/admin/js/admin.js:126 +msgid "No more domain to show" +msgstr "Inga fler domäner att visa" + +#: static/admin/js/admin.js:145 +msgid "Do not delete domain directory" +msgstr "Radera inte domän katalog" + +#: static/admin/js/admin.js:147 +msgid "This operation will remove ALL data associated to this domain." +msgstr "Denna åtgärd kommer radera ALL data associerad med den här domänen." + +#: static/admin/js/admin.js:148 +msgid "" +"This operation will remove all data associated to this domain, excepting " +"accounts." +msgstr "" +"Den här operationen raderar all data som är associerad med denna domän, " +"förutom konton." + +#: static/admin/js/admin.js:231 +msgid "No domain administrator defined" +msgstr "Ingen domänadministratör definierad" + +#: static/admin/js/admin.js:286 static/admin/js/admin.js.c:354 +msgid "No more identity to show" +msgstr "Inga fler konton att visa" + +#: static/admin/js/admin.js:328 +msgid "Do not delete mailbox directory" +msgstr "Radera inte brevlådans katalog" + +#: static/admin/js/admin.js:357 +msgid "No more quota to show" +msgstr "Ingen mer kvota att visa" diff --git a/modoboa/admin/management/__init__.py b/modoboa/admin/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/admin/management/commands/__init__.py b/modoboa/admin/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/admin/management/commands/_import.py b/modoboa/admin/management/commands/_import.py new file mode 100644 index 000000000..514bceb5d --- /dev/null +++ b/modoboa/admin/management/commands/_import.py @@ -0,0 +1,53 @@ +import os.path +import sys +import csv +from progressbar import ProgressBar, Percentage, Bar, ETA + +from modoboa.core.models import User +from modoboa.lib import events +from modoboa.lib.exceptions import Conflict + +from ... import lib + + +def import_csv(filename, options): + """Import objects from a CSV file.""" + superadmin = User.objects.filter(is_superuser=True).first() + if not os.path.exists(filename): + print('File not found') + sys.exit(1) + + num_lines = sum(1 for line in open(filename)) + pbar = ProgressBar( + widgets=[Percentage(), Bar(), ETA()], maxval=num_lines + ).start() + with open(filename, 'r') as f: + reader = csv.reader(f, delimiter=';') + i = 0 + for row in reader: + i += 1 + pbar.update(i) + if not row: + continue + + try: + fct = getattr(lib, "import_%s" % row[0].strip()) + except AttributeError: + fct = events.raiseQueryEvent( + 'ImportObject', row[0].strip()) + if not fct: + continue + fct = fct[0] + + try: + fct(superadmin, row, options) + except Conflict: + if options['continue_if_exists']: + continue + + raise Conflict( + "Object already exists: %s" + % options['sepchar'].join(row[:2]) + ) + + pbar.finish() diff --git a/modoboa/admin/management/commands/export_domains.py b/modoboa/admin/management/commands/export_domains.py new file mode 100644 index 000000000..0f1114619 --- /dev/null +++ b/modoboa/admin/management/commands/export_domains.py @@ -0,0 +1,28 @@ +import sys +import csv +from optparse import make_option + +from django.core.management.base import BaseCommand + +from modoboa.core import load_core_settings +from modoboa.core.extensions import exts_pool +from modoboa.core.management.commands import CloseConnectionMixin +from ...models import Domain + + +class Command(BaseCommand, CloseConnectionMixin): + help = 'Export domains and domain aliases to a csv' + + option_list = BaseCommand.option_list + ( + make_option( + '--sepchar', action='store_true', dest='sepchar', default=';' + ), + ) + + def handle(self, *args, **kwargs): + exts_pool.load_all() + load_core_settings() + + csvwriter = csv.writer(sys.stdout, delimiter=kwargs['sepchar']) + for dom in Domain.objects.all(): + dom.to_csv(csvwriter) diff --git a/modoboa/admin/management/commands/export_identities.py b/modoboa/admin/management/commands/export_identities.py new file mode 100644 index 000000000..95551647e --- /dev/null +++ b/modoboa/admin/management/commands/export_identities.py @@ -0,0 +1,32 @@ +import sys +import csv +from optparse import make_option + +from django.core.management.base import BaseCommand + +from modoboa.core import load_core_settings +from modoboa.core.models import User +from modoboa.core.extensions import exts_pool +from modoboa.core.management.commands import CloseConnectionMixin +from ...models import Alias + + +class Command(BaseCommand, CloseConnectionMixin): + help = 'Export identities (mailbox and aliases) to a csv' + + option_list = BaseCommand.option_list + ( + make_option( + '--sepchar', action='store_true', dest='sepchar', default=';' + ), + ) + + def handle(self, *args, **kwargs): + exts_pool.load_all() + load_core_settings() + + csvwriter = csv.writer(sys.stdout, delimiter=kwargs['sepchar']) + for u in User.objects.all(): + u.to_csv(csvwriter) + + for a in Alias.objects.prefetch_related('mboxes', 'aliases'): + a.to_csv(csvwriter) diff --git a/modoboa/admin/management/commands/handle_mailbox_operations.py b/modoboa/admin/management/commands/handle_mailbox_operations.py new file mode 100644 index 000000000..b87823be4 --- /dev/null +++ b/modoboa/admin/management/commands/handle_mailbox_operations.py @@ -0,0 +1,97 @@ + +import logging +from optparse import make_option +import os + +from django.core.management.base import BaseCommand + +from modoboa.core.management.commands import CloseConnectionMixin +from modoboa.lib import parameters +from modoboa.lib.sysutils import exec_cmd +from modoboa.lib.exceptions import InternalError + +from ...models import MailboxOperation +from ...modo_extension import AdminConsole + + +class OperationError(Exception): + pass + + +class Command(BaseCommand, CloseConnectionMixin): + help = 'Handles rename and delete operations on mailboxes' + + option_list = BaseCommand.option_list + ( + make_option( + '--pidfile', default='/tmp/handle_mailbox_operations.pid', + help='Path to the file that will contain the PID of this process' + ), + ) + + def __init__(self, *args, **kwargs): + super(Command, self).__init__(*args, **kwargs) + self.logger = logging.getLogger('modoboa.admin') + + def rename_mailbox(self, operation): + if not os.path.exists(operation.argument): + return + new_mail_home = operation.mailbox.mail_home + dirname = os.path.dirname(new_mail_home) + if not os.path.exists(dirname): + try: + os.makedirs(dirname) + except os.error as e: + raise OperationError(str(e)) + code, output = exec_cmd( + "mv %s %s" % (operation.argument, new_mail_home) + ) + if code: + raise OperationError(output) + + def delete_mailbox(self, operation): + if not os.path.exists(operation.argument): + return + code, output = exec_cmd( + "rm -r %s" % operation.argument + ) + if code: + raise OperationError(output) + + def check_pidfile(self, path): + """Check if this command is already running + + :param str path: path to the file containing the PID + :return: a boolean, True means we can go further + """ + if os.path.exists(path): + with open(path) as fp: + pid = fp.read().strip() + code, output = exec_cmd( + "grep handle_mailbox_operations /proc/%s/cmdline" % pid + ) + if not code: + return False + with open(path, 'w') as fp: + print >> fp, os.getpid() + return True + + def handle(self, *args, **options): + AdminConsole().load() + if parameters.get_admin("HANDLE_MAILBOXES") == 'no': + return + if not self.check_pidfile(options['pidfile']): + return + for ope in MailboxOperation.objects.all(): + try: + f = getattr(self, '%s_mailbox' % ope.type) + except AttributeError: + continue + try: + f(ope) + except (OperationError, InternalError) as e: + self.logger.critical('%s failed (reason: %s)', + ope, str(e).encode('utf-8')) + else: + self.logger.info('%s succeed', ope) + ope.delete() + os.unlink(options['pidfile']) diff --git a/modoboa/admin/management/commands/import_domains.py b/modoboa/admin/management/commands/import_domains.py new file mode 100644 index 000000000..a4669da14 --- /dev/null +++ b/modoboa/admin/management/commands/import_domains.py @@ -0,0 +1,32 @@ +from optparse import make_option + +from django.core.management.base import BaseCommand + + +from modoboa.core import load_core_settings +from modoboa.core.extensions import exts_pool +from modoboa.core.management.commands import CloseConnectionMixin + +from ._import import import_csv + + +class Command(BaseCommand, CloseConnectionMixin): + args = 'csvfile' + help = 'Import domains and domain aliases from a csv file' + + option_list = BaseCommand.option_list + ( + make_option( + '--sepchar', action='store_true', dest='sepchar', default=';' + ), + make_option( + '--continue-if-exists', action='store_true', + dest='continue_if_exists', default=True + ) + ) + + def handle(self, *args, **kwargs): + exts_pool.load_all() + load_core_settings() + + for filename in args: + import_csv(filename, kwargs) diff --git a/modoboa/admin/management/commands/import_identities.py b/modoboa/admin/management/commands/import_identities.py new file mode 100644 index 000000000..1cf710bd7 --- /dev/null +++ b/modoboa/admin/management/commands/import_identities.py @@ -0,0 +1,35 @@ +from optparse import make_option + +from django.core.management.base import BaseCommand + +from modoboa.core import load_core_settings +from modoboa.core.extensions import exts_pool +from modoboa.core.management.commands import CloseConnectionMixin + +from ._import import import_csv + + +class Command(BaseCommand, CloseConnectionMixin): + args = 'csvfile' + help = 'Import identities from a csv file' + + option_list = BaseCommand.option_list + ( + make_option( + '--sepchar', action='store_true', dest='sepchar', default=';' + ), + make_option( + '--continue-if-exists', action='store_true', + dest='continue_if_exists', default=True + ), + make_option( + '--crypt-password', action='store_true', dest='crypt_password', + default=False + ) + ) + + def handle(self, *args, **kwargs): + exts_pool.load_all() + load_core_settings() + + for filename in args: + import_csv(filename, kwargs) diff --git a/modoboa/admin/migrations/0001_initial.py b/modoboa/admin/migrations/0001_initial.py new file mode 100644 index 000000000..ec5eb96bb --- /dev/null +++ b/modoboa/admin/migrations/0001_initial.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Alias', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('address', models.CharField(help_text='The alias address.', max_length=254, verbose_name='address')), + ('enabled', models.BooleanField(default=True, help_text='Check to activate this alias', verbose_name='enabled')), + ('internal', models.BooleanField(default=False)), + ], + options={ + 'ordering': ['address'], + 'permissions': (('view_aliases', 'View aliases'),), + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='AliasRecipient', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('address', models.EmailField(max_length=75)), + ('alias', models.ForeignKey(to='admin.Alias')), + ('r_alias', models.ForeignKey(related_name='alias_recipient_aliases', blank=True, to='admin.Alias', null=True)), + ], + options={ + 'db_table': 'modoboa_admin_aliasrecipient', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Domain', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(help_text='The domain name', unique=True, max_length=100, verbose_name='name')), + ('quota', models.IntegerField()), + ('enabled', models.BooleanField(default=True, help_text='Check to activate this domain', verbose_name='enabled')), + ('type', models.CharField(default=b'domain', max_length=20)), + ], + options={ + 'ordering': ['name'], + 'permissions': (('view_domain', 'View domain'), ('view_domains', 'View domains')), + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='DomainAlias', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(help_text='The alias name', unique=True, max_length=100, verbose_name='name')), + ('enabled', models.BooleanField(default=True, help_text='Check to activate this alias', verbose_name='enabled')), + ], + options={ + 'permissions': (('view_domaliases', 'View domain aliases'),), + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Mailbox', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('address', models.CharField(help_text='Mailbox address (without the @domain.tld part)', max_length=252, verbose_name='address')), + ('quota', models.PositiveIntegerField()), + ('use_domain_quota', models.BooleanField(default=False)), + ], + options={ + 'permissions': (('view_mailboxes', 'View mailboxes'),), + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='MailboxOperation', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('type', models.CharField(max_length=20, choices=[(b'rename', b'rename'), (b'delete', b'delete')])), + ('argument', models.TextField()), + ('mailbox', models.ForeignKey(blank=True, to='admin.Mailbox', null=True)), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='ObjectDates', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('creation', models.DateTimeField(auto_now_add=True)), + ('last_modification', models.DateTimeField(auto_now=True)), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Quota', + fields=[ + ('username', models.EmailField(max_length=254, serialize=False, primary_key=True)), + ('bytes', models.BigIntegerField(default=0)), + ('messages', models.IntegerField(default=0)), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.AddField( + model_name='mailbox', + name='dates', + field=models.ForeignKey(to='admin.ObjectDates'), + preserve_default=True, + ), + migrations.AddField( + model_name='mailbox', + name='domain', + field=models.ForeignKey(to='admin.Domain'), + preserve_default=True, + ), + migrations.AddField( + model_name='mailbox', + name='user', + field=models.OneToOneField(to=settings.AUTH_USER_MODEL), + preserve_default=True, + ), + migrations.AddField( + model_name='domainalias', + name='dates', + field=models.ForeignKey(to='admin.ObjectDates'), + preserve_default=True, + ), + migrations.AddField( + model_name='domainalias', + name='target', + field=models.ForeignKey(verbose_name='target', to='admin.Domain', help_text='The domain this alias points to'), + preserve_default=True, + ), + migrations.AddField( + model_name='domain', + name='dates', + field=models.ForeignKey(to='admin.ObjectDates'), + preserve_default=True, + ), + migrations.AddField( + model_name='aliasrecipient', + name='r_mailbox', + field=models.ForeignKey(blank=True, to='admin.Mailbox', null=True), + preserve_default=True, + ), + migrations.AlterUniqueTogether( + name='aliasrecipient', + unique_together=set([('alias', 'r_alias'), ('alias', 'r_mailbox')]), + ), + migrations.AddField( + model_name='alias', + name='dates', + field=models.ForeignKey(to='admin.ObjectDates'), + preserve_default=True, + ), + migrations.AddField( + model_name='alias', + name='domain', + field=models.ForeignKey(to='admin.Domain', null=True), + preserve_default=True, + ), + migrations.AlterUniqueTogether( + name='alias', + unique_together=set([('address', 'internal')]), + ), + ] diff --git a/modoboa/admin/migrations/0002_migrate_from_modoboa_admin.py b/modoboa/admin/migrations/0002_migrate_from_modoboa_admin.py new file mode 100644 index 000000000..96527fae5 --- /dev/null +++ b/modoboa/admin/migrations/0002_migrate_from_modoboa_admin.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +def rename_content_types(apps, schema_editor): + """Rename old content types if necessary.""" + ContentType = apps.get_model("contenttypes", "ContentType") + for ct in ContentType.objects.filter(app_label="admin"): + try: + old_ct = ContentType.objects.get( + app_label="modoboa_admin", model=ct.model) + except ContentType.DoesNotExist: + continue + old_ct.app_label = "admin" + ct.delete() + old_ct.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('admin', '0001_initial'), + ] + + operations = [ + migrations.RunPython(rename_content_types), + ] diff --git a/modoboa/admin/migrations/__init__.py b/modoboa/admin/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/admin/models/__init__.py b/modoboa/admin/models/__init__.py new file mode 100644 index 000000000..c2c223752 --- /dev/null +++ b/modoboa/admin/models/__init__.py @@ -0,0 +1,10 @@ +from .base import AdminObject +from .domain import Domain +from .domain_alias import DomainAlias +from .mailbox import Mailbox, Quota, MailboxOperation +from .alias import Alias, AliasRecipient + +__all__ = [ + 'Domain', 'DomainAlias', 'Mailbox', 'Quota', 'Alias', + 'MailboxOperation', 'AdminObject', 'AliasRecipient' +] diff --git a/modoboa/admin/models/alias.py b/modoboa/admin/models/alias.py new file mode 100644 index 000000000..7964b1dee --- /dev/null +++ b/modoboa/admin/models/alias.py @@ -0,0 +1,214 @@ +"""Models related to aliases management.""" + +from django.db import models +from django.utils.encoding import python_2_unicode_compatible, smart_text +from django.utils.translation import ugettext as _, ugettext_lazy + +import reversion + +from modoboa.lib.email_utils import split_mailbox +from modoboa.lib.exceptions import ( + PermDeniedException, BadRequest, Conflict, NotFound +) + +from .base import AdminObject +from .domain import Domain +from .mailbox import Mailbox +from .. import signals + + +@python_2_unicode_compatible +class Alias(AdminObject): + + """Mailbox alias.""" + + address = models.CharField( + ugettext_lazy('address'), max_length=254, + help_text=ugettext_lazy( + "The alias address." + ) + ) + domain = models.ForeignKey(Domain, null=True) + enabled = models.BooleanField( + ugettext_lazy('enabled'), + help_text=ugettext_lazy("Check to activate this alias"), + default=True + ) + internal = models.BooleanField(default=False) + _objectname = 'MailboxAlias' + + class Meta: + permissions = ( + ("view_aliases", "View aliases"), + ) + ordering = ["address"] + unique_together = (("address", "internal"), ) + app_label = "admin" + + def __str__(self): + return smart_text(self.address) + + @property + def identity(self): + return self.address + + @property + def name_or_rcpt(self): + rcpts_count = self.recipients_count + if not rcpts_count: + return "---" + rcpts = self.recipients + if rcpts_count > 1: + return "%s, ..." % rcpts[0] + return rcpts[0] + + @property + def type(self): + cpt = self.recipients_count + if cpt > 1: + return "dlist" + qset = self.aliasrecipient_set.filter( + r_mailbox__isnull=True, r_alias__isnull=True) + if qset.exists(): + return "forward" + return "alias" + + @property + def tags(self): + labels = { + "dlist": _("distribution list"), + "forward": _("forward"), + "alias": _("alias") + } + altype = self.type + return [{"name": altype, "label": labels[altype], "type": "idt"}] + + def post_create(self, creator): + from modoboa.lib.permissions import grant_access_to_object + super(Alias, self).post_create(creator) + if creator.is_superuser: + for admin in self.domain.admins: + grant_access_to_object(admin, self) + + def set_recipients(self, address_list): + """Set recipients for this alias. Special recipients: + + * local mailbox + extension: r_mailbox will be set to local mailbox + * alias address == recipient address: valid only to keep local copies + (when a forward is defined) and to create exceptions when a catchall + is defined on the associated domain + + """ + to_create = [] + for address in set(address_list): + if not address: + continue + if self.aliasrecipient_set.filter(address=address).exists(): + continue + local_part, domname, extension = ( + split_mailbox(address, return_extension=True)) + if domname is None: + raise BadRequest( + u"%s %s" % (_("Invalid address"), address) + ) + domain = Domain.objects.filter(name=domname).first() + kwargs = {"address": address, "alias": self} + if ( + (domain is not None) and + ( + any( + r[1] for r in signals.use_external_recipients.send( + self, recipients=address) + ) is False + ) + ): + rcpt = Mailbox.objects.filter( + domain=domain, address=local_part).first() + if rcpt is None: + rcpt = Alias.objects.filter( + address='%s@%s' % (local_part, domname) + ).first() + if rcpt is None: + raise NotFound( + _("Local recipient {}@{} not found") + .format(local_part, domname) + ) + if rcpt.address == self.address: + raise Conflict + kwargs["r_alias"] = rcpt + else: + kwargs["r_mailbox"] = rcpt + to_create.append(AliasRecipient(**kwargs)) + AliasRecipient.objects.bulk_create(to_create) + # Remove old recipients + self.aliasrecipient_set.exclude( + address__in=address_list).delete() + + @property + def recipients(self): + """Return the recipient list.""" + return self.aliasrecipient_set.values_list("address", flat=True) + + @property + def recipients_count(self): + """Return the number of recipients of this alias.""" + return self.aliasrecipient_set.count() + + def from_csv(self, user, row, expected_elements=5): + """Create a new alias from a CSV file entry + + """ + if len(row) < expected_elements: + raise BadRequest(_("Invalid line: %s" % row)) + address = row[1].strip() + localpart, domname = split_mailbox(address) + try: + domain = Domain.objects.get(name=domname) + except Domain.DoesNotExist: + raise BadRequest(_("Domain '%s' does not exist" % domname)) + if not user.can_access(domain): + raise PermDeniedException + if Alias.objects.filter(address=address).exists(): + raise Conflict + self.address = address + self.domain = domain + self.enabled = (row[2].strip() in ["True", "1", "yes", "y"]) + self.save() + self.set_recipients([raddress.strip() for raddress in row[3:]]) + self.post_create(user) + + def to_csv(self, csvwriter): + row = [self.type, self.address.encode("utf-8"), self.enabled] + row += self.recipients + csvwriter.writerow(row) + +reversion.register(Alias) + + +@python_2_unicode_compatible +class AliasRecipient(models.Model): + + """An alias recipient.""" + + address = models.EmailField() + alias = models.ForeignKey(Alias) + + # if recipient is a local mailbox + r_mailbox = models.ForeignKey(Mailbox, blank=True, null=True) + # if recipient is a local alias + r_alias = models.ForeignKey( + Alias, related_name="alias_recipient_aliases", blank=True, null=True) + + class Meta: + app_label = "admin" + db_table = "modoboa_admin_aliasrecipient" + unique_together = [ + ("alias", "r_mailbox"), + ("alias", "r_alias") + ] + + def __str__(self): + """Return alias and recipient.""" + return smart_text( + "{} -> {}".format(self.alias.address, self.alias) + ) diff --git a/modoboa/admin/models/base.py b/modoboa/admin/models/base.py new file mode 100644 index 000000000..b1d7ae3ae --- /dev/null +++ b/modoboa/admin/models/base.py @@ -0,0 +1,82 @@ +from django.db import models + +from modoboa.lib import events +from modoboa.lib.permissions import ( + grant_access_to_object, ungrant_access_to_object +) + + +class ObjectDates(models.Model): + + """Dates recording for admin objects + + This table keeps creation and last modification dates for Domains, + domain aliases, mailboxes and aliases objects. + """ + creation = models.DateTimeField(auto_now_add=True) + last_modification = models.DateTimeField(auto_now=True) + + class Meta: + app_label = "admin" + + @staticmethod + def set_for_object(obj): + """Initialize or update dates for a given object. + + :param obj: an admin object (Domain, Mailbox, etc) + """ + try: + dates = getattr(obj, "dates") + except ObjectDates.DoesNotExist: + dates = ObjectDates() + dates.save() + obj.dates = dates + + +class AdminObject(models.Model): + + """Abstract model to support dates + + Inherit from this model to automatically add the "dates" feature + to another model. It defines the appropriate field and handles + saves. + """ + dates = models.ForeignKey(ObjectDates) + _objectname = None + + class Meta: + abstract = True + + @property + def creation(self): + return self.dates.creation + + @property + def last_modification(self): + return self.dates.last_modification + + @property + def objectname(self): + if self._objectname is None: + return self.__class__.__name__ + return self._objectname + + def post_create(self, creator): + grant_access_to_object(creator, self, is_owner=True) + events.raiseEvent("%sCreated" % self.objectname, creator, self) + + def save(self, *args, **kwargs): + ObjectDates.set_for_object(self) + if "creator" in kwargs: + creator = kwargs["creator"] + del kwargs["creator"] + else: + creator = None + super(AdminObject, self).save(*args, **kwargs) + if creator is not None: + self.post_create(creator) + + def delete(self): + events.raiseEvent("%sDeleted" % self.objectname, self) + ungrant_access_to_object(self) + super(AdminObject, self).delete() diff --git a/modoboa/admin/models/domain.py b/modoboa/admin/models/domain.py new file mode 100644 index 000000000..3021e7281 --- /dev/null +++ b/modoboa/admin/models/domain.py @@ -0,0 +1,186 @@ +"""Models related to domains management.""" + +from django.contrib.contenttypes import generic +from django.db import models +from django.db.models.manager import Manager +from django.utils.translation import ugettext as _, ugettext_lazy + +import reversion + +from .base import AdminObject +from modoboa.core.models import User, ObjectAccess +from modoboa.lib import events, parameters +from modoboa.lib.exceptions import BadRequest, Conflict + + +class DomainManager(Manager): + + def get_for_admin(self, admin): + """Return the domains belonging to this admin + + The result is a ``QuerySet`` object, so this function can be used + to fill ``ModelChoiceField`` objects. + """ + if admin.is_superuser: + return self.get_query_set() + return self.get_query_set().filter(owners__user=admin) + + +class Domain(AdminObject): + + """Mail domain.""" + + name = models.CharField(ugettext_lazy('name'), max_length=100, unique=True, + help_text=ugettext_lazy("The domain name")) + quota = models.IntegerField() + enabled = models.BooleanField( + ugettext_lazy('enabled'), + help_text=ugettext_lazy("Check to activate this domain"), + default=True + ) + owners = generic.GenericRelation(ObjectAccess) + type = models.CharField(default="domain", max_length=20) + + objects = DomainManager() + + class Meta: + permissions = ( + ("view_domain", "View domain"), + ("view_domains", "View domains"), + ) + ordering = ["name"] + app_label = "admin" + + @property + def domainalias_count(self): + return self.domainalias_set.count() + + @property + def mailbox_count(self): + return self.mailbox_set.count() + + @property + def mbalias_count(self): + return self.alias_set.count() + + @property + def identities_count(self): + """Total number of identities in this domain.""" + return ( + self.mailbox_set.count() + + self.alias_set.filter(internal=False).count()) + + @property + def tags(self): + if self.type == "domain": + return [{"name": "domain", "label": _("Domain"), "type": "dom"}] + return events.raiseQueryEvent("GetTagsForDomain", self) + + @property + def admins(self): + """Return the domain administrators of this domain + + :return: a list of User objects + """ + return [oa.user for oa in self.owners.filter(user__is_superuser=False)] + + @property + def aliases(self): + return self.domainalias_set + + def add_admin(self, account): + """Add a new administrator for this domain + + :param User account: the administrotor to add + """ + from modoboa.lib.permissions import grant_access_to_object + grant_access_to_object(account, self) + for mb in self.mailbox_set.all(): + if mb.user.has_perm("admin.add_domain"): + continue + grant_access_to_object(account, mb) + grant_access_to_object(account, mb.user) + for al in self.alias_set.all(): + grant_access_to_object(account, al) + + def remove_admin(self, account): + """Remove an administrator of this domain. + + :param User account: administrator to remove + """ + from modoboa.lib.permissions import \ + ungrant_access_to_object, get_object_owner + + if get_object_owner(self) == account: + events.raiseEvent('DomainOwnershipRemoved', account, self) + ungrant_access_to_object(self, account) + for mb in self.mailbox_set.all(): + if mb.user.has_perm("admin.add_domain"): + continue + ungrant_access_to_object(mb, account) + ungrant_access_to_object(mb.user, account) + for al in self.alias_set.all(): + ungrant_access_to_object(al, account) + + def delete(self, fromuser, keepdir=False): + """Custom delete method. + """ + from modoboa.lib.permissions import ungrant_access_to_objects + from .mailbox import Quota + + if self.domainalias_set.count(): + events.raiseEvent("DomainAliasDeleted", self.domainalias_set.all()) + ungrant_access_to_objects(self.domainalias_set.all()) + if self.alias_set.count(): + events.raiseEvent("MailboxAliasDeleted", self.alias_set.all()) + ungrant_access_to_objects(self.alias_set.all()) + if parameters.get_admin("AUTO_ACCOUNT_REMOVAL") == "yes": + for account in User.objects.filter(mailbox__domain__name=self.name): + account.delete(fromuser, keepdir) + elif self.mailbox_set.count(): + Quota.objects.filter(username__contains='@%s' % self.name).delete() + events.raiseEvent("MailboxDeleted", self.mailbox_set.all()) + ungrant_access_to_objects(self.mailbox_set.all()) + super(Domain, self).delete() + + def __str__(self): + return self.name + + def from_csv(self, user, row): + """Create a new domain from a CSV entry. + + The expected fields order is the following:: + + "domain", name, quota, enabled + + :param ``core.User`` user: user creating the domain + :param str row: a list containing domain's definition + """ + if len(row) < 4: + raise BadRequest(_("Invalid line")) + self.name = row[1].strip() + if Domain.objects.filter(name=self.name).exists(): + raise Conflict + try: + self.quota = int(row[2].strip()) + except ValueError: + raise BadRequest( + _("Invalid quota value for domain '%s'") % self.name) + self.enabled = (row[3].strip() in ["True", "1", "yes", "y"]) + self.save(creator=user) + + def to_csv(self, csvwriter): + csvwriter.writerow(["domain", self.name, self.quota, self.enabled]) + for dalias in self.domainalias_set.all(): + dalias.to_csv(csvwriter) + + def post_create(self, creator): + """Post creation actions. + + :param ``User`` creator: user whos created this domain + """ + super(Domain, self).post_create(creator) + for domalias in self.domainalias_set.all(): + domalias.post_create(creator) + +reversion.register(Domain) diff --git a/modoboa/admin/models/domain_alias.py b/modoboa/admin/models/domain_alias.py new file mode 100644 index 000000000..5d6f37854 --- /dev/null +++ b/modoboa/admin/models/domain_alias.py @@ -0,0 +1,68 @@ +"""Models related to domain aliases management.""" + +from django.db import models +from django.utils.translation import ugettext as _, ugettext_lazy + +import reversion + +from .base import AdminObject +from .domain import Domain +from modoboa.lib.exceptions import BadRequest, Conflict + + +class DomainAlias(AdminObject): + + """Domain aliases.""" + + name = models.CharField(ugettext_lazy("name"), max_length=100, unique=True, + help_text=ugettext_lazy("The alias name")) + target = models.ForeignKey( + Domain, verbose_name=ugettext_lazy('target'), + help_text=ugettext_lazy("The domain this alias points to") + ) + enabled = models.BooleanField( + ugettext_lazy('enabled'), + help_text=ugettext_lazy("Check to activate this alias"), + default=True + ) + + class Meta: + permissions = ( + ("view_domaliases", "View domain aliases"), + ) + app_label = "admin" + + def __unicode__(self): + return self.name + + def from_csv(self, user, row): + """Create a domain alias from a CSV row + + Expected format: ["domainalias", domain alias name, targeted domain, enabled] + + :param user: a ``User`` object + :param row: a list containing the alias definition + """ + if len(row) < 4: + raise BadRequest(_("Invalid line")) + self.name = row[1].strip() + for model in [DomainAlias, Domain]: + if model.objects.filter(name=self.name).exists(): + raise Conflict + domname = row[2].strip() + try: + self.target = Domain.objects.get(name=domname) + except Domain.DoesNotExist: + raise BadRequest(_("Unknown domain %s") % domname) + self.enabled = row[3].strip() in ["True", "1", "yes", "y"] + self.save(creator=user) + + def to_csv(self, csvwriter): + """Export a domain alias using CSV format + + :param csvwriter: a ``csv.writer`` object + """ + csvwriter.writerow(["domainalias", self.name, + self.target.name, self.enabled]) + +reversion.register(DomainAlias) diff --git a/modoboa/admin/models/mailbox.py b/modoboa/admin/models/mailbox.py new file mode 100644 index 000000000..275291fb5 --- /dev/null +++ b/modoboa/admin/models/mailbox.py @@ -0,0 +1,320 @@ +"""Models related to mailboxes management.""" + +import os +import pwd + +from django.contrib.contenttypes.models import ContentType +from django.db import models +from django.db.models import Q +from django.db.models.manager import Manager +from django.utils.translation import ugettext as _, ugettext_lazy + +import reversion + +from .base import AdminObject +from .domain import Domain +from modoboa.core.models import User +from modoboa.lib import parameters +from modoboa.lib.exceptions import BadRequest, InternalError +from modoboa.lib.sysutils import exec_cmd + + +class Quota(models.Model): + + """Keeps track of Mailbox current quota.""" + + username = models.EmailField(primary_key=True, max_length=254) + bytes = models.BigIntegerField(default=0) + messages = models.IntegerField(default=0) + + class Meta: + app_label = "admin" + + +class MailboxManager(Manager): + + """Custom manager for Mailbox.""" + + def get_for_admin(self, admin, squery=None): + """Return the mailboxes that belong to this admin. + + The result will contain the mailboxes defined for each domain that + user can see. + + :param string squery: a search query + :return: a list of ``Mailbox`` objects + """ + qf = None + if squery is not None: + if '@' in squery: + parts = squery.split('@') + addrfilter = '@'.join(parts[:-1]) + domfilter = parts[-1] + qf = ( + Q(address__contains=addrfilter) & + Q(domain__name__contains=domfilter) + ) + else: + qf = ( + Q(address__contains=squery) | + Q(domain__name__contains=squery) + ) + ids = admin.objectaccess_set \ + .filter(content_type=ContentType.objects.get_for_model(Mailbox)) \ + .values_list('object_id', flat=True) + if qf is not None: + qf = Q(pk__in=ids) & qf + else: + qf = Q(pk__in=ids) + return self.get_query_set().select_related().filter(qf) + + +class Mailbox(AdminObject): + + """User mailbox.""" + + address = models.CharField( + ugettext_lazy('address'), max_length=252, + help_text=ugettext_lazy( + "Mailbox address (without the @domain.tld part)") + ) + quota = models.PositiveIntegerField() + use_domain_quota = models.BooleanField(default=False) + domain = models.ForeignKey(Domain) + user = models.OneToOneField(User) + + objects = MailboxManager() + + class Meta: + permissions = ( + ("view_mailboxes", "View mailboxes"), + ) + app_label = "admin" + + def __init__(self, *args, **kwargs): + super(Mailbox, self).__init__(*args, **kwargs) + self.__mail_home = None + + def __str__(self): + return self.full_address + + def __full_address(self, localpart): + return "%s@%s" % (localpart, self.domain.name) + + @property + def full_address(self): + return self.__full_address(self.address) + + @property + def enabled(self): + return self.user.is_active + + @property + def alias_count(self): + return ( + self.recipientalias_set.select_related("alias") + .filter(alias__internal=False).count() + ) + + @property + def mail_home(self): + """Retrieve the home directory of this mailbox. + + The home directory refers to the place on the file system + where the mailbox data is stored. + + We ask dovecot to give us this information because there are + several patterns to understand and we don't want to implement + them. + """ + hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False) + if hm is None or hm == "no": + return None + if self.__mail_home is None: + curuser = pwd.getpwuid(os.getuid()).pw_name + mbowner = parameters.get_admin("MAILBOXES_OWNER") + options = {} + if curuser != mbowner: + options['sudo_user'] = mbowner + code, output = exec_cmd( + "doveadm user %s -f home" % self.full_address, **options + ) + if code: + raise InternalError( + _("Failed to retrieve mailbox location (%s)" % output)) + self.__mail_home = output.strip() + return self.__mail_home + + @property + def alias_addresses(self): + """Return all alias address of this mailbox. + + :rtype: list of string + """ + aliases = [] + qset = ( + self.aliasrecipient_set.select_related("alias") + .filter(alias__internal=False) + ) + for alr in qset: + aliases += [alr.alias.address] + return aliases + + @property + def quota_value(self): + """Retrieve the ``Quota`` instance associated to this mailbox.""" + if not hasattr(self, "_quota_value"): + try: + self._quota_value = Quota.objects.get( + username=self.full_address) + except Quota.DoesNotExist: + return None + return self._quota_value + + @quota_value.setter + def quota_value(self, instance): + """Set the ``Quota`` for this mailbox.""" + self._quota_value = instance + + def rename_dir(self, old_mail_home): + hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False) + if hm is None or hm == "no": + return + MailboxOperation.objects.create( + mailbox=self, type='rename', argument=old_mail_home + ) + + def rename(self, address, domain): + """Rename the mailbox. + + To update the associated Quota record, we must create a new + one first, update the foreign key and then we can delete the + original record! + + :param string address: the new mailbox's address (local part) + :param Domain domain: the new mailbox's domain + + """ + old_mail_home = self.mail_home + old_qvalue = self.quota_value + self.address = address + self.domain = domain + self.quota_value = Quota.objects.create( + username=self.full_address, bytes=old_qvalue.bytes, + messages=old_qvalue.messages + ) + old_qvalue.delete() + self.rename_dir(old_mail_home) + + def delete_dir(self): + hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False) + if hm is None or hm == "no": + return + MailboxOperation.objects.create(type='delete', argument=self.mail_home) + + def set_quota(self, value=None, override_rules=False): + """Set or update quota's value for this mailbox. + + A value equal to 0 means the mailbox won't have any quota. The + following cases allow people to define such behaviour: + * The domain has no quota + * :keyword:`override_rules` is True + + :param integer value: the quota's value + :param bool override_rules: allow to override defined quota rules + """ + if value is None: + if self.use_domain_quota: + self.quota = self.domain.quota + else: + self.quota = 0 + elif int(value) > self.domain.quota and not override_rules: + raise BadRequest( + _("Quota is greater than the allowed domain's limit (%dM)" + % self.domain.quota) + ) + else: + self.quota = value + if not self.quota and self.domain.quota and not override_rules: + raise BadRequest(_("A quota is required")) + + def get_quota(self): + """Get quota limit. + + :rtype: int + """ + return int(self.quota_value.bytes / 1048576) + + def get_quota_in_percent(self): + """Get current quota usage. + + :rtype: int + """ + if not self.quota: + return 0 + return int( + self.quota_value.bytes / float(self.quota * 1048576) * 100 + ) + + def post_create(self, creator): + from modoboa.lib.permissions import grant_access_to_object + super(Mailbox, self).post_create(creator) + conditions = ( + creator.is_superuser, + not self.user.has_perm("admin.add_domain") + ) + if all(conditions): + # A super user is creating a new mailbox. Give + # access to that mailbox (and the associated + # account) to the appropriate domain admins, + # except if the new account has a more important + # role (SuperAdmin, Reseller) + for admin in self.domain.admins: + grant_access_to_object(admin, self) + grant_access_to_object(admin, self.user) + + def save(self, *args, **kwargs): + """Custom save. + + We just make sure a quota record is defined for this mailbox. + """ + if self.quota_value is None: + self.quota_value, created = Quota.objects.get_or_create( + username=self.full_address) + super(Mailbox, self).save(*args, **kwargs) + + def delete(self, keepdir=False): + """Custom delete method + + We try to delete the associated quota in the same time (it may + has already been removed if we're deleting a domain). + + :param bool keepdir: delete the mailbox home dir on the + filesystem or not + + """ + Quota.objects.filter(username=self.full_address).delete() + if not keepdir: + self.delete_dir() + super(Mailbox, self).delete() + +reversion.register(Mailbox) + + +class MailboxOperation(models.Model): + + """An operation on a mailbox.""" + + mailbox = models.ForeignKey(Mailbox, blank=True, null=True) + type = models.CharField( + max_length=20, choices=(('rename', 'rename'), ('delete', 'delete')) + ) + argument = models.TextField() + + class Meta: + app_label = "admin" + + def __str__(self): + if self.type == 'rename': + return 'Rename %s -> %s' % (self.argument, self.mailbox.mail_home) + return 'Delete %s' % self.argument diff --git a/modoboa/admin/postfix_maps.py b/modoboa/admin/postfix_maps.py new file mode 100644 index 000000000..7bfd541bf --- /dev/null +++ b/modoboa/admin/postfix_maps.py @@ -0,0 +1,143 @@ +"""Map file definitions for postfix.""" + +from modoboa.core.commands.postfix_maps import registry + + +class DomainsMap(object): + + """Map to list all domains.""" + + filename = 'sql-domains.cf' + mysql = ( + "SELECT name FROM admin_domain " + "WHERE name='%s' AND type='domain' AND enabled=1" + ) + postgres = ( + "SELECT name FROM admin_domain " + "WHERE name='%s' AND type='domain' AND enabled" + ) + sqlite = ( + "SELECT name FROM admin_domain " + "WHERE name='%s' AND type='domain' AND enabled=1" + ) + + +class DomainsAliasesMap(object): + + """Map to list all domain aliases.""" + + filename = 'sql-domain-aliases.cf' + mysql = ( + "SELECT dom.name FROM admin_domain dom " + "INNER JOIN admin_domainalias domal ON dom.id=domal.target_id " + "WHERE domal.name='%s' AND domal.enabled=1 AND dom.enabled=1" + ) + postgres = ( + "SELECT dom.name FROM admin_domain dom " + "INNER JOIN admin_domainalias domal ON dom.id=domal.target_id " + "WHERE domal.name='%s' AND domal.enabled AND dom.enabled" + ) + sqlite = ( + "SELECT dom.name FROM admin_domain dom " + "INNER JOIN admin_domainalias domal ON dom.id=domal.target_id " + "WHERE domal.name='%s' AND domal.enabled=1 AND dom.enabled=1" + ) + + +class AliasesMap(object): + + """A map to list all mailbox aliases.""" + + filename = 'sql-aliases.cf' + mysql = ( + "SELECT alr.address FROM admin_aliasrecipient AS alr " + "INNER JOIN admin_alias AS al ON alr.alias_id=al.id " + "WHERE al.enabled=1 AND al.address='%s'" + ) + postgres = ( + "SELECT alr.address FROM admin_aliasrecipient AS alr " + "INNER JOIN admin_alias AS al ON alr.alias_id=al.id " + "WHERE al.enabled AND al.address='%s'" + ) + sqlite = ( + "SELECT alr.address FROM admin_aliasrecipient AS alr " + "INNER JOIN admin_alias AS al ON alr.alias_id=al.id " + "WHERE al.enabled=1 AND al.address='%s'" + ) + + +class MaintainMap(object): + + """Map files to list non available mailboxes.""" + + filename = 'sql-maintain.cf' + mysql = ( + "SELECT '450 Requested mail action not taken: mailbox unavailable' " + "FROM admin_mailbox mb INNER JOIN admin_domain dom " + "ON mb.domain_id=dom.id INNER JOIN admin_mailboxoperation mbop " + "ON mbop.mailbox_id=mb.id WHERE dom.name='%d' AND mb.address='%u' " + "LIMIT 1" + ) + postgres = ( + "SELECT '450 Requested mail action not taken: mailbox unavailable' " + "FROM admin_mailbox mb INNER JOIN admin_domain dom " + "ON mb.domain_id=dom.id INNER JOIN admin_mailboxoperation mbop " + "ON mbop.mailbox_id=mb.id WHERE dom.name='%d' AND mb.address='%u' " + "LIMIT 1" + ) + sqlite = ( + "SELECT '450 Requested mail action not taken: mailbox unavailable' " + "FROM admin_mailbox mb INNER JOIN admin_domain dom " + "ON mb.domain_id=dom.id INNER JOIN admin_mailboxoperation mbop " + "ON mbop.mailbox_id=mb.id WHERE dom.name='%d' AND mb.address='%u' " + "LIMIT 1" + ) + + +class SenderLoginMailboxMap(object): + + """Map file to list authorized sender addresses (from mailboxes).""" + + filename = "sql-sender-login-mailboxes.cf" + mysql = ( + "SELECT email FROM core_user WHERE email='%s' AND is_active=1 " + ) + postgres = ( + "SELECT email FROM core_user WHERE email='%s' AND is_active" + ) + sqlite = ( + "SELECT email FROM core_user WHERE email='%s' AND is_active=1" + ) + + +class SenderLoginAliasMap(object): + + """Map file to list authorized sender addresses (from aliases).""" + + filename = "sql-sender-login-aliases.cf" + mysql = ( + "SELECT concat(mb.address, '@', dom.name) FROM admin_mailbox mb " + "INNER JOIN admin_aliasrecipient alr ON alr.r_mailbox_id=mb.id" + " INNER JOIN admin_domain dom ON dom.id=mb.domain_id" + " INNER JOIN admin_alias al ON alr.alias_id=al.id " + "WHERE al.enabled=1 AND al.address='%s'" + ) + postgres = ( + "SELECT mb.address || '@' || dom.name FROM admin_mailbox mb " + "INNER JOIN admin_aliasrecipient alr ON alr.r_mailbox_id=mb.id" + " INNER JOIN admin_domain dom ON dom.id=mb.domain_id" + " INNER JOIN admin_alias al ON alr.alias_id=al.id " + "WHERE al.enabled AND al.address='%s'" + ) + sqlite = ( + "SELECT mb.address || '@' || dom.name FROM admin_mailbox mb " + "INNER JOIN admin_aliasrecipient alr ON alr.r_mailbox_id=mb.id" + " INNER JOIN admin_domain dom ON dom.id=mb.domain_id" + " INNER JOIN admin_alias al ON alr.alias_id=al.id " + "WHERE al.enabled=1 AND al.address='%s'" + ) + +registry.add_files([ + DomainsMap, DomainsAliasesMap, AliasesMap, MaintainMap, + SenderLoginAliasMap, SenderLoginMailboxMap +]) diff --git a/modoboa/admin/signals.py b/modoboa/admin/signals.py new file mode 100644 index 000000000..6878843c4 --- /dev/null +++ b/modoboa/admin/signals.py @@ -0,0 +1,7 @@ +"""Modoboa admin signals.""" + +import django.dispatch + +use_external_recipients = django.dispatch.Signal(providing_args=['recipients']) +extra_domain_actions = django.dispatch.Signal( + providing_args=["user", "domain"]) diff --git a/modoboa/admin/static/admin/css/admin.css b/modoboa/admin/static/admin/css/admin.css new file mode 100644 index 000000000..2768bf843 --- /dev/null +++ b/modoboa/admin/static/admin/css/admin.css @@ -0,0 +1,33 @@ +#searchform { + margin-bottom: 10px; +} + +span.title { + margin-left: 40px; + margin-bottom: 10px; + float: left; + font-weight: bold; + width: 180px; +} + +table th#actions { + text-align: right; +} + +table td[name="actions"] { + text-align: right; +} + +table .progress { + margin-bottom: 0 !important; +} + +table tbody tr:hover { + background-color: #F9F9F9; +} + +.search { + width: 85%; + -webkit-border-radius:14px;-moz-border-radius:14px;border-radius:14px; + padding-left: 14px; +} diff --git a/modoboa/admin/static/admin/css/settings.css b/modoboa/admin/static/admin/css/settings.css new file mode 100644 index 000000000..24fc269b9 --- /dev/null +++ b/modoboa/admin/static/admin/css/settings.css @@ -0,0 +1,9 @@ + +#settings_content .form-horizontal .control-label { + margin-left: 20px; + text-align: left; +} + +#settings_content > .form-horizontal .controls { + margin-left: 240px; +} \ No newline at end of file diff --git a/modoboa/admin/static/admin/js/admin.js b/modoboa/admin/static/admin/js/admin.js new file mode 100644 index 000000000..b35e5caa8 --- /dev/null +++ b/modoboa/admin/static/admin/js/admin.js @@ -0,0 +1,573 @@ +/** + * Creates an instance of Admin. + * + * @constructor + * @param {Object} options - instance options + * @classdesc This class contains utility methods used by the admin + * application. + */ +var Admin = function(options) { + Listing.call(this, options); +}; + +Admin.prototype = { + defaults: { + deflocation: "list/", + squery: null + }, + + initialize: function(options) { + this.options = $.extend({}, this.defaults, options); + this.options.defcallback = $.proxy(this.list_cb, this); + Listing.prototype.initialize.call(this, this.options); + this.listen(); + }, + + /** + * Callback used when the initial content of the listing is + * received. + * + * @this Admin + * @param {Object} data - response of the ajax call (JSON) + */ + list_cb: function(data) { + if (data.rows) { + $("#objects_table tbody").html(data.rows); + } else { + $("#objects_table tbody").html(""); + } + this.update_listing(data); + if (data.length === 0) { + this.get_load_page_args(); + this.end_of_list_reached(); + } + }, + + /** + * Children must override this method. + */ + listen: function() { + }, + + /** + * Load a new page using an AJAX request. + * + * @this Admin + * @param {Object} e - event object + */ + page_loader: function(e) { + var $link = get_target(e); + e.preventDefault(); + if ($link.hasClass("navigation")) { + $(".sidebar li.active").removeClass("active"); + $link.parent().addClass("active"); + } + this.navobj.baseurl($link.attr("href")).update(); + }, + + importform_cb: function() { + $(".submit").one('click', function(e) { + e.preventDefault(); + if ($("#id_sourcefile").val() === "") { + return; + } + $("#import_status").css("display", "block"); + $("#import_result").html("").removeClass("alert alert-danger"); + $("#importform").submit(); + }); + }, + + importdone: function(status, msg) { + $("#import_status").css("display", "none"); + if (status === "ok") { + $("#modalbox").modal('hide'); + this.reload_listing(msg); + } else { + $("#import_result").addClass("alert alert-danger"); + $("#import_result").html(msg); + this.importform_cb(); + } + }, + + exportform_cb: function() { + $(".submit").one('click', function(e) { + e.preventDefault(); + $("#exportform").submit(); + $("#modalbox").modal('hide'); + }); + }, + + reload_listing: function(data) { + this.navobj.update(true); + if (data) { + $("body").notify("success", data, 2000); + } + } +}; + +Admin.prototype = $.extend({}, Listing.prototype, Admin.prototype); + +/** + * Creates an instance of Domains. + * + * @constructor + * @param {Object} options - instance options + * @classdesc This class extends the Admin one by adding methods + * specific to domains management. + */ +var Domains = function(options) { + Admin.call(this, options); +}; + +Domains.prototype = { + initialize: function(options) { + Admin.prototype.initialize.call(this, options); + this.options.navigation_params.push("domfilter", "srvfilter"); + this.options.eor_message = gettext("No more domain to show"); + this.register_tag_handler("dom"); + }, + + /** + * Custom callbacks declaration. + */ + listen: function() { + Admin.prototype.listen.call(this); + $(document).on("click", "a.ajaxnav", $.proxy(this.page_loader, this)); + }, + + /** + * Initialize the domain links embedded within a page. + * + * @param {Object} data - options + */ + init_domain_links: function(data) { + var deloptions = (data.handle_mailboxes === "yes") ? + {keepdir: gettext("Do not delete domain directory")} : {}; + var warnmsg = (data.auto_account_removal && data.auto_account_removal === "yes") + ? gettext("This operation will remove ALL data associated to this domain.") + : gettext("This operation will remove all data associated to this domain, excepting accounts."); + + $("a[name=deldomain]").confirm({ + question: function() { return this.$element.attr('title'); }, + method: "POST", + warning: warnmsg, + checkboxes: deloptions, + success_cb: $.proxy(this.reload_listing, this) + }); + $(document).trigger('domain_listing_refresh'); + }, + + /** + * Navigation callback: default. + * + * @param {Object} data - response of the ajax call (JSON) + */ + list_cb: function(data) { + Admin.prototype.list_cb.call(this, data); + this.init_domain_links(data); + }, + + /** + * A new page has been received, inject it. + * + * @param {Object} data - page content + * @param {string} direction - + */ + add_new_page: function(data, direction) { + Admin.prototype.add_new_page.call(this, data, direction); + this.init_domain_links(data); + }, + + change_inputs_state: function(value) { + $("#id_dom_admin_username").attr("disabled", value); + $("input[name=create_aliases]").attr("disabled", value); + }, + + create_dom_admin_changed: function(e) { + var $target = $(e.target); + this.change_inputs_state(($target.val() === "yes") ? false : true); + }, + + /** + * Initialize the main form contained in the domain edition modal. + * + * @this Domains + */ + generalform_init: function() { + $('input:text:visible:first').focus(); + $("#original_aliases").dynamic_input({ + input_added: function($row) { + $row.find("label").html(""); + }, + input_removed: function($input) { + $input.parents(".form-group").remove(); + return true; + } + }); + }, + + optionsform_init: function() { + $("input[name=create_dom_admin]").click( + $.proxy(this.create_dom_admin_changed, this)); + this.change_inputs_state( + $("input[name=create_dom_admin]:checked").val() === "yes" ? false : true + ); + this.optionsform_prefill(); + }, + + optionsform_prefill: function() { + var $span = $("#id_dom_admin_username").next("span"); + $span.html("@" + $("#id_name").val()); + }, + + domadminsform_init: function() { + $("a[name=removeperm]").click(function(e) { + var $tr = $(this).parents('tr'); + simple_ajax_request.apply(this, [e, { + ok_cb: function(resp) { + $tr.remove(); + if (!$("#domadmins").find("tr").length) { + $("#admins").html('
    ' + + gettext("No domain administrator defined") + "
    "); + } + } + }]); + }); + }, + + newdomain_cb: function() { + this.generalform_init(); + this.optionsform_init(); + $("#wizard").cwizard({ + formid: "domform", + transition_callbacks: { + "options": this.optionsform_prefill + }, + error_callbacks: { + "options": $.proxy(this.optionsform_init, this) + }, + success_callback: $.proxy(this.reload_listing, this) + }); + }, + + domainform_cb: function() { + this.generalform_init(); + this.domadminsform_init(); + $(".submit").one('click', $.proxy(function(e) { + simple_ajax_form_post(e, { + formid: "domform", + error_cb: $.proxy(this.domainform_cb, this), + reload_on_success: false, + success_cb: $.proxy(this.reload_listing, this) + }); + }, this)); + $(document).trigger('domform_init'); + } +}; + +Domains.prototype = $.extend({}, Admin.prototype, Domains.prototype); + +/** + * Creates an instance of Identities. + * + * @constructor + * @param {Object} options - instance options + * @classdesc This class extends the Admin one by adding methods + * specific to identities management. + */ +var Identities = function(options) { + Admin.call(this, options); +}; + +Identities.prototype = { + initialize: function(options) { + Admin.prototype.initialize.call(this, options); + this.options.navigation_params.push("idtfilter", "grpfilter"); + this.options.eor_message = gettext("No more identity to show"); + this.domain_list = []; + this.register_tag_handler("idt"); + this.register_tag_handler("grp", this.grp_tag_handler); + }, + + listen: function() { + Admin.prototype.listen.call(this); + $(document).on("click", "a.ajaxnav", $.proxy(this.page_loader, this)); + $(document).on("click", "a.ajaxcall", $.proxy(this.send_call, this)); + }, + + /** + * Send an ajax call. + */ + send_call: function(evt) { + var $link = get_target(evt, "a"); + var $this = this; + var method = $link.attr("data-call_method"); + + if (method === undefined) { + method = "GET"; + } + evt.preventDefault(); + $.ajax({ + url: $link.attr("href"), + type: method, + dataType: "json" + }).done(function(data) { + $this.reload_listing(data.respmsg); + }); + }, + + /** + * Initialize the links embedded within a page. + * + * @param {Object} data - options + */ + init_identity_links: function(data) { + var deloptions = {}; + + if (data.handle_mailboxes === "yes") { + deloptions = {keepdir: gettext("Do not delete mailbox directory")}; + } + + $("a[name=delaccount]").confirm({ + question: function() { return this.$element.attr('title'); }, + method: "POST", + checkboxes: deloptions, + success_cb: $.proxy(this.reload_listing, this) + }); + $("a[name=delalias]").confirm({ + question: function() { return this.$element.attr('title'); }, + method: "DELETE", + success_cb: $.proxy(this.reload_listing, this) + }); + }, + + /** + * Add a type to let the server know what kind of object we expect. + * + * @this Listing + */ + get_load_page_args: function() { + var args = Admin.prototype.get_load_page_args.call(this); + + if (this.navobj.getbaseurl() === "list") { + args.objtype = "identity"; + this.options.eor_message = gettext("No more identity to show"); + } else { + args.objtype = "quota"; + this.options.eor_message = gettext("No more quota to show"); + } + return args; + }, + + /** + * A new page has been received, inject it. + * + * @param {Object} data - page content + * @param {string} direction - + */ + add_new_page: function(data, direction) { + Admin.prototype.add_new_page.call(this, data, direction); + this.init_identity_links(data); + }, + + /** + * Navigation callback: default. + * + * @param {Object} data - response of the ajax call (JSON) + */ + list_cb: function(data) { + if ($(".sidebar li.active").length === 0) { + var menu = this.navobj.getbaseurl(); + if (menu === "list") { + menu = "identities"; + } + $("a[name={0}]".format(menu)).parent().addClass("active"); + } + $("#objects_table thead tr").html(data.headers); + Admin.prototype.list_cb.call(this, data); + this.init_identity_links(data); + }, + + grp_tag_handler: function(tag, $link) { + if (this.navobj.getparam(tag + "filter") === undefined && + $link.hasClass(tag)) { + var text = $link.attr("name"); + this.navobj + .setparam("idtfilter", "account") + .setparam(tag + "filter", text) + .update(); + if ($("a[name=idt]").length === 0) { + $("#taglist").append(this.make_tag("account", "idt")); + } + $("#taglist").append(this.make_tag(text, tag)); + return true; + } + return false; + }, + + /** + * Retrieve a list of domain from the server. + * + * @this Identities + * @return {Array} a list of domain names + */ + get_domain_list: function() { + if (!this.domain_list.length) { + $.ajax({ + url: this.options.domain_list_url, + dataType: "json", + async: false + }).done($.proxy(function(data) { + this.domain_list = data; + }, this)); + } + return this.domain_list; + }, + + simpleuser_mode: function() { + $("#id_username").autocompleter({ + from_character: "@", + choices: $.proxy(this.get_domain_list, this) + }); + $("#id_email").addClass("disabled") + .attr("readonly", "") + .autocompleter("unbind"); + }, + + normal_mode: function(group) { + $("#id_email").removeClass("disabled") + .attr("readonly", null) + .autocompleter("listen"); + this.toggle_master_user(group); + }, + + /** + * Hide or show the master user checkbox according to desired role. + * + * @param {string} group - The desired role (or group) + */ + toggle_master_user: function(group) { + var $target = $("#id_master_user").parents(".form-group"); + if (group !== "SuperAdmins") { + $target.hide(); + } else { + $target.show(); + } + }, + + generalform_init: function(notrigger) { + $("#id_role").change($.proxy(function(e) { + var $this = $(e.target); + var value = $this.val(); + + if (value == "SimpleUsers" || value === "") { + this.simpleuser_mode(); + } else { + this.normal_mode(value); + } + }, this)); + this.toggle_master_user($("#id_role").val()); + if (notrigger !== undefined && notrigger) { + return; + } + $("#id_role").trigger("change"); + }, + + mailform_init: function() { + $("#id_aliases").autocompleter({ + from_character: "@", + choices: $.proxy(this.get_domain_list, this) + }); + $("#original_aliases").dynamic_input({ + input_added: function($row) { + $row.find("label").html(""); + }, + input_removed: function($input) { + $input.parents(".form-group").remove(); + return true; + } + }); + $("#id_email").autocompleter({ + from_character: "@", + choices: $.proxy(this.get_domain_list, this) + }); + if ($("#id_role").length) { + $("#id_role").trigger("change"); + } else { + this.simpleuser_mode(); + } + $("#id_domains").autocompleter({ + choices: $.proxy(this.get_domain_list, this) + }); + $("#original_domains").dynamic_input({ + input_added: function($row) { + $row.find("label").html(""); + }, + input_removed: function($input) { + $input.parents(".form-group").remove(); + return true; + } + }); + activate_widget.call($("#id_quota_act")); + }, + + accountform_init: function() { + this.generalform_init(true); + this.mailform_init(); + }, + + mailform_prefill: function() { + var $role = $("#id_role"); + if (!$role.length || $role.val() === "" || $role.val() === "SimpleUsers") { + $("#id_email").val($("#id_username").val()); + } + }, + + newaccount_cb: function() { + this.accountform_init(); + $("#wizard").cwizard({ + formid: "newaccount_form", + transition_callbacks: { + "mail": this.mailform_prefill + }, + success_callback: $.proxy(this.reload_listing, this) + }); + }, + + editaccount_cb: function() { + this.accountform_init(); + $('.submit').on('click', $.proxy(function(e) { + simple_ajax_form_post(e, { + formid: "accountform", + reload_on_success: false, + success_cb: $.proxy(this.reload_listing, this) + }); + }, this)); + }, + + aliasform_cb: function() { + $("#id_address").autocompleter({ + from_character: "@", + choices: $.proxy(this.get_domain_list, this) + }); + $("#original_recipients").dynamic_input({ + input_added: function($row) { + $row.find("label").html(""); + }, + input_removed: function($input) { + $input.parents(".form-group").remove(); + return true; + } + }); + $(".submit").on('click', $.proxy(function(e) { + simple_ajax_form_post(e, { + formid: "aliasform", + reload_on_success: false, + success_cb: $.proxy(this.reload_listing, this) + }); + }, this)); + } + +}; + +Identities.prototype = $.extend({}, Admin.prototype, Identities.prototype); diff --git a/modoboa/admin/templates/admin/account_general_form.html b/modoboa/admin/templates/admin/account_general_form.html new file mode 100644 index 000000000..d71149c46 --- /dev/null +++ b/modoboa/admin/templates/admin/account_general_form.html @@ -0,0 +1,14 @@ +{% load form_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.master_user %}{% render_field form.master_user %}{% endif %} +{% if form.domain_is_disabled %} +{% include "common/generic_field.html" with field=form.is_active label_width="col-sm-4" inline_message=_("Domain is disabled") %} +{% else %} +{% render_field form.is_active %} +{% endif %} diff --git a/modoboa/admin/templates/admin/aliasform.html b/modoboa/admin/templates/admin/aliasform.html new file mode 100644 index 000000000..af9b43954 --- /dev/null +++ b/modoboa/admin/templates/admin/aliasform.html @@ -0,0 +1,10 @@ +{% extends "common/generic_modal_form.html" %}{% load form_tags %} +{% block formbody %} +{% render_field form.address %} +{% render_fields_group form "recipients" %} +{% if not form.instance.pk or form.instance.domain.enabled %} +{% render_field form.enabled %} +{% else %} +{% include "common/generic_field.html" with field=form.enabled label_width="col-sm-4" inline_message=_("Domain is disabled") extra_classes="info" %} +{% endif %} +{% endblock %} diff --git a/modoboa/admin/templates/admin/domain_general_form.html b/modoboa/admin/templates/admin/domain_general_form.html new file mode 100644 index 000000000..3657ab58b --- /dev/null +++ b/modoboa/admin/templates/admin/domain_general_form.html @@ -0,0 +1,6 @@ +{% load i18n form_tags %} +{% render_field form.name %} +{% render_field form.type %} +{% render_field form.quota appended_text=_("MB") %} +{% render_fields_group form "aliases" %} +{% render_field form.enabled %} diff --git a/modoboa/admin/templates/admin/domain_options_form.html b/modoboa/admin/templates/admin/domain_options_form.html new file mode 100644 index 000000000..0993c11fa --- /dev/null +++ b/modoboa/admin/templates/admin/domain_options_form.html @@ -0,0 +1,8 @@ +{% load i18n form_tags %} +{% if form.fields %} +{% render_field form.create_dom_admin %} +{% render_field form.dom_admin_username appended_text="@" %} +{% render_field form.create_aliases %} +{% else %} +
    {% trans "You can't create a new domain administrator" %}
    +{% endif %} diff --git a/modoboa/admin/templates/admin/domain_statistics.html b/modoboa/admin/templates/admin/domain_statistics.html new file mode 100644 index 000000000..1123c93bf --- /dev/null +++ b/modoboa/admin/templates/admin/domain_statistics.html @@ -0,0 +1,63 @@ +{% extends "admin/header.html" %} + +{% load i18n %} +{% load admin_tags %} + +{% block leftcol %} + + +{% get_extra_admin_content user "leftcol" selection %} +{% endblock %} + +{% block apparea %} +

    {% trans "Statistics" %} {% trans "Domains and identities" %}

    +
    + +
    + {% if user.group == "SuperAdmins" %} +
    +
    +

    {% trans "Global counters" %}

    +
    + + + + + + + + + + + + + + +
    {% trans "Type" %}{% trans "Quantity" %}
    {% trans "Domains" %}{{ domains_counter }}
    {% trans "Domain aliases" %}{{ domain_aliases_counter }}
    {% trans "Identities" %}{{ identities_counter }}
    +
    +
    + {% endif %} + +
    +
    +

    {% trans "Per-domain counters" %}

    +
    + + + + + + {% for domain in domains %} + + + + {% endfor %} +
    {% trans "Domain" %}{% trans "Identities" %}
    {{ domain.name }}{{ domain.identities_count }}
    +
    +
    + +
    + +{% endblock %} diff --git a/modoboa/admin/templates/admin/domains.html b/modoboa/admin/templates/admin/domains.html new file mode 100644 index 000000000..ec4025f0c --- /dev/null +++ b/modoboa/admin/templates/admin/domains.html @@ -0,0 +1,29 @@ +{% extends "admin/listing.html" %}{% load i18n admin_tags lib_tags %} + +{% block pagetitle %}{% trans "Domains" %}{% endblock %} + +{% block extra_css %} +{{ block.super }} +{% extra_static_content "domains" "css" user %} +{% endblock %} + +{% block extra_js %} +{{ block.super }} + +{% extra_static_content "domains" "js" user %} +{% endblock %} + +{% block actions %}{% domains_menu "domains" user %}{% endblock %} + +{% block table_header %} +{% trans "Name" %} +{% trans "Alias(es)" %} +{% trans "Tags" %} +{% trans "Actions" %} +{% endblock %} diff --git a/modoboa/admin/templates/admin/domains_table.html b/modoboa/admin/templates/admin/domains_table.html new file mode 100644 index 000000000..c79777a8b --- /dev/null +++ b/modoboa/admin/templates/admin/domains_table.html @@ -0,0 +1,9 @@ +{% load i18n admin_tags lib_tags %} +{% for domain in domains %} + + {% domain_modify_link domain %}{% if not domain.enabled %} ({% trans "disabled" %}){% endif %} + {% domain_aliases domain %} + {% render_tags domain.tags %} + {% domain_actions user domain %} + +{% endfor %} diff --git a/modoboa/admin/templates/admin/editdomainform.html b/modoboa/admin/templates/admin/editdomainform.html new file mode 100644 index 000000000..09d19065f --- /dev/null +++ b/modoboa/admin/templates/admin/editdomainform.html @@ -0,0 +1,28 @@ +{% extends "common/tabforms.html" %}{% load i18n lib_tags admin_tags %} +{% block extratabs %} +
  • + {% trans "Administrators" %} +
  • +
  • + {% trans "Miscellaneous" %} +
  • +{% endblock %} +{% block extratabs_content %} +
    {% if domadmins %} + + + {% for da in domadmins %} + + + + {% endfor %} +
    {% trans "Username" %}{% trans "Name" %}
    {{ da.username }}{{ da.fullname }}{% if perms.admin.change_domain %}{% domadmin_actions da.id domain.id %}{% endif %}
    {% else %}
    {% trans "No domain administrator defined" %}
    {% endif %} +
    +
    +
    {% trans "Creation date" %}{{ domain.creation }}
    +
    {% trans "Last modification date" %}{{ domain.last_modification }}
    +
    {% trans "Domain aliases" %}{{ domain.domainalias_count }}
    +
    {% trans "Mailboxes" %}{{ domain.mailbox_count }}
    +
    {% trans "Mailbox aliases" %}{{ domain.mbalias_count }}
    +
    +{% endblock %} diff --git a/modoboa/admin/templates/admin/forward.html b/modoboa/admin/templates/admin/forward.html new file mode 100644 index 000000000..3146beea6 --- /dev/null +++ b/modoboa/admin/templates/admin/forward.html @@ -0,0 +1,11 @@ +{% load i18n form_tags %}{% load url from future %} +

    {% trans "Forward" %} {% trans "Automatically forward emails to another address" %}

    +
    +
    + {% csrf_token %} + {% render_form form %} +
    + +
    +
    diff --git a/modoboa/admin/templates/admin/header.html b/modoboa/admin/templates/admin/header.html new file mode 100644 index 000000000..1dacea7a8 --- /dev/null +++ b/modoboa/admin/templates/admin/header.html @@ -0,0 +1,12 @@ +{% extends "twocols.html" %} +{% load static %} + +{% block extra_css %} + +{% endblock %} + +{% block extra_js %} + + + +{% endblock %} diff --git a/modoboa/admin/templates/admin/identities.html b/modoboa/admin/templates/admin/identities.html new file mode 100644 index 000000000..dde5b0e5f --- /dev/null +++ b/modoboa/admin/templates/admin/identities.html @@ -0,0 +1,26 @@ +{% extends "admin/listing.html" %} +{% load admin_tags lib_tags i18n %}{% load url from future %} +{% load static %} + +{% block pagetitle %}{% trans "Identities" %}{% endblock %} + +{% block extra_css %} +{{ block.super }} +{% extra_static_content "identities" "css" user %} +{% endblock %} + +{% block extra_js %} +{{ block.super }} + + +{% extra_static_content "identities" "js" user %} +{% endblock %} + +{% block actions %}{% identities_menu user "identities" %}{% endblock %} diff --git a/modoboa/admin/templates/admin/identities_table.html b/modoboa/admin/templates/admin/identities_table.html new file mode 100644 index 000000000..15fad1e00 --- /dev/null +++ b/modoboa/admin/templates/admin/identities_table.html @@ -0,0 +1,10 @@ +{% load i18n admin_tags lib_tags %} +{% for ident in identities %} + {% check_identity_status ident as identity_is_enabled %} + + {% identity_modify_link ident %}{% if not identity_is_enabled %} (disabled){% endif %} + {{ ident.name_or_rcpt }} + {% render_tags ident.tags %} + {% identity_actions user ident %} + +{% endfor %} diff --git a/modoboa/admin/templates/admin/identity_headers.html b/modoboa/admin/templates/admin/identity_headers.html new file mode 100644 index 000000000..53c18e745 --- /dev/null +++ b/modoboa/admin/templates/admin/identity_headers.html @@ -0,0 +1,5 @@ +{% load i18n %} +{% trans "Name" %} +{% trans "Fullname/Recipient" %} +{% trans "Tags" %} +{% trans "Actions" %} diff --git a/modoboa/admin/templates/admin/import_done.html b/modoboa/admin/templates/admin/import_done.html new file mode 100644 index 000000000..55a3191b0 --- /dev/null +++ b/modoboa/admin/templates/admin/import_done.html @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/modoboa/admin/templates/admin/importform.html b/modoboa/admin/templates/admin/importform.html new file mode 100644 index 000000000..8e15acf69 --- /dev/null +++ b/modoboa/admin/templates/admin/importform.html @@ -0,0 +1,16 @@ +{% extends "common/generic_modal_form.html" %} +{% load form_tags i18n %} +{% load static %} +{% block formbody %} +
    +

    {% trans "Help" %}

    + {{ helptext|safe }} +
    +{% render_form form %} + + +
    +{% endblock %} diff --git a/modoboa/admin/templates/admin/listing.html b/modoboa/admin/templates/admin/listing.html new file mode 100644 index 000000000..a5c5c7dbb --- /dev/null +++ b/modoboa/admin/templates/admin/listing.html @@ -0,0 +1,47 @@ +{% extends "admin/header.html" %}{% load i18n admin_tags %} +{% load static %} + +{% block extra_css %} +{{ block.super }} + +{% endblock %} + +{% block extra_js %} +{{ block.super }} + + + + + +{% endblock %} + +{% block leftcol %} + + +{% get_extra_admin_content user "leftcol" selection %} +{% endblock %} + +{% block apparea %} +
    +
    + {% include "admin/searchform.html" %} +
    +
    +
    + +
    + + + + {% block table_header %}{% endblock %} + + + + +
    +
    +{% endblock %} diff --git a/modoboa/admin/templates/admin/mailboxes_raw.html b/modoboa/admin/templates/admin/mailboxes_raw.html new file mode 100644 index 000000000..9723f4a20 --- /dev/null +++ b/modoboa/admin/templates/admin/mailboxes_raw.html @@ -0,0 +1,11 @@ +{% load i18n %} +{% if target == "permissions" %} + +{% endif %} +{% for mb in mailboxes %} +{% if target == "permissions" %} + +{% else %} + +{% endif %} +{% endfor %} diff --git a/modoboa/admin/templates/admin/mailform.html b/modoboa/admin/templates/admin/mailform.html new file mode 100644 index 000000000..4b7b6ec63 --- /dev/null +++ b/modoboa/admin/templates/admin/mailform.html @@ -0,0 +1,5 @@ +{% load form_tags %} +{% render_field form.email %} +{% include "common/generic_field.html" with field=form.quota help_display_mode="tooltip" label_width="col-sm-4" appended_text=_("MB") activator=True activator_value=form.quota_act.value deactivate_if_empty=False activator_label=_("Use domain default value") activator_size="col-sm-5" %} +{% render_extra_fields form %} +{% render_fields_group form "aliases" %} diff --git a/modoboa/admin/templates/admin/newdomainform.html b/modoboa/admin/templates/admin/newdomainform.html new file mode 100644 index 000000000..8b4d2ba29 --- /dev/null +++ b/modoboa/admin/templates/admin/newdomainform.html @@ -0,0 +1,6 @@ +{% extends "common/generic_modal_form.html" %}{% load form_tags %}{% block formbody %} +{% render_field form.name %} +{% render_field form.quota appended_text=_("MB") %} +{% render_fields_group form "aliases" %} +{% render_field form.enabled %} +{% endblock %} diff --git a/modoboa/admin/templates/admin/permsform.html b/modoboa/admin/templates/admin/permsform.html new file mode 100644 index 000000000..b1856a04e --- /dev/null +++ b/modoboa/admin/templates/admin/permsform.html @@ -0,0 +1,2 @@ +{% load form_tags %} +{% render_fields_group form "domains" %} diff --git a/modoboa/admin/templates/admin/quota_headers.html b/modoboa/admin/templates/admin/quota_headers.html new file mode 100644 index 000000000..e496973e0 --- /dev/null +++ b/modoboa/admin/templates/admin/quota_headers.html @@ -0,0 +1,5 @@ +{% load i18n %} +{% trans "Address" %} +{% trans "Value" %} +{% trans "Limit" %} +{% trans "Usage (%)" %} diff --git a/modoboa/admin/templates/admin/quotas.html b/modoboa/admin/templates/admin/quotas.html new file mode 100644 index 000000000..266474b21 --- /dev/null +++ b/modoboa/admin/templates/admin/quotas.html @@ -0,0 +1,15 @@ +{% load lib_tags admin_tags %} + +{% for mbox in mboxes %} + + {% identity_modify_link mbox.user "mail" %} + {{ mbox.get_quota }}M + {{ mbox.quota }}M + +
    +
    +
    + + +{% endfor %} diff --git a/modoboa/admin/templates/admin/searchform.html b/modoboa/admin/templates/admin/searchform.html new file mode 100644 index 000000000..bac694e17 --- /dev/null +++ b/modoboa/admin/templates/admin/searchform.html @@ -0,0 +1,11 @@ +{% load i18n %} +
    +
    +
    + + +
    +
    +
    diff --git a/modoboa/admin/templatetags/__init__.py b/modoboa/admin/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/admin/templatetags/admin_tags.py b/modoboa/admin/templatetags/admin_tags.py new file mode 100644 index 000000000..6503d8f9f --- /dev/null +++ b/modoboa/admin/templatetags/admin_tags.py @@ -0,0 +1,286 @@ +""" +Admin extension tags. +""" + +from django import template +from django.core.urlresolvers import reverse +from django.template.loader import render_to_string +from django.utils.translation import ugettext as _, ugettext_lazy + +from modoboa.lib import events +from modoboa.lib.templatetags.lib_tags import render_link +from modoboa.lib.web_utils import render_actions + +from .. import signals + +register = template.Library() + +genders = { + "Enabled": (ugettext_lazy("enabled_m"), ugettext_lazy("enabled_f")) +} + + +@register.simple_tag +def domains_menu(selection, user, ajax_mode=True): + """Specific menu for domain related operations. + + Corresponds to the menu visible on the left column when you go to + *Domains*. + + :param str selection: menu entry currently selected + :param ``User`` user: connected user + :rtype: str + :return: rendered menu (as HTML) + """ + domain_list_url = ( + "list/" if ajax_mode else reverse("admin:domain_list") + ) + entries = [ + {"name": "domains", + "label": _("List domains"), + "img": "fa fa-user", + "class": "ajaxnav navigation", + "url": domain_list_url}, + {"name": "statistics", + "label": _("Statistics"), + "img": "fa fa-line-chart", + "class": "navigation", + "url": reverse("admin:domain_statistics")} + ] + if user.has_perm("admin.add_domain"): + entries += [ + {"name": "newdomain", + "label": _("Add domain"), + "img": "fa fa-plus", + "modal": True, + "modalcb": "admin.newdomain_cb", + "url": reverse("admin:domain_add")}, + ] + entries += events.raiseQueryEvent("ExtraDomainMenuEntries", user) + entries += [ + {"name": "import", + "label": _("Import"), + "img": "fa fa-folder-open", + "url": reverse("admin:domain_import"), + "modal": True, + "modalcb": "admin.importform_cb"}, + {"name": "export", + "label": _("Export"), + "img": "fa fa-share-alt", + "url": reverse("admin:domain_export"), + "modal": True, + "modalcb": "admin.exportform_cb"} + ] + + return render_to_string('common/menulist.html', { + "entries": entries, + "selection": selection, + "user": user + }) + + +@register.simple_tag +def identities_menu(user, selection=None): + """Menu specific to the Identities page. + + :param ``User`` user: the connecter user + :rtype: str + :return: the rendered menu + """ + entries = [ + {"name": "identities", + "label": _("List identities"), + "img": "fa fa-user", + "class": "ajaxnav navigation", + "url": "list/"}, + {"name": "quotas", + "label": _("List quotas"), + "img": "fa fa-hdd-o", + "class": "ajaxnav navigation", + "url": "quotas/"}, + {"name": "newaccount", + "label": _("Add account"), + "img": "fa fa-plus", + "modal": True, + "modalcb": "admin.newaccount_cb", + "url": reverse("admin:account_add")}, + {"name": "newalias", + "label": _("Add alias"), + "img": "fa fa-plus", + "modal": True, + "modalcb": "admin.aliasform_cb", + "url": reverse("admin:alias_add")}, + {"name": "newforward", + "label": _("Add forward"), + "img": "fa fa-plus", + "modal": True, + "modalcb": "admin.aliasform_cb", + "url": reverse("admin:forward_add")}, + {"name": "newdlist", + "label": _("Add distribution list"), + "img": "fa fa-plus", + "modal": True, + "modalcb": "admin.aliasform_cb", + "url": reverse("admin:dlist_add")}, + {"name": "import", + "label": _("Import"), + "img": "fa fa-folder-open", + "url": reverse("admin:identity_import"), + "modal": True, + "modalcb": "admin.importform_cb"}, + {"name": "export", + "label": _("Export"), + "img": "fa fa-share-alt", + "url": reverse("admin:identity_export"), + "modal": True, + "modalcb": "admin.exportform_cb" + } + ] + + return render_to_string('common/menulist.html', { + "entries": entries, + "user": user + }) + + +@register.simple_tag +def domain_actions(user, domain): + actions = [ + {"name": "listidentities", + "url": "{0}#list/?searchquery=@{1}".format( + reverse("admin:identity_list"), domain.name), + "title": _("View the domain's identities"), + "img": "fa fa-user"} + ] + if user.has_perm("admin.delete_domain"): + actions.append({ + "name": "deldomain", + "url": reverse("admin:domain_delete", args=[domain.id]), + "title": _("Delete %s?" % domain.name), + "img": "fa fa-trash" + }) + + responses = signals.extra_domain_actions.send( + sender=None, user=user, domain=domain) + for receiver, response in responses: + if response: + actions += response + + return render_actions(actions) + + +@register.simple_tag +def identity_actions(user, ident): + name = ident.__class__.__name__ + objid = ident.id + if name == "User": + actions = events.raiseQueryEvent("ExtraAccountActions", ident) + actions += [ + {"name": "delaccount", + "url": reverse("admin:account_delete", args=[objid]), + "img": "fa fa-trash", + "title": _("Delete %s?" % ident.username)}, + ] + else: + actions = [ + {"name": "delalias", + "url": "{}?selection={}".format( + reverse("admin:alias_delete"), objid), + "img": "fa fa-trash", + "title": _("Delete %s?" % ident.address)}, + ] + return render_actions(actions) + + +@register.assignment_tag +def check_identity_status(identity): + """Check if identity is enabled or not.""" + if identity.__class__.__name__ == "User": + if hasattr(identity, "mailbox") \ + and not identity.mailbox.domain.enabled: + return False + elif not identity.is_active: + return False + elif not identity.enabled or not identity.domain.enabled: + return False + return True + + +@register.simple_tag +def domain_modify_link(domain): + linkdef = {"label": domain.name, "modal": True} + if domain.__class__.__name__ == "Domain": + linkdef["url"] = reverse( + "admin:domain_change", args=[domain.id]) + linkdef["modalcb"] = "admin.domainform_cb" + else: + tmp = events.raiseDictEvent('GetDomainModifyLink', domain) + for key in ['url', 'modalcb']: + linkdef[key] = tmp[key] + return render_link(linkdef) + + +@register.simple_tag +def domain_aliases(domain): + """Display domain aliases of this domain. + + :param domain: + :rtype: str + """ + if not domain.aliases.count(): + return '---' + res = '' + for alias in domain.aliases.all(): + res += '%s
    ' % alias.name + return res + + +@register.simple_tag +def identity_modify_link(identity, active_tab='default'): + """Return the appropriate modification link. + + According to the identity type, a specific modification link (URL) + must be used. + + :param identity: a ``User`` or ``Alias`` instance + :param str active_tab: the tab to display + :rtype: str + """ + linkdef = {"label": identity.identity, "modal": True} + if identity.__class__.__name__ == "User": + linkdef["url"] = reverse("admin:account_change", args=[identity.id]) + linkdef["url"] += "?active_tab=%s" % active_tab + linkdef["modalcb"] = "admin.editaccount_cb" + else: + linkdef["url"] = reverse("admin:alias_change", args=[identity.id]) + linkdef["modalcb"] = "admin.aliasform_cb" + return render_link(linkdef) + + +@register.simple_tag +def domadmin_actions(daid, domid): + actions = [{ + "name": "removeperm", + "url": "{0}?domid={1}&daid={2}".format( + reverse("admin:permission_remove"), domid, daid), + "img": "fa fa-trash", + "title": _("Remove this permission") + }] + return render_actions(actions) + + +@register.filter +def gender(value, target): + if value in genders: + trans = target == "m" and genders[value][0] or genders[value][1] + if trans.find("_") == -1: + return trans + return value + + +@register.simple_tag +def get_extra_admin_content(user, target, currentpage): + res = events.raiseQueryEvent( + "ExtraAdminContent", user, target, currentpage + ) + return "".join(res) diff --git a/modoboa/admin/tests/__init__.py b/modoboa/admin/tests/__init__.py new file mode 100644 index 000000000..bdc06b647 --- /dev/null +++ b/modoboa/admin/tests/__init__.py @@ -0,0 +1,24 @@ +"""modoboa-admin unit tests.""" + +from .account import AccountTestCase, PermissionsTestCase +from .alias import AliasTestCase +from .domain import DomainTestCase +from .domain_alias import DomainAliasTestCase +from .export import ExportTestCase +from .import_ import ImportTestCase +from .mapfiles import MapFilesTestCase +from .password_schemes import PasswordSchemesTestCase +from .user import ForwardTestCase + +__all__ = [ + 'AccountTestCase', + 'AliasTestCase', + 'DomainTestCase', + 'DomainAliasTestCase', + 'ExportTestCase', + 'ForwardTestCase', + 'ImportTestCase', + 'MapFilesTestCase', + 'PasswordSchemesTestCase', + 'PermissionsTestCase', +] diff --git a/modoboa/admin/tests/account.py b/modoboa/admin/tests/account.py new file mode 100644 index 000000000..1b82547e7 --- /dev/null +++ b/modoboa/admin/tests/account.py @@ -0,0 +1,249 @@ +from django.core.urlresolvers import reverse + +from modoboa.core.models import User +from modoboa.lib.tests import ModoTestCase + +from .. import factories +from .. import models + + +class AccountTestCase(ModoTestCase): + + def setUp(self): + super(AccountTestCase, self).setUp() + factories.populate_database() + + def test_crud(self): + values = dict( + username="tester@test.com", first_name="Tester", last_name="Toto", + password1="Toto1234", password2="Toto1234", role="SimpleUsers", + quota_act=True, + is_active=True, email="tester@test.com", stepid='step2' + ) + self.ajax_post(reverse("admin:account_add"), values) + + account = User.objects.get(username="tester@test.com") + mb = account.mailbox + self.assertEqual(mb.full_address, "tester@test.com") + self.assertEqual(mb.quota, 10) + self.assertTrue(mb.enabled) + self.assertEqual(mb.quota_value.username, "tester@test.com") + self.assertEqual(account.username, mb.full_address) + self.assertTrue(account.check_password("Toto1234")) + self.assertEqual(account.first_name, "Tester") + self.assertEqual(account.last_name, "Toto") + self.assertEqual(mb.domain.mailbox_count, 3) + # Check if self alias has been created + self.assertTrue( + models.AliasRecipient.objects.select_related("alias").filter( + alias__address=mb.full_address, address=mb.full_address, + alias__internal=True).exists() + ) + + values["username"] = "pouet@test.com" + self.ajax_post( + reverse("admin:account_change", args=[account.id]), values + ) + mb = models.Mailbox.objects.get(pk=mb.id) + self.assertEqual(mb.full_address, "pouet@test.com") + self.assertEqual(mb.quota_value.username, "pouet@test.com") + # Check if self alias has been updated + self.assertTrue( + models.AliasRecipient.objects.select_related("alias").filter( + alias__address=mb.full_address, address=mb.full_address, + alias__internal=True).exists() + ) + self.ajax_post( + reverse("admin:account_delete", args=[account.id]), {} + ) + # Check if self alias has been deleted + self.assertFalse( + models.AliasRecipient.objects.select_related("alias").filter( + alias__address=mb.full_address, address=mb.full_address, + alias__internal=True).exists() + ) + + def _set_quota(self, email, value, expected_status=200): + account = User.objects.get(username=email) + values = { + "username": email, "role": "SimpleUsers", "quota_act": False, + "is_active": True, "quota": value, "email": email + } + self.ajax_post( + reverse("admin:account_change", args=[account.id]), + values, status=expected_status + ) + + def test_set_nul_quota_as_superadmin(self): + self._set_quota("user@test.com", 0) + + def test_set_nul_quota_as_domainadmin(self): + self.clt.logout() + self.assertTrue( + self.clt.login(username="admin@test.com", password="toto") + ) + self._set_quota("user@test.com", 0, 400) + self.clt.logout() + self.assertTrue( + self.clt.login(username="admin@test2.com", password="toto") + ) + self._set_quota("user@test2.com", 0) + + def test_master_user(self): + """Validate the master user mode.""" + values = { + "username": "masteruser", "role": "SuperAdmins", + "quota_act": False, + "is_active": True, "master_user": True, "stepid": "step2", + "password1": "Toto1234", "password2": "Toto1234" + } + self.ajax_post( + reverse("admin:account_add"), values + ) + self.assertTrue(User.objects.get(username="masteruser").master_user) + + values = { + "username": "testuser", "role": "DomainAdmins", + "quota_act": False, + "is_active": True, "master_user": True, "stepid": "step2", + "password1": "Toto1234", "password2": "Toto1234" + } + self.ajax_post( + reverse("admin:account_add"), values, status=400 + ) + + +class PermissionsTestCase(ModoTestCase): + + def setUp(self): + super(PermissionsTestCase, self).setUp() + factories.populate_database() + self.user = User.objects.get(username='user@test.com') + self.values = dict( + username=self.user.username, role="DomainAdmins", + is_active=self.user.is_active, email="user@test.com", + quota_act=True + ) + + def tearDown(self): + self.clt.logout() + + def test_domain_admins(self): + self.ajax_post( + reverse("admin:account_change", args=[self.user.id]), + self.values + ) + self.assertEqual(self.user.group, "DomainAdmins") + + self.values["role"] = "SimpleUsers" + self.ajax_post( + reverse("admin:account_change", args=[self.user.id]), + self.values + ) + self.assertNotEqual(self.user.group, 'DomainAdmins') + + def test_superusers(self): + self.values["role"] = "SuperAdmins" + self.ajax_post( + reverse("admin:account_change", args=[self.user.id]), + self.values + ) + self.assertEqual( + User.objects.get(username="user@test.com").is_superuser, True + ) + + self.values["role"] = "SimpleUsers" + self.ajax_post( + reverse("admin:account_change", args=[self.user.id]), + self.values + ) + self.assertEqual(User.objects.get( + username="user@test.com").is_superuser, False) + + def test_self_modif(self): + self.clt.logout() + self.assertTrue( + self.clt.login(username="admin@test.com", password="toto") + ) + admin = User.objects.get(username="admin@test.com") + values = dict( + username="admin@test.com", first_name="Admin", + password1="", password2="", + quota=10, is_active=True, email="admin@test.com" + ) + self.ajax_post( + reverse("admin:account_change", args=[admin.id]), + values + ) + self.assertEqual(admin.group, "DomainAdmins") + self.assertTrue(admin.can_access( + models.Domain.objects.get(name="test.com"))) + + values["role"] = "SuperAdmins" + self.ajax_post( + reverse("admin:account_change", args=[admin.id]), + values + ) + admin = User.objects.get(username="admin@test.com") + self.assertEqual(admin.group, "DomainAdmins") + + def test_domadmin_access(self): + self.clt.logout() + self.assertEqual( + self.clt.login(username="admin@test.com", password="toto"), + True) + response = self.clt.get(reverse("admin:domain_list")) + self.assertEqual(response.status_code, 200) + + response = self.clt.get( + reverse("admin:account_change", args=[self.user.id]), + HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertNotEqual(response["Content-Type"], "application/json") + + def test_domainadmin_deletes_superadmin(self): + """Check domain admins restrictions about super admins + + When a super admin owns a mailbox and a domain admin exists + for the associated domain, this domain admin must not be able + to access the super admin. + """ + values = dict( + username="superadmin2@test.com", first_name="Super", + last_name="Admin", password1="Toto1234", password2="Toto1234", + role="SuperAdmins", is_active=True, + email="superadmin2@test.com", stepid='step2' + ) + self.ajax_post( + reverse("admin:account_add"), + values + ) + account = User.objects.get(username="superadmin2@test.com") + self.clt.logout() + self.clt.login(username="admin@test.com", password="toto") + self.ajax_post( + reverse("admin:account_delete", args=[account.id]), {}, 403 + ) + + def test_domainadmin_dlist_local_domain_not_owned(self): + """Check if a domain admin can use a local mailbox he can't + access as a recipient in a distribution list""" + values = dict( + address="all@test.com", + recipients="user@test.com", + recipients_1="user@test2.com", + enabled=True + ) + self.ajax_post(reverse("admin:dlist_add"), values) + + def test_domainadmin_master_user(self): + """Check domain administrator is not allowed to access this feature.""" + values = dict( + username="user10@test.com", first_name="Test", + last_name="Test", password1="Toto1234", password2="Toto1234", + role="SimpleUsers", is_active=True, master_user=True, + email="user10@test.com", stepid='step2' + ) + self.ajax_post( + reverse("admin:account_add"), + values, status=400 + ) diff --git a/modoboa/admin/tests/alias.py b/modoboa/admin/tests/alias.py new file mode 100644 index 000000000..d6aab928f --- /dev/null +++ b/modoboa/admin/tests/alias.py @@ -0,0 +1,202 @@ +# coding: utf-8 + +"""Admin test cases.""" + +from django.core.urlresolvers import reverse + +from modoboa.core.models import User +from modoboa.lib.tests import ModoTestCase + +from .. import factories +from ..models import Alias, AliasRecipient + + +class AliasTestCase(ModoTestCase): + + def setUp(self): + super(AliasTestCase, self).setUp() + factories.populate_database() + + def test_alias(self): + user = User.objects.get(username="user@test.com") + values = dict( + username="user@test.com", role=user.group, + is_active=user.is_active, email="user@test.com", + aliases="toto@test.com", aliases_1="titi@test.com" + ) + self.ajax_post( + reverse("admin:account_change", args=[user.id]), + values + ) + self.assertEqual( + AliasRecipient.objects.filter + (alias__internal=False, r_mailbox=user.mailbox) + .count(), 2 + ) + del values["aliases_1"] + self.ajax_post( + reverse("admin:account_change", args=[user.id]), + values + ) + self.assertEqual( + AliasRecipient.objects.filter( + alias__internal=False, r_mailbox=user.mailbox) + .count(), 1 + ) + + def test_alias_with_duplicated_recipient(self): + """Check for duplicates.""" + values = { + "address": "badalias@test.com", + "recipients": "user@test.com", + "recipients_1": "user@test.com", + "enabled": True + } + self.ajax_post( + reverse("admin:dlist_add"), values + ) + alias = Alias.objects.get(address="badalias@test.com") + self.assertEqual(alias.recipients_count, 1) + + def test_upper_case_alias(self): + """Try to create an upper case alias.""" + user = User.objects.get(username="user@test.com") + values = dict( + username="user@test.com", role=user.group, + is_active=user.is_active, email="user@test.com", + aliases="Toto@test.com" + ) + self.ajax_post( + reverse("admin:account_change", args=[user.id]), + values + ) + mb = user.mailbox + self.assertEqual( + AliasRecipient.objects + .filter(alias__internal=False, r_mailbox=mb).count(), 1) + self.assertEqual( + AliasRecipient.objects.get( + alias__internal=False, r_mailbox=mb).alias.address, + "toto@test.com" + ) + + values = { + "address": "Titi@test.com", "recipients": "user@test.com", + "enabled": True + } + self.ajax_post(reverse("admin:alias_add"), values) + self.assertTrue( + Alias.objects.filter(address="titi@test.com").exists()) + + def test_append_alias_with_tag(self): + """Try to create a alias with tag in recipient address""" + user = User.objects.get(username="user@test.com") + values = dict( + username="user@test.com", role=user.group, + is_active=user.is_active, email="user@test.com" + ) + self.ajax_post( + reverse("admin:account_change", args=[user.id]), + values + ) + + values = { + "address": "foobar@test.com", "recipients": "user+spam@test.com", + "enabled": True + } + self.ajax_post(reverse("admin:alias_add"), values) + alias = Alias.objects.get(address="foobar@test.com") + self.assertTrue( + alias.aliasrecipient_set.filter( + address="user+spam@test.com", r_mailbox__isnull=False, + r_alias__isnull=True).exists() + ) + + def test_dlist(self): + values = dict(address="all@test.com", + recipients="user@test.com", + recipients_1="admin@test.com", + recipients_2="ext@titi.com", + enabled=True) + self.ajax_post( + reverse("admin:dlist_add"), values + ) + user = User.objects.get(username="user@test.com") + mb = user.mailbox + self.assertEqual( + AliasRecipient.objects.filter( + alias__internal=False, r_mailbox=mb).count(), 2) + admin = User.objects.get(username="admin@test.com") + mb = admin.mailbox + self.assertEqual( + AliasRecipient.objects.filter( + alias__internal=False, r_mailbox=mb).count(), 1) + dlist = Alias.objects.get(address="all@test.com") + self.assertEqual(dlist.recipients_count, 3) + del values["recipients_1"] + self.ajax_post( + reverse("admin:alias_change", args=[dlist.id]), + values + ) + self.assertEqual(dlist.recipients_count, 2) + + self.ajax_post( + "{}?selection={}".format( + reverse("admin:alias_delete"), dlist.id), + {} + ) + self.assertRaises( + Alias.DoesNotExist, Alias.objects.get, address="all@test.com") + + def test_forward(self): + values = dict(address="forward2@test.com", recipients="rcpt@dest.com") + self.ajax_post( + reverse("admin:forward_add"), values + ) + fwd = Alias.objects.get(address="forward2@test.com") + self.assertEqual(fwd.recipients_count, 1) + + values["recipients"] = "rcpt2@dest.com" + self.ajax_post( + reverse("admin:alias_change", + args=[fwd.id]), + values + ) + self.assertEqual(fwd.recipients_count, 1) + + self.ajax_post( + reverse("admin:alias_delete") + "?selection=%d" + % fwd.id, {} + ) + self.assertRaises( + Alias.DoesNotExist, Alias.objects.get, address="forward2@test.com") + + def test_forward_and_local_copies(self): + values = dict(address="user@test.com", recipients="rcpt@dest.com") + self.ajax_post( + reverse("admin:forward_add"), values + ) + fwd = Alias.objects.get(address="user@test.com", internal=False) + self.assertEqual(fwd.recipients_count, 1) + + values["recipients"] = "rcpt@dest.com" + values["recipients_1"] = "user@test.com" + self.ajax_post( + reverse("admin:alias_change", args=[fwd.id]), + values + ) + fwd = Alias.objects.get(pk=fwd.pk) + self.assertEqual(fwd.aliasrecipient_set.count(), 2) + self.assertEqual( + fwd.aliasrecipient_set.filter(r_alias__isnull=True).count(), 2) + + def test_wildcard_alias(self): + """Test creation of a wildcard alias.""" + values = { + "address": "@test.com", + "recipients": "user@test.com", + "enabled": True + } + self.ajax_post( + reverse("admin:dlist_add"), values + ) diff --git a/modoboa/admin/tests/domain.py b/modoboa/admin/tests/domain.py new file mode 100644 index 000000000..d175e61ba --- /dev/null +++ b/modoboa/admin/tests/domain.py @@ -0,0 +1,131 @@ +"""Domain related test cases.""" + +import json + +from django.core.urlresolvers import reverse + +from modoboa.core.models import User +from modoboa.lib import parameters +from modoboa.lib.tests import ModoTestCase + +from .. import factories +from ..models import Domain, Alias + + +class DomainTestCase(ModoTestCase): + + """Test case for Domain.""" + + def setUp(self): + super(DomainTestCase, self).setUp() + factories.populate_database() + + def test_create(self): + """Test the creation of a domain.""" + values = { + "name": "pouet.com", "quota": 100, "create_dom_admin": "no", + "type": "domain", "stepid": "step3" + } + self.ajax_post(reverse("admin:domain_add"), values) + dom = Domain.objects.get(name="pouet.com") + self.assertEqual(dom.name, "pouet.com") + self.assertEqual(dom.quota, 100) + self.assertEqual(dom.enabled, False) + self.assertFalse(dom.admins) + + def test_create_with_template(self): + """Test the creation of a domain with a template + + """ + values = { + "name": "pouet.com", "quota": 100, "create_dom_admin": "yes", + "dom_admin_username": "toto", "create_aliases": "yes", + "type": "domain", "stepid": 'step3' + } + self.ajax_post( + reverse("admin:domain_add"), + values + ) + dom = Domain.objects.get(name="pouet.com") + da = User.objects.get(username="toto@pouet.com") + self.assertIn(da, dom.admins) + al = Alias.objects.get(address="postmaster@pouet.com") + self.assertTrue( + al.aliasrecipient_set.filter(r_mailbox=da.mailbox) + .exists() + ) + self.assertTrue(da.can_access(al)) + + def test_create_with_template_and_empty_quota(self): + """Test the creation of a domain with a template and no quota""" + values = { + "name": "pouet.com", "quota": 0, "create_dom_admin": "yes", + "dom_admin_username": "toto", "create_aliases": "yes", + "type": "domain", "stepid": 'step3' + } + self.ajax_post( + reverse("admin:domain_add"), + values + ) + dom = Domain.objects.get(name="pouet.com") + da = User.objects.get(username="toto@pouet.com") + self.assertIn(da, dom.admins) + al = Alias.objects.get(address="postmaster@pouet.com") + self.assertTrue( + al.aliasrecipient_set.filter(r_mailbox=da.mailbox) + .exists() + ) + self.assertTrue(da.can_access(al)) + + def test_create_using_default_quota(self): + parameters.save_admin('DEFAULT_DOMAIN_QUOTA', 50, app='admin') + values = { + "name": "pouet.com", "create_dom_admin": "yes", + "dom_admin_username": "toto", "create_aliases": "yes", + "type": "domain", "stepid": 'step3' + } + self.ajax_post( + reverse("admin:domain_add"), + values + ) + dom = Domain.objects.get(name="pouet.com") + self.assertEqual(dom.quota, 50) + da = User.objects.get(username="toto@pouet.com") + self.assertEqual(da.mailbox.quota, 50) + + def test_modify(self): + """Test the modification of a domain + + Rename 'test.com' domain to 'pouet.com' + """ + values = { + "name": "pouet.com", "quota": 100, "type": "domain", + "enabled": True + } + dom = Domain.objects.get(name="test.com") + self.ajax_post( + reverse("admin:domain_change", args=[dom.id]), + values + ) + dom = Domain.objects.get(name="pouet.com") + self.assertEqual(dom.name, "pouet.com") + self.assertTrue(dom.enabled) + + def test_delete(self): + """Test the removal of a domain + """ + dom = Domain.objects.get(name="test.com") + self.ajax_post( + reverse("admin:domain_delete", args=[dom.id]), + {} + ) + with self.assertRaises(Domain.DoesNotExist): + Domain.objects.get(pk=1) + + def test_domain_flat_list(self): + """Test the 'domain_flat_list' view.""" + response = self.clt.get(reverse("admin:domain_flat_list")) + self.assertEqual(response.status_code, 200) + content = json.loads(response.content) + self.assertIn("test.com", content) + self.assertIn("test2.com", content) diff --git a/modoboa/admin/tests/domain_alias.py b/modoboa/admin/tests/domain_alias.py new file mode 100644 index 000000000..f99fa32c8 --- /dev/null +++ b/modoboa/admin/tests/domain_alias.py @@ -0,0 +1,55 @@ +from django.core.urlresolvers import reverse + +from modoboa.lib.tests import ModoTestCase + +from .. import factories +from ..models import Domain, DomainAlias, Alias + + +class DomainAliasTestCase(ModoTestCase): + + def setUp(self): + super(DomainAliasTestCase, self).setUp() + factories.populate_database() + self.dom = Domain.objects.get(name='test.com') + + def test_model(self): + dom = Domain.objects.get(name="test.com") + domal = DomainAlias() + domal.name = "domalias.net" + domal.target = dom + domal.save() + self.assertEqual(dom.domainalias_count, 1) + self.assertTrue( + Alias.objects.filter( + address="@{}".format(domal.name)).exists()) + + domal.name = "domalias.org" + domal.save() + + domal.delete() + self.assertFalse( + Alias.objects.filter( + address="@{}".format(domal.name)).exists()) + + def test_form(self): + dom = Domain.objects.get(name="test.com") + values = dict( + name=dom.name, quota=dom.quota, enabled=dom.enabled, + aliases="domalias.net", aliases_1="domalias.com", + type="domain" + ) + self.ajax_post( + reverse("admin:domain_change", + args=[dom.id]), + values + ) + self.assertEqual(dom.domainalias_set.count(), 2) + + del values["aliases_1"] + self.ajax_post( + reverse("admin:domain_change", + args=[dom.id]), + values + ) + self.assertEqual(dom.domainalias_set.count(), 1) diff --git a/modoboa/admin/tests/export.py b/modoboa/admin/tests/export.py new file mode 100644 index 000000000..f6d35d521 --- /dev/null +++ b/modoboa/admin/tests/export.py @@ -0,0 +1,94 @@ +"""Export related test cases.""" + +from django.core.urlresolvers import reverse + +from modoboa.lib.tests import ModoTestCase + +from .. import factories + + +class ExportTestCase(ModoTestCase): + + """Test case for export operations.""" + + def setUp(self): + super(ExportTestCase, self).setUp() + factories.populate_database() + + def __export_identities(self, idtfilter="", grpfilter=""): + self.clt.get( + reverse("admin:_identity_list") + + "?grpfilter=%s&idtfilter=%s" % (grpfilter, idtfilter) + ) + return self.clt.post( + reverse("admin:identity_export"), + {"filename": "test.csv"} + ) + + def assertListEqual(self, list1, list2): + list2 = list2.split('\r\n') + for entry in list1.split('\r\n'): + if not entry: + continue + self.assertIn(entry, list2) + + def test_export_identities(self): + response = self.__export_identities() + self.assertListEqual( + "account;admin@test.com;{PLAIN}toto;;;True;DomainAdmins;admin@test.com;10;test.com\r\naccount;admin@test2.com;{PLAIN}toto;;;True;DomainAdmins;admin@test2.com;10;test2.com\r\naccount;user@test.com;{PLAIN}toto;;;True;SimpleUsers;user@test.com;10\r\naccount;user@test2.com;{PLAIN}toto;;;True;SimpleUsers;user@test2.com;10\r\nalias;alias@test.com;True;user@test.com\r\nforward;forward@test.com;True;user@external.com\r\ndlist;postmaster@test.com;True;toto@titi.com;test@truc.fr\r\n", + response.content.strip() + ) + + def test_export_simpleusers(self): + response = self.__export_identities( + idtfilter="account", grpfilter="SimpleUsers" + ) + self.assertListEqual( + "account;user@test.com;{PLAIN}toto;;;True;SimpleUsers;user@test.com;10\r\naccount;user@test2.com;{PLAIN}toto;;;True;SimpleUsers;user@test2.com;10", + response.content.strip() + ) + + def test_export_superadmins(self): + """A test to validate we only export 1 super admin. + + The password is removed since it is hashed using SHA512-CRYPT. + """ + response = self.__export_identities( + idtfilter="account", grpfilter="SuperAdmins" + ) + elements = response.content.strip().split(";") + self.assertEqual(len(elements), 9) + elements[2] = "" + self.assertEqual( + ";".join(elements), "account;admin;;;;True;SuperAdmins;;" + ) + + def test_export_domainadmins(self): + response = self.__export_identities( + idtfilter="account", grpfilter="DomainAdmins" + ) + self.assertListEqual( + "account;admin@test.com;{PLAIN}toto;;;True;DomainAdmins;admin@test.com;10;test.com\r\naccount;admin@test2.com;{PLAIN}toto;;;True;DomainAdmins;admin@test2.com;10;test2.com", + response.content.strip() + ) + + def test_export_aliases(self): + response = self.__export_identities(idtfilter="alias") + self.assertEqual( + response.content.strip(), + "alias;alias@test.com;True;user@test.com" + ) + + def test_export_forwards(self): + response = self.__export_identities(idtfilter="forward") + self.assertEqual( + response.content.strip(), + "forward;forward@test.com;True;user@external.com" + ) + + def test_export_dlists(self): + response = self.__export_identities(idtfilter="dlist") + self.assertEqual( + response.content.strip(), + "dlist;postmaster@test.com;True;toto@titi.com;test@truc.fr" + ) diff --git a/modoboa/admin/tests/import_.py b/modoboa/admin/tests/import_.py new file mode 100644 index 000000000..efbb43650 --- /dev/null +++ b/modoboa/admin/tests/import_.py @@ -0,0 +1,237 @@ +# coding: utf-8 +from django.core.files.base import ContentFile +from django.core.urlresolvers import reverse + +from modoboa.core.models import User +from modoboa.lib.tests import ModoTestCase + +from .. import factories +from ..models import Domain, Alias, DomainAlias + + +class ImportTestCase(ModoTestCase): + + def setUp(self): + super(ImportTestCase, self).setUp() + factories.populate_database() + + def test_domains_import(self): + f = ContentFile(b"""domain; domain1.com; 100; True +domain; domain2.com; 200; False +domainalias; domalias1.com; domain1.com; True +""", name="domains.csv") + self.clt.post( + reverse("admin:domain_import"), { + "sourcefile": f + } + ) + admin = User.objects.get(username="admin") + dom = Domain.objects.get(name="domain1.com") + self.assertEqual(dom.quota, 100) + self.assertTrue(dom.enabled) + self.assertTrue(admin.is_owner(dom)) + domalias = DomainAlias.objects.get(name="domalias1.com") + self.assertEqual(domalias.target, dom) + self.assertTrue(dom.enabled) + self.assertTrue(admin.is_owner(domalias)) + dom = Domain.objects.get(name="domain2.com") + self.assertEqual(dom.quota, 200) + self.assertFalse(dom.enabled) + self.assertTrue(admin.is_owner(dom)) + + def test_import_domains_with_conflict(self): + f = ContentFile(b"""domain;test.alias;10;True +domainalias;test.alias;test.com;True +""", name="domains.csv") + resp = self.clt.post( + reverse("admin:domain_import"), { + "sourcefile": f + } + ) + self.assertIn('Object already exists: domainalias', resp.content) + + def test_identities_import(self): + f = ContentFile(b""" +account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com; 0 +account; truc@test.com; toto; René; Truc; True; DomainAdmins; truc@test.com; 5; test.com +alias; alias1@test.com; True; user1@test.com +forward; alias2@test.com; True; user1+ext@test.com +forward; fwd1@test.com; True; user@extdomain.com +dlist; dlist@test.com; True; user1@test.com; user@extdomain.com +""", name="identities.csv") + self.clt.post( + reverse("admin:identity_import"), + {"sourcefile": f, "crypt_password": True} + ) + admin = User.objects.get(username="admin") + u1 = User.objects.get(username="user1@test.com") + mb1 = u1.mailbox + self.assertTrue(admin.is_owner(u1)) + self.assertEqual(u1.email, "user1@test.com") + self.assertEqual(u1.first_name, "User") + self.assertEqual(u1.last_name, "One") + self.assertTrue(u1.is_active) + self.assertEqual(u1.group, "SimpleUsers") + self.assertTrue(mb1.use_domain_quota) + self.assertEqual(mb1.quota, 0) + self.assertTrue(admin.is_owner(mb1)) + self.assertEqual(mb1.full_address, "user1@test.com") + self.assertTrue( + self.clt.login(username="user1@test.com", password="toto") + ) + + da = User.objects.get(username="truc@test.com") + damb = da.mailbox + self.assertEqual(da.first_name, u"René") + self.assertEqual(da.group, "DomainAdmins") + self.assertEqual(damb.quota, 5) + self.assertFalse(damb.use_domain_quota) + self.assertEqual(damb.full_address, "truc@test.com") + dom = Domain.objects.get(name="test.com") + self.assertIn(da, dom.admins) + u = User.objects.get(username="user@test.com") + self.assertTrue(da.can_access(u)) + + al = Alias.objects.get(address="alias1@test.com") + self.assertTrue( + al.aliasrecipient_set + .filter(r_mailbox=u1.mailbox).exists() + ) + self.assertTrue(admin.is_owner(al)) + + fwd = Alias.objects.get(address="fwd1@test.com") + self.assertTrue( + fwd.aliasrecipient_set + .filter( + address="user@extdomain.com", r_mailbox__isnull=True, + r_alias__isnull=True) + .exists() + ) + self.assertTrue(admin.is_owner(fwd)) + + dlist = Alias.objects.get(address="dlist@test.com") + self.assertTrue( + dlist.aliasrecipient_set + .filter(r_mailbox=u1.mailbox).exists() + ) + self.assertTrue( + dlist.aliasrecipient_set.filter(address="user@extdomain.com") + .exists() + ) + self.assertTrue(admin.is_owner(dlist)) + + def test_import_for_nonlocal_domain(self): + """Try to import an account for nonlocal domain.""" + f = ContentFile(b""" +account; user1@nonlocal.com; toto; User; One; True; SimpleUsers; user1@nonlocal.com; 0 +""", name="identities.csv") + self.clt.post( + reverse("admin:identity_import"), + {"sourcefile": f, "crypt_password": True} + ) + self.assertFalse( + User.objects.filter(username="user1@nonlocal.com").exists()) + + 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("admin:identity_import"), + {"sourcefile": f, "crypt_password": True} + ) + self.assertIn('wrong quota value', resp.content) + + def test_import_domain_by_domainadmin(self): + """Check if a domain admin is not allowed to import a domain.""" + self.clt.logout() + self.clt.login(username="admin@test.com", password="toto") + f = ContentFile(b""" +domain; domain2.com; 200; False +""", name="identities.csv") + resp = self.clt.post( + reverse("admin:identity_import"), + {"sourcefile": f, "crypt_password": True} + ) + self.assertIn("You are not allowed to import domains", resp.content) + f = ContentFile(b""" +domainalias; domalias1.com; test.com; True +""", name="identities.csv") + resp = self.clt.post( + reverse("admin:identity_import"), + {"sourcefile": f, "crypt_password": True} + ) + self.assertIn( + "You are not allowed to import domain aliases", resp.content) + + def test_import_quota_too_big(self): + self.clt.logout() + self.clt.login(username="admin@test.com", password="toto") + f = ContentFile(b""" +account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com; 20 +""", name="identities.csv") + resp = self.clt.post( + reverse("admin:identity_import"), + {"sourcefile": f, "crypt_password": True} + ) + 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("admin:identity_import"), + {"sourcefile": f, "crypt_password": True} + ) + account = User.objects.get(username="user1@test.com") + self.assertEqual( + account.mailbox.quota, + account.mailbox.domain.quota + ) + + def test_import_duplicate(self): + f = ContentFile(b""" +account; admin@test.com; toto; Admin; ; True; DomainAdmins; admin@test.com; 0; test.com +account; truc@test.com; toto; René; Truc; True; DomainAdmins; truc@test.com; 0; test.com +""", name="identities.csv") + self.clt.post( + reverse("admin:identity_import"), + {"sourcefile": f, "crypt_password": True, + "continue_if_exists": True} + ) + admin = User.objects.get(username="admin") + u1 = User.objects.get(username="truc@test.com") + self.assertTrue(admin.is_owner(u1)) + + def test_import_superadmin(self): + """Check if a domain admin can import a superadmin + + Expected result: no + """ + self.clt.logout() + self.assertTrue( + self.clt.login(username="admin@test.com", password="toto") + ) + f = ContentFile(b""" +account; sa@test.com; toto; Super; Admin; True; SuperAdmins; superadmin@test.com; 50 +""", name="identities.csv") + self.clt.post( + reverse("admin:identity_import"), + {"sourcefile": f, "crypt_password": True, + "continue_if_exists": True} + ) + 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("admin:identity_import"), + {"sourcefile": f, "crypt_password": True, + "continue_if_exists": True} + ) + alias = Alias.objects.get(address="user.alias@test.com") + self.assertEqual(alias.type, "alias") diff --git a/modoboa/admin/tests/mapfiles.py b/modoboa/admin/tests/mapfiles.py new file mode 100644 index 000000000..e9cfe135b --- /dev/null +++ b/modoboa/admin/tests/mapfiles.py @@ -0,0 +1,17 @@ +"""Test map files generation.""" + +from django.test import TestCase + +from modoboa.lib.test_utils import MapFilesTestCaseMixin + + +class MapFilesTestCase(MapFilesTestCaseMixin, TestCase): + + """Test case for admin.""" + + extension = "admin" + + MAP_FILES = [ + "sql-domains.cf", "sql-domain-aliases.cf", "sql-aliases.cf", + "sql-maintain.cf" + ] diff --git a/modoboa/admin/tests/password_schemes.py b/modoboa/admin/tests/password_schemes.py new file mode 100644 index 000000000..605d90c64 --- /dev/null +++ b/modoboa/admin/tests/password_schemes.py @@ -0,0 +1,57 @@ +from django.core.urlresolvers import reverse + +from modoboa.core import load_core_settings +from modoboa.core.models import User +from modoboa.lib.tests import ModoTestCase +from modoboa.lib import parameters + +from .. import factories + + +class PasswordSchemesTestCase(ModoTestCase): + fixtures = ['initial_users.json'] + + def setUp(self): + super(PasswordSchemesTestCase, self).setUp() + factories.populate_database() + load_core_settings() + + def _create_account(self): + values = dict( + username="tester@test.com", first_name="Tester", last_name="Toto", + password1="Toto1234", password2="Toto1234", role="SimpleUsers", + quota_act=True, + is_active=True, email="tester@test.com", stepid='step2' + ) + self.ajax_post( + reverse("admin:account_add"), + values + ) + + def _test_scheme(self, name, startpattern): + parameters.save_admin('PASSWORD_SCHEME', name, app='core') + self._create_account() + account = User.objects.get(username='tester@test.com') + self.assertTrue(account.password.startswith(startpattern)) + self.assertTrue(account.check_password('Toto1234')) + + def test_sha512crypt_scheme(self): + self._test_scheme('sha512crypt', '{SHA512-CRYPT}') + + def test_sha256crypt_scheme(self): + self._test_scheme('sha256crypt', '{SHA256-CRYPT}') + + def test_md5crypt_scheme(self): + self._test_scheme('md5crypt', '{MD5-CRYPT}') + + def test_sha256_scheme(self): + self._test_scheme('sha256', '{SHA256}') + + def test_md5_scheme(self): + self._test_scheme('md5', '{MD5}') + + def test_crypt(self): + self._test_scheme('crypt', '{CRYPT}') + + def test_plain(self): + self._test_scheme('plain', '{PLAIN}') diff --git a/modoboa/admin/tests/user.py b/modoboa/admin/tests/user.py new file mode 100644 index 000000000..492b0b660 --- /dev/null +++ b/modoboa/admin/tests/user.py @@ -0,0 +1,27 @@ +from django.core.urlresolvers import reverse + +from modoboa.core.models import User +from modoboa.lib.tests import ModoTestCase + +from ..factories import populate_database +from ..models import Alias + + +class ForwardTestCase(ModoTestCase): + + def setUp(self): + super(ForwardTestCase, self).setUp() + populate_database() + + def test_forward_permissions(self): + self.clt.logout() + self.clt.login(username='user@test.com', password='toto') + self.ajax_post( + reverse('user_forward'), + {'dest': 'user@extdomain.com', 'keepcopies': True} + ) + forward = Alias.objects.get(address='user@test.com', internal=False) + sadmin = User.objects.get(username='admin') + self.assertTrue(sadmin.can_access(forward)) + domadmin = User.objects.get(username='admin@test.com') + self.assertTrue(domadmin.can_access(forward)) diff --git a/modoboa/admin/urls.py b/modoboa/admin/urls.py new file mode 100644 index 000000000..3119542df --- /dev/null +++ b/modoboa/admin/urls.py @@ -0,0 +1,61 @@ +from django.conf.urls import patterns, url + + +urlpatterns = patterns( + 'modoboa.admin.views.domain', + + url(r'^$', 'index', name="index"), + url(r'^domains/$', 'domains', name="domain_list"), + url(r'^domains/list/$', '_domains', name="_domain_list"), + url(r'^domains/flatlist/$', 'domains_list', name="domain_flat_list"), + url(r'^domains/stats/$', 'domain_statistics', name="domain_statistics"), + url(r'^domains/new/', 'newdomain', name='domain_add'), + url(r'^domains/(?P\d+)/edit/$', 'editdomain', + name="domain_change"), + url(r'^domains/(?P\d+)/delete/$', 'deldomain', + name="domain_delete"), +) + +urlpatterns += patterns( + 'modoboa.admin.views.identity', + + url(r'^permissions/remove/$', 'remove_permission', + name="permission_remove"), + + url(r'^identities/$', 'identities', name="identity_list"), + url(r'^identities/list/$', '_identities', name="_identity_list"), + url(r'^identities/quotas/$', 'list_quotas', name="quota_list"), + url(r'^identities/page/$', 'get_next_page', name="identity_page"), + + url(r'^accounts/list/$', 'accounts_list', name="account_list"), + url(r'^accounts/new/$', 'newaccount', name="account_add"), + url(r'^accounts/edit/(?P\d+)/$', 'editaccount', + name="account_change"), + url(r'^accounts/delete/(?P\d+)/$', 'delaccount', + name="account_delete"), +) + +urlpatterns += patterns( + 'modoboa.admin.views.alias', + + url(r'^distriblists/new/$', 'newdlist', name="dlist_add"), + url(r'^forwards/new/$', 'newforward', name="forward_add"), + + url(r'^aliases/new/$', 'newalias', name="alias_add"), + url(r'^aliases/edit/(?P\d+)/$', 'editalias', name="alias_change"), + url(r'^aliases/delete/$', 'delalias', name="alias_delete"), +) + +urlpatterns += patterns( + 'modoboa.admin.views.import_', + + url(r'^domains/import/$', 'import_domains', name="domain_import"), + url(r'^identities/import/$', 'import_identities', name="identity_import"), +) + +urlpatterns += patterns( + 'modoboa.admin.views.export', + + url(r'^domains/export/$', 'export_domains', name="domain_export"), + url(r'^identities/export/$', 'export_identities', name="identity_export"), +) diff --git a/modoboa/admin/views/__init__.py b/modoboa/admin/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/admin/views/alias.py b/modoboa/admin/views/alias.py new file mode 100644 index 000000000..6b7a32199 --- /dev/null +++ b/modoboa/admin/views/alias.py @@ -0,0 +1,140 @@ + +from django.contrib.auth.decorators import ( + login_required, permission_required +) +from django.core.urlresolvers import reverse +from django.db import IntegrityError +from django.shortcuts import render +from django.utils.translation import ugettext as _, ungettext + +import reversion + +from modoboa.lib import events +from modoboa.lib.exceptions import PermDeniedException, Conflict +from modoboa.lib.web_utils import render_to_json_response + +from ..forms import AliasForm +from ..models import Alias + + +def _validate_alias(request, form, successmsg, callback=None): + """Alias validation + + Common function shared between creation and modification actions. + """ + if form.is_valid(): + try: + alias = form.save() + except IntegrityError: + raise Conflict(_("Alias with this name already exists")) + if callback: + callback(request.user, alias) + return render_to_json_response(successmsg) + + return render_to_json_response({'form_errors': form.errors}, status=400) + + +def _new_alias(request, title, action, successmsg, + tplname="admin/aliasform.html"): + events.raiseEvent("CanCreate", request.user, "mailbox_aliases") + if request.method == "POST": + def callback(user, alias): + alias.post_create(user) + + form = AliasForm(request.user, request.POST) + return _validate_alias( + request, form, successmsg, callback + ) + + ctx = { + "title": title, + "action": action, + "formid": "aliasform", + "action_label": _("Create"), + "action_classes": "submit", + "form": AliasForm(request.user) + } + return render(request, tplname, ctx) + + +@login_required +@permission_required("admin.add_alias") +@reversion.create_revision() +def newdlist(request): + return _new_alias( + request, _("New distribution list"), + reverse("admin:dlist_add"), + _("Distribution list created") + ) + + +@login_required +@permission_required("admin.add_alias") +@reversion.create_revision() +def newalias(request): + return _new_alias( + request, _("New alias"), reverse("admin:alias_add"), + _("Alias created") + ) + + +@login_required +@permission_required("admin.add_alias") +@reversion.create_revision() +def newforward(request): + return _new_alias( + request, _("New forward"), reverse("admin:forward_add"), + _("Forward created") + ) + + +@login_required +@permission_required("admin.change_alias") +@reversion.create_revision() +def editalias(request, alid, tplname="admin/aliasform.html"): + alias = Alias.objects.get(pk=alid) + if not request.user.can_access(alias): + raise PermDeniedException + if request.method == "POST": + altype = alias.type + if altype == "dlist": + successmsg = _("Distribution list modified") + elif altype == "forward": + successmsg = _("Forward modified") + else: + successmsg = _("Alias modified") + form = AliasForm(request.user, request.POST, instance=alias) + return _validate_alias(request, form, successmsg) + + ctx = { + 'action': reverse("admin:alias_change", args=[alias.id]), + 'formid': 'aliasform', + 'title': alias.address, + 'action_label': _('Update'), + 'action_classes': 'submit', + 'form': AliasForm(request.user, instance=alias) + } + return render(request, tplname, ctx) + + +@login_required +@permission_required("admin.delete_alias") +def delalias(request): + selection = request.GET["selection"].split(",") + for alid in selection: + alias = Alias.objects.get(pk=alid) + if not request.user.can_access(alias): + raise PermDeniedException + if alias.type == 'dlist': + msg = "Distribution list deleted" + msgs = "Distribution lists deleted" + elif alias.type == 'forward': + msg = "Forward deleted" + msgs = "Forwards deleted" + else: + msg = "Alias deleted" + msgs = "Aliases deleted" + alias.delete() + + msg = ungettext(msg, msgs, len(selection)) + return render_to_json_response(msg) diff --git a/modoboa/admin/views/domain.py b/modoboa/admin/views/domain.py new file mode 100644 index 000000000..433896e1f --- /dev/null +++ b/modoboa/admin/views/domain.py @@ -0,0 +1,153 @@ +""" +Domain related views. +""" + +from django.http import HttpResponseRedirect +from django.utils.translation import ugettext as _, ungettext +from django.core.urlresolvers import reverse +from django.shortcuts import render +from django.views.decorators.csrf import ensure_csrf_cookie +from django.contrib.auth.decorators import ( + login_required, permission_required, user_passes_test +) + +import reversion + +from modoboa.core.models import User +from modoboa.lib import parameters, events +from modoboa.lib.web_utils import ( + _render_to_string, render_to_json_response +) +from modoboa.lib.exceptions import ( + PermDeniedException +) +from modoboa.lib.listing import get_sort_order, get_listing_page + +from ..forms import DomainForm, DomainWizard +from ..lib import get_domains +from ..models import ( + Domain, DomainAlias, Mailbox, Alias +) + + +@login_required +def index(request): + return HttpResponseRedirect(reverse("admin:domain_list")) + + +@login_required +@user_passes_test( + lambda u: u.has_perm("admin.view_domains") + or u.has_perm("admin.view_mailboxes") +) +def _domains(request): + sort_order, sort_dir = get_sort_order(request.GET, "name") + filters = dict( + (flt, request.GET.get(flt, None)) + for flt in ['domfilter', 'searchquery'] + + events.raiseQueryEvent('ExtraDomainFilters') + ) + request.session['domains_filters'] = filters + domainlist = get_domains(request.user, **filters) + if sort_order == 'name': + domainlist = sorted( + domainlist, + key=lambda d: getattr(d, sort_order), reverse=sort_dir == '-' + ) + else: + domainlist = sorted(domainlist, key=lambda d: d.tags[0], + reverse=sort_dir == '-') + context = { + "handle_mailboxes": parameters.get_admin( + "HANDLE_MAILBOXES", raise_error=False), + "auto_account_removal": parameters.get_admin("AUTO_ACCOUNT_REMOVAL") + } + page = get_listing_page(domainlist, request.GET.get("page", 1)) + if page is None: + context["length"] = 0 + else: + context["rows"] = _render_to_string( + request, "admin/domains_table.html", { + 'domains': page.object_list, + } + ) + context["pages"] = [page.number] + return render_to_json_response(context) + + +@login_required +@ensure_csrf_cookie +def domains(request, tplname="admin/domains.html"): + if not request.user.has_perm("admin.view_domains"): + if request.user.has_perm("admin.view_mailboxes"): + return HttpResponseRedirect( + reverse("admin:identity_list") + ) + return HttpResponseRedirect(reverse("core:user_index")) + return render(request, tplname, {"selection": "domains"}) + + +@login_required +@permission_required("core.add_user") +def domains_list(request): + doms = [dom.name for dom in Domain.objects.get_for_admin(request.user)] + return render_to_json_response(doms) + + +@login_required +@permission_required("admin.add_domain") +@reversion.create_revision() +def newdomain(request): + events.raiseEvent("CanCreate", request.user, "domains") + return DomainWizard(request).process() + + +@login_required +@permission_required("admin.view_domains") +@reversion.create_revision() +def editdomain(request, dom_id): + """Edit domain view.""" + domain = Domain.objects.get(pk=dom_id) + if not request.user.can_access(domain): + raise PermDeniedException + + instances = dict(general=domain) + events.raiseEvent("FillDomainInstances", request.user, domain, instances) + return DomainForm(request, instances=instances).process() + + +@login_required +@permission_required("admin.delete_domain") +def deldomain(request, dom_id): + keepdir = True if request.POST.get("keepdir", "false") == "true" else False + try: + mb = Mailbox.objects.get(user__id=request.user.id) + except Mailbox.DoesNotExist: + mb = None + + dom = Domain.objects.get(pk=dom_id) + if not request.user.can_access(dom): + raise PermDeniedException + if mb and mb.domain == dom: + raise PermDeniedException(_("You can't delete your own domain")) + dom.delete(request.user, keepdir) + + msg = ungettext("Domain deleted", "Domains deleted", 1) + return render_to_json_response(msg) + + +@login_required +@permission_required("admin.view_domains") +def domain_statistics(request): + """A simple page to present domain statistics.""" + context = {} + if request.user.group == "SuperAdmins": + context.update({ + "domains_counter": Domain.objects.count(), + "domain_aliases_counter": DomainAlias.objects.count(), + "identities_counter": ( + User.objects.count() + + Alias.objects.filter(internal=False).count()), + }) + context.update({"domains": Domain.objects.get_for_admin(request.user)}) + return render(request, "admin/domain_statistics.html", context) diff --git a/modoboa/admin/views/export.py b/modoboa/admin/views/export.py new file mode 100644 index 000000000..4e9430aa0 --- /dev/null +++ b/modoboa/admin/views/export.py @@ -0,0 +1,88 @@ +"""Export related views.""" + +import cStringIO +import csv + +from rfc6266 import build_header + +from django.contrib.auth.decorators import ( + login_required, permission_required, user_passes_test +) +from django.core.urlresolvers import reverse +from django.http import HttpResponse +from django.shortcuts import render +from django.utils.translation import ugettext as _ + +from ..forms import ExportIdentitiesForm, ExportDomainsForm +from ..lib import get_domains, get_identities + + +def _export(content, filename): + """Export a csv file's content + + :param content: the content to export (string) + :param filename: the name that will appear into the response + :return: an ``HttpResponse`` object + """ + resp = HttpResponse(content) + resp["Content-Type"] = "text/csv" + resp["Content-Length"] = len(content) + resp["Content-Disposition"] = build_header(filename) + return resp + + +@login_required +@user_passes_test( + lambda u: u.has_perm("core.add_user") or u.has_perm("admin.add_alias") +) +def export_identities(request): + ctx = { + "title": _("Export identities"), + "action_label": _("Export"), + "action_classes": "submit", + "formid": "exportform", + "action": reverse("admin:identity_export"), + } + + if request.method == "POST": + form = ExportIdentitiesForm(request.POST) + form.is_valid() + fp = cStringIO.StringIO() + csvwriter = csv.writer(fp, delimiter=form.cleaned_data["sepchar"]) + identities = get_identities( + request.user, **request.session['identities_filters']) + for ident in identities: + ident.to_csv(csvwriter) + content = fp.getvalue() + fp.close() + return _export(content, form.cleaned_data["filename"]) + + ctx["form"] = ExportIdentitiesForm() + return render(request, "common/generic_modal_form.html", ctx) + + +@login_required +@permission_required("admin.add_domain") +def export_domains(request): + ctx = { + "title": _("Export domains"), + "action_label": _("Export"), + "action_classes": "submit", + "formid": "exportform", + "action": reverse("admin:domain_export"), + } + + if request.method == "POST": + form = ExportDomainsForm(request.POST) + form.is_valid() + fp = cStringIO.StringIO() + csvwriter = csv.writer(fp, delimiter=form.cleaned_data["sepchar"]) + for dom in get_domains(request.user, + **request.session['domains_filters']): + dom.to_csv(csvwriter) + content = fp.getvalue() + fp.close() + return _export(content, form.cleaned_data["filename"]) + + ctx["form"] = ExportDomainsForm() + return render(request, "common/generic_modal_form.html", ctx) diff --git a/modoboa/admin/views/identity.py b/modoboa/admin/views/identity.py new file mode 100644 index 000000000..7e58118c1 --- /dev/null +++ b/modoboa/admin/views/identity.py @@ -0,0 +1,210 @@ +"""Identity related views.""" + +from django.contrib.auth.decorators import ( + login_required, permission_required, user_passes_test +) +from django.shortcuts import render +from django.utils.translation import ugettext as _, ungettext +from django.views.decorators.csrf import ensure_csrf_cookie + +import reversion + +from modoboa.core.models import User +from modoboa.lib import parameters, events +from modoboa.lib.exceptions import ( + PermDeniedException, BadRequest +) +from modoboa.lib.listing import ( + get_sort_order, get_listing_page +) +from modoboa.lib.web_utils import ( + _render_to_string, render_to_json_response +) + +from ..forms import AccountForm, AccountWizard +from ..lib import get_identities +from ..models import Mailbox, Domain + + +@login_required +@user_passes_test( + lambda u: u.has_perm("core.add_user") or + u.has_perm("admin.add_alias") +) +def _identities(request): + filters = dict((fname, request.GET.get(fname, None)) + for fname in ['searchquery', 'idtfilter', 'grpfilter']) + request.session['identities_filters'] = filters + idents_list = get_identities(request.user, **filters) + sort_order, sort_dir = get_sort_order(request.GET, "identity", + ["identity", "name_or_rcpt", "tags"]) + if sort_order in ["identity", "name_or_rcpt"]: + objects = sorted(idents_list, key=lambda o: getattr(o, sort_order), + reverse=sort_dir == '-') + else: + objects = sorted(idents_list, key=lambda o: o.tags[0], + reverse=sort_dir == '-') + context = { + "handle_mailboxes": parameters.get_admin( + "HANDLE_MAILBOXES", raise_error=False) + } + page = get_listing_page(objects, request.GET.get("page", 1)) + if page is None: + context["length"] = 0 + else: + context["headers"] = _render_to_string( + request, "admin/identity_headers.html", {}) + context["rows"] = _render_to_string( + request, "admin/identities_table.html", { + "identities": page.object_list + } + ) + context["pages"] = [page.number] + return render_to_json_response(context) + + +@login_required +@permission_required("admin.add_mailbox") +def list_quotas(request): + from modoboa.lib.db_utils import db_type + + sort_order, sort_dir = get_sort_order(request.GET, "address") + mboxes = Mailbox.objects.get_for_admin( + request.user, request.GET.get("searchquery", None) + ) + mboxes = mboxes.exclude(quota=0) + if sort_order in ["address", "quota"]: + mboxes = mboxes.order_by("%s%s" % (sort_dir, sort_order)) + elif sort_order == "quota_value__bytes": + where = "admin_mailbox.address||'@'||admin_domain.name" + mboxes = mboxes.extra( + select={"quota_value__bytes": "admin_quota.bytes"}, + where=["admin_quota.username=%s" % where], + tables=["admin_quota", "admin_domain"], + order_by=["%s%s" % (sort_dir, sort_order)] + ) + elif sort_order == "quota_usage": + where = "admin_mailbox.address||'@'||admin_domain.name" + db_type = db_type() + if db_type == "postgres": + select = ( + "(admin_quota.bytes::float / (CAST(admin_mailbox.quota " + "AS BIGINT) * 1048576)) * 100" + ) + else: + select = ( + "admin_quota.bytes / (admin_mailbox.quota " + "* 1048576) * 100" + ) + if db_type == "mysql": + where = "CONCAT(admin_mailbox.address,'@',admin_domain.name)" + mboxes = mboxes.extra( + select={'quota_usage': select}, + where=["admin_quota.username=%s" % where], + tables=["admin_quota", "admin_domain"], + order_by=["%s%s" % (sort_dir, sort_order)] + ) + else: + raise BadRequest(_("Invalid request")) + page = get_listing_page(mboxes, request.GET.get("page", 1)) + context = { + "headers": _render_to_string( + request, "admin/quota_headers.html", {} + ) + } + if page is None: + context["length"] = 0 + else: + context["rows"] = _render_to_string( + request, "admin/quotas.html", { + "mboxes": page + } + ) + context["pages"] = [page.number] + return render_to_json_response(context) + + +@login_required +@user_passes_test( + lambda u: u.has_perm("admin.add_user") or + u.has_perm("admin.add_alias") or + u.has_perm("admin.add_mailbox") +) +def get_next_page(request): + """Return the next page of the identity list.""" + if request.GET.get("objtype", "identity") == "identity": + return _identities(request) + return list_quotas(request) + + +@login_required +@user_passes_test( + lambda u: u.has_perm("admin.add_user") or + u.has_perm("admin.add_alias") +) +@ensure_csrf_cookie +def identities(request, tplname="admin/identities.html"): + return render(request, tplname, { + "selection": "identities", + "deflocation": "list/" + }) + + +@login_required +@permission_required("core.add_user") +def accounts_list(request): + accs = User.objects.filter(is_superuser=False) \ + .exclude(groups__name='SimpleUsers') + res = [a.username for a in accs.all()] + return render_to_json_response(res) + + +@login_required +@permission_required("core.add_user") +@reversion.create_revision() +def newaccount(request): + """Create a new account.""" + return AccountWizard(request).process() + + +@login_required +@permission_required("core.change_user") +@reversion.create_revision() +def editaccount(request, accountid, tplname="common/tabforms.html"): + account = User.objects.get(pk=accountid) + if not request.user.can_access(account): + raise PermDeniedException + mb = account.mailbox if hasattr(account, "mailbox") else None + + instances = dict(general=account, mail=mb, perms=account) + events.raiseEvent("FillAccountInstances", request.user, account, instances) + return AccountForm(request, instances=instances).process() + + +@login_required +@permission_required("core.delete_user") +def delaccount(request, accountid): + keepdir = True if request.POST.get("keepdir", "false") == "true" else False + User.objects.get(pk=accountid).delete(request.user, keep_mb_dir=keepdir) + return render_to_json_response( + ungettext("Account deleted", "Accounts deleted", 1) + ) + + +@login_required +@permission_required("admin.add_domain") +def remove_permission(request): + domid = request.GET.get("domid", None) + daid = request.GET.get("daid", None) + if domid is None or daid is None: + raise BadRequest(_("Invalid request")) + try: + account = User.objects.get(pk=daid) + domain = Domain.objects.get(pk=domid) + except (User.DoesNotExist, Domain.DoesNotExist): + raise BadRequest(_("Invalid request")) + if not request.user.can_access(account) or \ + not request.user.can_access(domain): + raise PermDeniedException + domain.remove_admin(account) + return render_to_json_response({}) diff --git a/modoboa/admin/views/import_.py b/modoboa/admin/views/import_.py new file mode 100644 index 000000000..6c73109cd --- /dev/null +++ b/modoboa/admin/views/import_.py @@ -0,0 +1,142 @@ +"""Import related views.""" + +import csv + +from django.contrib.auth.decorators import ( + login_required, permission_required, user_passes_test +) +from django.core.urlresolvers import reverse +from django.db import transaction +from django.shortcuts import render +from django.utils.translation import ugettext as _ + +import reversion + +from modoboa.lib import events +from modoboa.lib.exceptions import ModoboaException, Conflict + +from ..forms import ImportIdentitiesForm, ImportDataForm +from .. import lib + + +@reversion.create_revision() +def importdata(request, formclass=ImportDataForm): + """Generic import function + + As the process of importing data from a CSV file is the same + whatever the type, we do a maximum of the work here. + + :param request: a ``Request`` instance + :param typ: a string indicating the object type being imported + :return: a ``Response`` instance + """ + error = None + form = formclass(request.POST, request.FILES) + if form.is_valid(): + try: + reader = csv.reader(request.FILES['sourcefile'], + delimiter=form.cleaned_data['sepchar']) + except csv.Error as inst: + error = str(inst) + + if error is None: + try: + cpt = 0 + for row in reader: + if not row: + continue + try: + fct = getattr(lib, "import_%s" % row[0].strip()) + except AttributeError: + fct = events.raiseQueryEvent( + 'ImportObject', row[0].strip() + ) + if not fct: + continue + fct = fct[0] + with transaction.atomic(): + try: + fct(request.user, row, form.cleaned_data) + except Conflict: + if form.cleaned_data["continue_if_exists"]: + continue + raise Conflict( + _("Object already exists: %s" + % form.cleaned_data['sepchar'].join(row[:2])) + ) + cpt += 1 + msg = _("%d objects imported successfully" % cpt) + return render(request, "admin/import_done.html", { + "status": "ok", "msg": msg + }) + except (ModoboaException) as e: + error = str(e) + + return render(request, "admin/import_done.html", { + "status": "ko", "msg": error + }) + + +@login_required +@permission_required("admin.add_domain") +def import_domains(request): + if request.method == "POST": + return importdata(request) + + helptext = _("""Provide a CSV file where lines respect one of the following formats: +
      +
    • domain; name; quota; enabled
    • +
    • domainalias; name; targeted domain; enabled
    • + %s +
    +

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

    +

    You can use a different character as separator.

    +""" % ''.join([unicode(hlp) for hlp in + events.raiseQueryEvent('ExtraDomainImportHelp')])) + + ctx = dict( + title=_("Import domains"), + action_label=_("Import"), + action_classes="submit", + action=reverse("admin:domain_import"), + formid="importform", + enctype="multipart/form-data", + target="import_target", + helptext=helptext, + form=ImportDataForm() + ) + return render(request, "admin/importform.html", ctx) + + +@login_required +@user_passes_test( + lambda u: u.has_perm("core.add_user") or + u.has_perm("admin.add_alias") +) +def import_identities(request): + if request.method == "POST": + return importdata(request, ImportIdentitiesForm) + + helptext = _("""Provide a CSV file where lines respect one of the following formats: +
      +
    • account; loginname; password; first name; last name; enabled; group; address; quota; [, domain, ...]
    • +
    • alias; address; enabled; internal recipient
    • +
    • forward; address; enabled; external recipient
    • +
    • dlist; address; enabled; recipient; recipient; ...
    • +
    +

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

    + +

    You can use a different character as separator.

    +""") + ctx = dict( + title=_("Import identities"), + action_label=_("Import"), + action_classes="submit", + action=reverse("admin:identity_import"), + formid="importform", + enctype="multipart/form-data", + target="import_target", + form=ImportIdentitiesForm(), + helptext=helptext + ) + return render(request, "admin/importform.html", ctx) diff --git a/modoboa/admin/views/user.py b/modoboa/admin/views/user.py new file mode 100644 index 000000000..4c6517a99 --- /dev/null +++ b/modoboa/admin/views/user.py @@ -0,0 +1,49 @@ + +from django.contrib.auth.decorators import login_required +from django.utils.translation import ugettext as _ + +import reversion + +from modoboa.lib.web_utils import ( + render_to_json_response, _render_to_string +) + +from ..forms import ForwardForm +from ..lib import needs_mailbox +from ..models import Alias + + +@login_required +@needs_mailbox() +@reversion.create_revision() +def forward(request, tplname="admin/forward.html"): + mb = request.user.mailbox + al = Alias.objects.filter(address=mb.full_address, internal=False).first() + if request.method == "POST": + form = ForwardForm(request.POST) + if form.is_valid(): + if al is None: + al = Alias.objects.create( + address=mb.full_address, domain=mb.domain, + enabled=mb.user.is_active) + recipients = form.get_recipients() + if form.cleaned_data["keepcopies"]: + recipients.append(mb.full_address) + al.set_recipients(recipients) + al.post_create(request.user) + return render_to_json_response(_("Forward updated")) + + return render_to_json_response( + {'form_errors': form.errors}, status=400 + ) + + form = ForwardForm() + if al is not None: + form.fields["dest"].initial = al.recipients + if al.aliasrecipient_set.filter(mailbox=mb.id).exists(): + form.fields["keepcopies"].initial = True + return render_to_json_response({ + "content": _render_to_string(request, tplname, { + "form": form + }) + }) diff --git a/modoboa/core/management/commands/load_initial_data.py b/modoboa/core/management/commands/load_initial_data.py index c9844b564..bc600ac31 100644 --- a/modoboa/core/management/commands/load_initial_data.py +++ b/modoboa/core/management/commands/load_initial_data.py @@ -8,6 +8,7 @@ from django.contrib.auth.models import Group from django.core.management.base import BaseCommand +from modoboa.admin.app_settings import load_admin_settings from modoboa.core import load_core_settings, PERMISSIONS from modoboa.core.extensions import exts_pool from modoboa.core.models import User, ObjectAccess @@ -15,6 +16,8 @@ from modoboa.lib.cryptutils import random_key from modoboa.lib import events from modoboa.lib import models as lib_models from modoboa.lib.permissions import add_permissions_to_group +from modoboa.limits.app_settings import load_limits_settings +import modoboa.relaydomains.models as relay_models from . import CloseConnectionMixin @@ -27,6 +30,8 @@ class Command(BaseCommand, CloseConnectionMixin): def handle(self, *args, **options): """Command entry point.""" load_core_settings() + load_admin_settings() + load_limits_settings() if not User.objects.filter(is_superuser=True).count(): admin = User(username="admin", is_superuser=True) @@ -41,6 +46,9 @@ class Command(BaseCommand, CloseConnectionMixin): lib_models.Parameter.objects.create( name=param_name, value=random_key()) + for service_name in ['relay', 'smtp']: + relay_models.Service.objects.get_or_create(name=service_name) + exts_pool.load_all() superadmin = User.objects.filter(is_superuser=True).first() diff --git a/modoboa/core/views/auth.py b/modoboa/core/views/auth.py index e515cb61f..44325a366 100644 --- a/modoboa/core/views/auth.py +++ b/modoboa/core/views/auth.py @@ -41,8 +41,7 @@ def dologin(request): if user.group == "SimpleUsers": nextlocation = reverse("topredirection") else: - # FIXME - nextlocation = reverse("modoboa_admin:domain_list") + nextlocation = reverse("admin:domain_list") return HttpResponseRedirect(nextlocation) error = _( "Your username and password didn't match. Please try again.") diff --git a/modoboa/lib/migrations/0003_rename_parameters.py b/modoboa/lib/migrations/0003_rename_parameters.py new file mode 100644 index 000000000..642e5a5a8 --- /dev/null +++ b/modoboa/lib/migrations/0003_rename_parameters.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + +APPS = ( + ("modoboa_admin", "admin"), + ("modoboa_admin_limits", "limits"), + ("modoboa_admin_relaydomains", "relaydomains"), +) + + +def rename_parameters(apps, schema_editor): + """Rename old parameters.""" + Parameter = apps.get_model("lib", "Parameter") + UserParameter = apps.get_model("lib", "UserParameter") + for oldapp, newapp in APPS: + for p in Parameter.objects.filter(name__startswith=oldapp): + p.name = p.name.replace(oldapp, newapp) + p.save() + for p in UserParameter.objects.filter(name__startswith=oldapp): + p.name = p.name.replace(oldapp, newapp) + p.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('lib', '0002_rename_parameters'), + ('admin', '0001_initial'), + ] + + operations = [ + migrations.RunPython(rename_parameters) + ] diff --git a/modoboa/lib/web_utils.py b/modoboa/lib/web_utils.py index 7d9467a27..eb06c157d 100644 --- a/modoboa/lib/web_utils.py +++ b/modoboa/lib/web_utils.py @@ -163,7 +163,7 @@ def topredirection(request): path = reverse("core:user_index") else: # FIXME - path = reverse("modoboa_admin:domain_list") + path = reverse("admin:domain_list") return HttpResponseRedirect(path) diff --git a/modoboa/limits/__init__.py b/modoboa/limits/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/limits/app_settings.py b/modoboa/limits/app_settings.py new file mode 100644 index 000000000..ce16d2e55 --- /dev/null +++ b/modoboa/limits/app_settings.py @@ -0,0 +1,83 @@ +"""Custom settings.""" + +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from modoboa.lib import events, parameters +from modoboa.lib.form_utils import SeparatorField +from modoboa.lib.parameters import AdminParametersForm + +EVENTS = [ + 'GetExtraLimitTemplates' +] + + +class ParametersForm(AdminParametersForm): + app = "limits" + + defv_sep = SeparatorField(label=_("Default limits")) + + deflt_domain_admins_limit = forms.IntegerField( + label=_("Domain admins"), + initial=0, + help_text=_( + "Maximum number of allowed domain administrators for a new " + "administrator. (0 to deny any creation, -1 to allow unlimited " + "creations)" + ), + widget=forms.widgets.TextInput( + attrs={"class": "col-md-1 form-control"}) + ) + deflt_domains_limit = forms.IntegerField( + label=_("Domains"), + initial=0, + help_text=_( + "Maximum number of allowed domains for a new administrator. " + "(0 to deny any creation, -1 to allow unlimited creations)" + ), + widget=forms.widgets.TextInput( + attrs={"class": "col-md-1 form-control"}) + ) + deflt_domain_aliases_limit = forms.IntegerField( + label=_("Domain aliases"), + initial=0, + help_text=_( + "Maximum number of allowed domain aliases for a new " + "administrator. (0 to deny any creation, -1 to allow " + "unlimited creations)" + ), + widget=forms.widgets.TextInput( + attrs={"class": "col-md-1 form-control"}) + ) + deflt_mailboxes_limit = forms.IntegerField( + label=_("Mailboxes"), + initial=0, + help_text=_( + "Maximum number of allowed mailboxes for a new administrator. " + "(0 to deny any creation, -1 to allow unlimited creations)" + ), + widget=forms.widgets.TextInput( + attrs={"class": "col-md-1 form-control"}) + ) + deflt_mailbox_aliases_limit = forms.IntegerField( + label=_("Mailbox aliases"), + initial=0, + help_text=_( + "Maximum number of allowed aliases for a new administrator. " + "(0 to deny any creation, -1 to allow unlimited creations)" + ), + widget=forms.widgets.TextInput( + attrs={"class": "col-md-1 form-control"}) + ) + + def __init__(self, *args, **kwargs): + super(AdminParametersForm, self).__init__(*args, **kwargs) + self._load_extra_parameters('A') + + +def load_limits_settings(): + """Load settings.""" + parameters.register(ParametersForm, _("Limits")) + events.declare(EVENTS) + from . import controls + from . import general_callbacks diff --git a/modoboa/limits/controls.py b/modoboa/limits/controls.py new file mode 100644 index 000000000..28cba46b0 --- /dev/null +++ b/modoboa/limits/controls.py @@ -0,0 +1,213 @@ +# coding: utf-8 +""" +:mod:`controls` --- provides event handlers that check if limits are reached +---------------------------------------------------------------------------- + +""" + +from modoboa.lib import events +from modoboa.lib.permissions import get_object_owner + +import modoboa.admin.models as admin_models + +from .lib import LimitReached, inc_limit_usage, dec_limit_usage +from .models import LimitTemplates, LimitsPool + + +def check_limit(user, lname, count=1): + """Check if a user has reached a defined limit + + We check if the user will not reach the given limit after this + creation. + + :param user: a ``User`` object + :param int count: the number of objects the user tries to create + :raises: ``LimitReached`` + """ + try: + if user.limitspool.will_be_reached(lname, count): + raise LimitReached(user.limitspool.get_limit(lname)) + except LimitsPool.DoesNotExist: + pass + + +def move_pool_resource(owner, user): + """Move resource from one pool to another + + When an account doesn't need a pool anymore, we move the + associated resource to the pool of its owner. + """ + if not hasattr(user, "limitspool"): + return + if not owner.is_superuser: + for ltpl in LimitTemplates().templates: + l = user.limitspool.get_limit(ltpl[0]) + if l.maxvalue < 0: + continue + ol = owner.limitspool.get_limit(ltpl[0]) + ol.curvalue += l.curvalue + ol.maxvalue += l.maxvalue + ol.save() + + user.limitspool.delete() + + +@events.observe('DomainCreated') +def inc_nb_domains(user, domain): + inc_limit_usage(user, 'domains_limit') + + +@events.observe('DomainDeleted') +def dec_nb_domains(domain, owner=None): + if owner is None: + owner = get_object_owner(domain) + dec_limit_usage(owner, 'domains_limit') + for domalias in domain.domainalias_set.all(): + dec_nb_domaliases(domalias) + for mailbox in domain.mailbox_set.all(): + dec_nb_mailboxes(mailbox) + for mbalias in domain.alias_set.all(): + dec_nb_mbaliases(mbalias) + + +@events.observe('DomainAliasCreated') +def inc_nb_domaliases(user, domalias): + inc_limit_usage(user, 'domain_aliases_limit') + + +@events.observe('DomainAliasDeleted') +def dec_nb_domaliases(domainaliases): + if isinstance(domainaliases, admin_models.DomainAlias): + domainaliases = [domainaliases] + for domainalias in domainaliases: + owner = get_object_owner(domainalias) + dec_limit_usage(owner, 'domain_aliases_limit') + + +@events.observe('MailboxCreated') +def inc_nb_mailboxes(user, mailbox): + inc_limit_usage(user, 'mailboxes_limit') + + +@events.observe('MailboxDeleted') +def dec_nb_mailboxes(mailboxes): + if isinstance(mailboxes, admin_models.Mailbox): + mailboxes = [mailboxes] + for mailbox in mailboxes: + owner = get_object_owner(mailbox) + dec_limit_usage(owner, 'mailboxes_limit') + + +@events.observe('MailboxAliasCreated') +def inc_nb_mbaliases(user, mailboxalias): + inc_limit_usage(user, 'mailbox_aliases_limit') + + +@events.observe('MailboxAliasDeleted') +def dec_nb_mbaliases(mailboxaliases): + if isinstance(mailboxaliases, admin_models.Alias): + mailboxaliases = [mailboxaliases] + for alias in mailboxaliases: + owner = get_object_owner(alias) + dec_limit_usage(owner, 'mailbox_aliases_limit') + + +@events.observe('CanCreate') +def can_create_new_object(user, objtype, count=1): + check_limit(user, '%s_limit' % objtype, count) + + +@events.observe("AccountCreated") +def create_pool(user): + owner = get_object_owner(user) + if owner.group not in ['SuperAdmins', 'Resellers']: + return + + if user.group == 'DomainAdmins': + check_limit(owner, 'domain_admins_limit') + inc_limit_usage(owner, 'domain_admins_limit') + + if user.group in ['DomainAdmins', 'Resellers']: + p, created = LimitsPool.objects.get_or_create(user=user) + p.create_limits(owner) + + +@events.observe("UserCanSetRole") +def user_can_set_role(user, role, account=None): + """Check if the user can still set this role. + + The only interesting case concerns resellers defining new domain + administrators. We want to check if they are allowed to do this + operation before any modification is made to :keyword:`account`. + + :param ``User`` user: connected user + :param ``User`` account: account modified (None on creation) + :param str newrole: role to check + """ + if role == 'DomainAdmins': + lname = 'domain_admins_limit' + else: + return [True] + if user.is_superuser or not user.limitspool.will_be_reached(lname): + return [True] + if account is not None and account.group == role: + return [True] + return [False] + + +@events.observe("AccountModified") +def on_account_modified(old, new): + """Update limits when roles are updated""" + owner = get_object_owner(old) + if owner.group not in ["SuperAdmins", "Resellers"]: + # Domain admins can't change the role so nothing to check. + return + + if new.group != "SuperAdmins": + # Check if account needs a pool (case: a superadmin is + # downgraded) + if not hasattr(new, "limitspool"): + p = LimitsPool(user=new) + p.save() + p.create_limits(owner) + + if new.group not in ["DomainAdmins", "Resellers"]: + move_pool_resource(owner, new) + + if old.oldgroup == "DomainAdmins": + if new.group != "DomainAdmins": + dec_limit_usage(owner, 'domain_admins_limit') + return + + if new.group == "DomainAdmins": + check_limit(owner, 'domain_admins_limit') + inc_limit_usage(owner, 'domain_admins_limit') + + +@events.observe("AccountDeleted") +def on_account_deleted(account, byuser, **kwargs): + owner = get_object_owner(account) + if owner.group not in ["SuperAdmins", "Resellers"]: + return + + move_pool_resource(owner, account) + + if account.group == "DomainAdmins": + dec_limit_usage(owner, 'domain_admins_limit') + + +@events.observe('DomainOwnershipRemoved') +def domain_ownership_removed(reseller, domain): + """DomainOwnershipRemoved listener. + + The access :keyword:`reseller` had to :keyword:`domain` has been + removed by a super adminstrator. We must decrement all limit + usages. + + :param ``User`` reseller: reseller that created :keyword:`domain` + :param ``Domain`` domain: domain + """ + dec_nb_domains(domain, reseller) + for dadmin in domain.admins: + if reseller.is_owner(dadmin): + dec_limit_usage(reseller, 'domain_admins_limit') diff --git a/modoboa/limits/forms.py b/modoboa/limits/forms.py new file mode 100644 index 000000000..7baa91eb7 --- /dev/null +++ b/modoboa/limits/forms.py @@ -0,0 +1,91 @@ +# coding: utf-8 + +"""Custom forms.""" + +from django import forms +from django.utils.translation import ugettext as _ + +from modoboa.lib import parameters +from modoboa.lib.permissions import get_object_owner + +from .lib import BadLimitValue, UnsufficientResource +from .models import LimitTemplates + + +class ResourcePoolForm(forms.Form): + + def __init__(self, *args, **kwargs): + self.account = None + if "instance" in kwargs: + self.account = kwargs["instance"] + del kwargs["instance"] + super(ResourcePoolForm, self).__init__(*args, **kwargs) + for tpl in LimitTemplates().templates: + if len(tpl) > 3: + if self.account is not None and self.account.group != tpl[3]: + continue + self.fields[tpl[0]] = forms.IntegerField( + label=tpl[1], help_text=tpl[2] + ) + if hasattr(self, "account"): + self.load_from_user(self.account) + + def check_limit_value(self, lname): + if self.cleaned_data[lname] < -1: + raise forms.ValidationError(_("Invalid limit")) + return self.cleaned_data[lname] + + def clean(self): + cleaned_data = super(ResourcePoolForm, self).clean() + for lname in self.fields.keys(): + if lname not in self._errors and cleaned_data[lname] < -1: + self.add_error(lname, _("Invalid limit")) + return cleaned_data + + def load_from_user(self, user): + for lname in self.fields.keys(): + self.fields[lname].initial = user.limitspool.getmaxvalue(lname) + # The following lines will become useless in a near + # future. + if self.fields[lname].initial == -2: + self.fields[lname].initial = parameters.get_admin( + "DEFLT_%s" % + lname.upper()) + + def allocate_from_pool(self, limit, pool): + """Allocate resource using an existing pool. + + When a reseller creates a domain administrator, he generally + assigns him resource to create new objetcs. As a reseller may + also be limited, the resource he gives is taken from its own + pool. + """ + ol = pool.get_limit(limit.name) + if ol.maxvalue == -2: + raise BadLimitValue(_("Your resources are not initialized yet")) + newvalue = self.cleaned_data[limit.name] + if newvalue == -1 and ol.maxvalue != -1: + raise BadLimitValue( + _("You're not allowed to define unlimited values") + ) + + if limit.maxvalue > -1: + newvalue -= limit.maxvalue + if newvalue == 0: + return + remain = ol.maxvalue - ol.curvalue + if newvalue > remain: + raise UnsufficientResource(ol) + ol.maxvalue -= newvalue + ol.save() + + def save(self): + owner = get_object_owner(self.account) + for ltpl in LimitTemplates().templates: + if ltpl[0] not in self.cleaned_data: + continue + l = self.account.limitspool.limit_set.get(name=ltpl[0]) + if not owner.is_superuser: + self.allocate_from_pool(l, owner.limitspool) + l.maxvalue = self.cleaned_data[ltpl[0]] + l.save() diff --git a/modoboa/limits/general_callbacks.py b/modoboa/limits/general_callbacks.py new file mode 100644 index 000000000..5b8010574 --- /dev/null +++ b/modoboa/limits/general_callbacks.py @@ -0,0 +1,127 @@ +"""General event callbacks.""" + +from django.utils.translation import ugettext as _ + +from modoboa.lib import events + +from modoboa.admin.callbacks import PERMISSIONS as ADMIN_PERMS + +from .forms import ResourcePoolForm +from .models import LimitTemplates + + +PERMISSIONS = { + "Resellers": ( + ADMIN_PERMS.get("DomainAdmins") + + [["admin", "domain", "view_domains"], + ["admin", "domain", "add_domain"], + ["admin", "domain", "change_domain"], + ["admin", "domain", "delete_domain"]] + ) +} + + +@events.observe("GetExtraRoles") +def get_extra_roles(user, account): + """Return additional roles.""" + if user.is_superuser: + return [("Resellers", _("Reseller")), ] + return [] + + +@events.observe("GetExtraRolePermissions") +def extra_permissions(rolename): + """Return extra permissions for Resellers.""" + return PERMISSIONS.get(rolename, []) + + +@events.observe("ExtraAdminContent") +def display_pool_usage(user, target, currentpage): + from django.template.loader import render_to_string + + if target != "leftcol" or user.is_superuser: + return [] + if currentpage == "identities": + names = ["mailboxes_limit", "mailbox_aliases_limit"] + if user.has_perm("admin.add_domain"): + names += ["domain_admins_limit"] + else: + names = [ + tpl[0] for tpl in LimitTemplates().templates + if tpl[0] not in ["domain_admins_limit", "mailboxes_limit", + "mailbox_aliases_limit"] + and (len(tpl) == 3 or tpl[3] == user.group) + ] + + limits = user.limitspool.limit_set.filter(name__in=names, maxvalue__gt=0) + if len(limits) == 0: + return [] + return [ + render_to_string("admin_limits/poolusage.html", + dict(limits=limits)) + ] + + +@events.observe("ExtraAccountForm") +def extra_account_form(user, account=None): + if user.group not in ["SuperAdmins", "Resellers"]: + return [] + if account is not None and \ + account.group not in ["Resellers", "DomainAdmins"]: + return [] + + return [ + dict( + id="resources", title=_("Resources"), cls=ResourcePoolForm + ) + ] + + +@events.observe("CheckExtraAccountForm") +def check_form_access(account, form): + if form["id"] != "resources": + return [True] + if not account.belongs_to_group("Resellers") and \ + not account.belongs_to_group("DomainAdmins"): + return [False] + return [True] + + +@events.observe("FillAccountInstances") +def fill_account_instances(user, account, instances): + if not user.is_superuser and not user.belongs_to_group("Resellers"): + return + if not account.belongs_to_group("Resellers") and \ + not account.belongs_to_group("DomainAdmins"): + return + instances["resources"] = account + + +@events.observe("GetStaticContent") +def get_static_content(caller, st_type, user): + if caller not in ['domains', 'identities']: + return [] + if user.group == "SimpleUsers": + return [] + if st_type == "css": + return [""" +"""] + return [""" + +"""] diff --git a/modoboa/limits/lib.py b/modoboa/limits/lib.py new file mode 100644 index 000000000..409bf758e --- /dev/null +++ b/modoboa/limits/lib.py @@ -0,0 +1,52 @@ +# coding: utf-8 +""" +:mod:`lib` --- public functions +------------------------------- + +""" +from django.utils.translation import ugettext as _ + +from modoboa.lib.exceptions import ModoboaException + +from .models import LimitsPool + + +class LimitReached(ModoboaException): + http_code = 403 + + def __init__(self, limit): + self.limit = limit + + def __str__(self): + return _("%s: limit reached") % self.limit.label + + +class UnsufficientResource(ModoboaException): + http_code = 424 + + def __init__(self, limit): + self.limit = limit + + def __str__(self): + return _("Not enough resources") + + +class BadLimitValue(ModoboaException): + http_code = 400 + + +def inc_limit_usage(user, lname): + """Increase a given limit usage.""" + try: + user.limitspool.inc_curvalue(lname) + except LimitsPool.DoesNotExist: + pass + + +def dec_limit_usage(user, lname): + if user is None: + return + try: + user.limitspool.dec_curvalue(lname) + except LimitsPool.DoesNotExist: + pass diff --git a/modoboa/limits/locale/cs_CZ/LC_MESSAGES/django.po b/modoboa/limits/locale/cs_CZ/LC_MESSAGES/django.po new file mode 100644 index 000000000..c3fec83b8 --- /dev/null +++ b/modoboa/limits/locale/cs_CZ/LC_MESSAGES/django.po @@ -0,0 +1,154 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Miroslav Abrahám , 2013-2015 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: 2015-01-06 10:05+0000\n" +"Last-Translator: Miroslav Abrahám \n" +"Language-Team: Czech (Czech Republic) (http://www.transifex.com/projects/p/" +"modoboa/language/cs_CZ/)\n" +"Language: cs_CZ\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" + +#: limits/app_settings.py:18 +msgid "Default limits" +msgstr "Výchozí omezení" + +#: limits/app_settings.py:21 limits/models.py:17 +msgid "Domain admins" +msgstr "Správci domén" + +#: limits/app_settings.py:24 +#, fuzzy +#| msgid "" +#| "Maximum number of allowed domain administrators for a new administrator" +msgid "" +"Maximum number of allowed domain administrators for a new administrator. (0 " +"to deny any creation, -1 to allow unlimited creations)" +msgstr "Maximální povolený počet správců domény (nastavení nového správce)" + +#: limits/app_settings.py:32 limits/models.py:21 +msgid "Domains" +msgstr "Domény" + +#: limits/app_settings.py:35 +#, fuzzy +#| msgid "Maximum number of allowed domains for a new administrator" +msgid "" +"Maximum number of allowed domains for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "Maximální povolený počet domén (nastavení nového správce)" + +#: limits/app_settings.py:42 limits/models.py:24 +msgid "Domain aliases" +msgstr "Doménové aliasy" + +#: limits/app_settings.py:45 +#, fuzzy +#| msgid "Maximum number of allowed domain aliases for a new administrator" +msgid "" +"Maximum number of allowed domain aliases for a new administrator. (0 to deny " +"any creation, -1 to allow unlimited creations)" +msgstr "Maximální povolený počet doménových aliasů (nastavení nového správce)" + +#: limits/app_settings.py:53 limits/models.py:28 +msgid "Mailboxes" +msgstr "Schránky" + +#: limits/app_settings.py:56 +#, fuzzy +#| msgid "Maximum number of allowed mailboxes for a new administrator" +msgid "" +"Maximum number of allowed mailboxes for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "Maximální povolený počet schránek (nastavení nového správce)" + +#: limits/app_settings.py:63 limits/models.py:30 +msgid "Mailbox aliases" +msgstr "Aliasy schránek" + +#: limits/app_settings.py:66 +#, fuzzy +#| msgid "Maximum number of allowed aliases for a new administrator" +msgid "" +"Maximum number of allowed aliases for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "Maximální povolený počet aliasů (nastavení nového správce)" + +#: limits/app_settings.py:80 +msgid "Limits" +msgstr "Omezení" + +#: limits/forms.py:35 limits/forms.py:42 +msgid "Invalid limit" +msgstr "Neplatné omezení" + +#: limits/forms.py:65 +msgid "Your resources are not initialized yet" +msgstr "Vaše zdroje nebyly dosud definovány" + +#: limits/forms.py:69 +msgid "You're not allowed to define unlimited values" +msgstr "Nemáte oprávnění nastavovat neomezené hodnoty" + +#: limits/general_callbacks.py:28 +msgid "Reseller" +msgstr "Distributor" + +#: limits/general_callbacks.py:75 +msgid "Resources" +msgstr "Zdroje" + +#: limits/lib.py:21 +#, python-format +msgid "%s: limit reached" +msgstr "%s: limit byl dosažen" + +#: limits/lib.py:31 +msgid "Not enough resources" +msgstr "Nedostatek zdrojů" + +#: limits/models.py:18 +msgid "Maximum number of domain administrators this user can create" +msgstr "" +"Maximální povolený počet správců domény, které může tento uživatel vytvořit" + +#: limits/models.py:22 +msgid "Maximum number of domains this user can create" +msgstr "Maximální povolený počet domén, které může tento uživatel vytvořit" + +#: limits/models.py:26 +msgid "Maximum number of domain aliases this user can create" +msgstr "" +"Maximální povolený počet doménových aliasů, které může tento uživatel " +"vytvořit" + +#: limits/models.py:29 +msgid "Maximum number of mailboxes this user can create" +msgstr "Maximální povolený počet schránek, které může tento uživatel vytvořit" + +#: limits/models.py:31 +msgid "Maximum number of mailbox aliases this user can create" +msgstr "" +"Maximální povolený počet aliasů chránek, které může tento uživatel vytvořit" + +#: limits/models.py:147 +msgid "undefined" +msgstr "nedefinováno" + +#: limits/models.py:149 +msgid "unlimited" +msgstr "neomezeno" + +#: limits/templates/limits/poolusage.html:3 +msgid "Resources usage" +msgstr "Využití zdrojů" diff --git a/modoboa/limits/locale/de/LC_MESSAGES/django.po b/modoboa/limits/locale/de/LC_MESSAGES/django.po new file mode 100644 index 000000000..13478b8c6 --- /dev/null +++ b/modoboa/limits/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,156 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Arvedui , 2014 +# Patrick Koetter , 2010 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: 2014-12-03 11:02+0000\n" +"Last-Translator: Arvedui \n" +"Language-Team: German (http://www.transifex.com/projects/p/modoboa/language/" +"de/)\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: limits/app_settings.py:18 +msgid "Default limits" +msgstr "Standard-Grenzwerte" + +#: limits/app_settings.py:21 limits/models.py:17 +msgid "Domain admins" +msgstr "Domain-Administratoren" + +#: limits/app_settings.py:24 +#, fuzzy +#| msgid "" +#| "Maximum number of allowed domain administrators for a new administrator" +msgid "" +"Maximum number of allowed domain administrators for a new administrator. (0 " +"to deny any creation, -1 to allow unlimited creations)" +msgstr "" +"Maximale Anzahl der Domain-Administratoren, die ein neuer Administrator " +"erstellen darf" + +#: limits/app_settings.py:32 limits/models.py:21 +msgid "Domains" +msgstr "Domains" + +#: limits/app_settings.py:35 +#, fuzzy +#| msgid "Maximum number of allowed domains for a new administrator" +msgid "" +"Maximum number of allowed domains for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "Maximale Anzahl Domains, die ein neuer Administrator erstellen darf" + +#: limits/app_settings.py:42 limits/models.py:24 +msgid "Domain aliases" +msgstr "Domain-Aliase" + +#: limits/app_settings.py:45 +#, fuzzy +#| msgid "Maximum number of allowed domain aliases for a new administrator" +msgid "" +"Maximum number of allowed domain aliases for a new administrator. (0 to deny " +"any creation, -1 to allow unlimited creations)" +msgstr "" +"Maximale Anzahl Domain-Aliase, die ein neuer Administrator erstellen darf" + +#: limits/app_settings.py:53 limits/models.py:28 +msgid "Mailboxes" +msgstr "Mailboxen" + +#: limits/app_settings.py:56 +#, fuzzy +#| msgid "Maximum number of allowed mailboxes for a new administrator" +msgid "" +"Maximum number of allowed mailboxes for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "Maximale Anzahl Mailboxen, die ein neuer Administrator erstellen darf" + +#: limits/app_settings.py:63 limits/models.py:30 +msgid "Mailbox aliases" +msgstr "Mailboxen-Aliase" + +#: limits/app_settings.py:66 +#, fuzzy +#| msgid "Maximum number of allowed aliases for a new administrator" +msgid "" +"Maximum number of allowed aliases for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "Maximale Anzahl Aliase, die ein neuer Administrator erstellen darf" + +#: limits/app_settings.py:80 +msgid "Limits" +msgstr "Grenzwerte" + +#: limits/forms.py:35 limits/forms.py:42 +msgid "Invalid limit" +msgstr "Ungültiger Grenzwert" + +#: limits/forms.py:65 +msgid "Your resources are not initialized yet" +msgstr "Ihre Ressourcen wurde noch nicht initialisiert" + +#: limits/forms.py:69 +msgid "You're not allowed to define unlimited values" +msgstr "" +"Sie verfügen nicht über die erforderlichen Rechte, um unbegrenzte Werte " +"anzugeben" + +#: limits/general_callbacks.py:28 +msgid "Reseller" +msgstr "Reseller" + +#: limits/general_callbacks.py:75 +msgid "Resources" +msgstr "Ressourcen" + +#: limits/lib.py:21 +#, python-format +msgid "%s: limit reached" +msgstr "%s: Grenze erreicht" + +#: limits/lib.py:31 +msgid "Not enough resources" +msgstr "Nicht ausreichend Ressourcen" + +#: limits/models.py:18 +msgid "Maximum number of domain administrators this user can create" +msgstr "Maximale Anzahl Domains, die ein Administrator erstellen darf" + +#: limits/models.py:22 +msgid "Maximum number of domains this user can create" +msgstr "Maximale Anzahl Domains, die dieser Benutzer erstellen darf" + +#: limits/models.py:26 +msgid "Maximum number of domain aliases this user can create" +msgstr "Maximale Anzahl Domain Aliase, die dieser Benutzer erstellen darf" + +#: limits/models.py:29 +msgid "Maximum number of mailboxes this user can create" +msgstr "Maximale Anzahl Mailboxen, die dieser Benutzer erstellen darf" + +#: limits/models.py:31 +msgid "Maximum number of mailbox aliases this user can create" +msgstr "Maximale Anzahl Mailbox-Aliase, die dieser Benutzer erstellen darf" + +#: limits/models.py:147 +msgid "undefined" +msgstr "unbestimmt" + +#: limits/models.py:149 +msgid "unlimited" +msgstr "unbegrenzt" + +#: limits/templates/limits/poolusage.html:3 +msgid "Resources usage" +msgstr "Ressourcen-Verbrauch" diff --git a/modoboa/limits/locale/en/LC_MESSAGES/django.po b/modoboa/limits/locale/en/LC_MESSAGES/django.po new file mode 100644 index 000000000..ee0ce4e55 --- /dev/null +++ b/modoboa/limits/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,137 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: limits/app_settings.py:18 +msgid "Default limits" +msgstr "" + +#: limits/app_settings.py:21 limits/models.py:17 +msgid "Domain admins" +msgstr "" + +#: limits/app_settings.py:24 +msgid "" +"Maximum number of allowed domain administrators for a new administrator. (0 " +"to deny any creation, -1 to allow unlimited creations)" +msgstr "" + +#: limits/app_settings.py:32 limits/models.py:21 +msgid "Domains" +msgstr "" + +#: limits/app_settings.py:35 +msgid "" +"Maximum number of allowed domains for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "" + +#: limits/app_settings.py:42 limits/models.py:24 +msgid "Domain aliases" +msgstr "" + +#: limits/app_settings.py:45 +msgid "" +"Maximum number of allowed domain aliases for a new administrator. (0 to deny " +"any creation, -1 to allow unlimited creations)" +msgstr "" + +#: limits/app_settings.py:53 limits/models.py:28 +msgid "Mailboxes" +msgstr "" + +#: limits/app_settings.py:56 +msgid "" +"Maximum number of allowed mailboxes for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "" + +#: limits/app_settings.py:63 limits/models.py:30 +msgid "Mailbox aliases" +msgstr "" + +#: limits/app_settings.py:66 +msgid "" +"Maximum number of allowed aliases for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "" + +#: limits/app_settings.py:80 +msgid "Limits" +msgstr "" + +#: limits/forms.py:35 limits/forms.py:42 +msgid "Invalid limit" +msgstr "" + +#: limits/forms.py:65 +msgid "Your resources are not initialized yet" +msgstr "" + +#: limits/forms.py:69 +msgid "You're not allowed to define unlimited values" +msgstr "" + +#: limits/general_callbacks.py:28 +msgid "Reseller" +msgstr "" + +#: limits/general_callbacks.py:75 +msgid "Resources" +msgstr "" + +#: limits/lib.py:21 +#, python-format +msgid "%s: limit reached" +msgstr "" + +#: limits/lib.py:31 +msgid "Not enough resources" +msgstr "" + +#: limits/models.py:18 +msgid "Maximum number of domain administrators this user can create" +msgstr "" + +#: limits/models.py:22 +msgid "Maximum number of domains this user can create" +msgstr "" + +#: limits/models.py:26 +msgid "Maximum number of domain aliases this user can create" +msgstr "" + +#: limits/models.py:29 +msgid "Maximum number of mailboxes this user can create" +msgstr "" + +#: limits/models.py:31 +msgid "Maximum number of mailbox aliases this user can create" +msgstr "" + +#: limits/models.py:147 +msgid "undefined" +msgstr "" + +#: limits/models.py:149 +msgid "unlimited" +msgstr "" + +#: limits/templates/limits/poolusage.html:3 +msgid "Resources usage" +msgstr "" diff --git a/modoboa/limits/locale/es/LC_MESSAGES/django.po b/modoboa/limits/locale/es/LC_MESSAGES/django.po new file mode 100644 index 000000000..9f467b027 --- /dev/null +++ b/modoboa/limits/locale/es/LC_MESSAGES/django.po @@ -0,0 +1,155 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Pedro Gracia , 2011-2013 +# Pedro Gracia , 2010-2011 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: 2013-10-01 09:56+0000\n" +"Last-Translator: Pedro Gracia \n" +"Language-Team: Spanish (http://www.transifex.com/projects/p/modoboa/language/" +"es/)\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: limits/app_settings.py:18 +msgid "Default limits" +msgstr "Límites por defecto" + +#: limits/app_settings.py:21 limits/models.py:17 +msgid "Domain admins" +msgstr "Administradores del dominio" + +#: limits/app_settings.py:24 +#, fuzzy +#| msgid "" +#| "Maximum number of allowed domain administrators for a new administrator" +msgid "" +"Maximum number of allowed domain administrators for a new administrator. (0 " +"to deny any creation, -1 to allow unlimited creations)" +msgstr "" +"Número máximo de dominios permitido de administradores para un nuevo " +"administrador" + +#: limits/app_settings.py:32 limits/models.py:21 +msgid "Domains" +msgstr "Dominios" + +#: limits/app_settings.py:35 +#, fuzzy +#| msgid "Maximum number of allowed domains for a new administrator" +msgid "" +"Maximum number of allowed domains for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "Número máximo de dominios permitidos para un nuevo administrador" + +#: limits/app_settings.py:42 limits/models.py:24 +msgid "Domain aliases" +msgstr "Alias del dominio" + +#: limits/app_settings.py:45 +#, fuzzy +#| msgid "Maximum number of allowed domain aliases for a new administrator" +msgid "" +"Maximum number of allowed domain aliases for a new administrator. (0 to deny " +"any creation, -1 to allow unlimited creations)" +msgstr "" +"Número máximo permitido de alias de dominio para un nuevo administrador" + +#: limits/app_settings.py:53 limits/models.py:28 +msgid "Mailboxes" +msgstr "Mailboxes" + +#: limits/app_settings.py:56 +#, fuzzy +#| msgid "Maximum number of allowed mailboxes for a new administrator" +msgid "" +"Maximum number of allowed mailboxes for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "Número máximo de mailboxes permitidas para un nuevo administrador" + +#: limits/app_settings.py:63 limits/models.py:30 +msgid "Mailbox aliases" +msgstr "Alias del mailbox" + +#: limits/app_settings.py:66 +#, fuzzy +#| msgid "Maximum number of allowed aliases for a new administrator" +msgid "" +"Maximum number of allowed aliases for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "Número máxido de alias permitidos para un nuevo administrador" + +#: limits/app_settings.py:80 +msgid "Limits" +msgstr "Límites" + +#: limits/forms.py:35 limits/forms.py:42 +msgid "Invalid limit" +msgstr "Límite inválido" + +#: limits/forms.py:65 +msgid "Your resources are not initialized yet" +msgstr "Sus recursos no han sido inicializados aún" + +#: limits/forms.py:69 +msgid "You're not allowed to define unlimited values" +msgstr "No está autorizado definir valores ilimitados" + +#: limits/general_callbacks.py:28 +msgid "Reseller" +msgstr "Revendedor" + +#: limits/general_callbacks.py:75 +msgid "Resources" +msgstr "Recursos" + +#: limits/lib.py:21 +#, python-format +msgid "%s: limit reached" +msgstr "%s: límete alcanzado" + +#: limits/lib.py:31 +msgid "Not enough resources" +msgstr "No hay suficientes recursos" + +#: limits/models.py:18 +msgid "Maximum number of domain administrators this user can create" +msgstr "" +"Número máximo de administradores de dominio que este usuario puede crear" + +#: limits/models.py:22 +msgid "Maximum number of domains this user can create" +msgstr "Número máxima de dominios que este usuario puede crear" + +#: limits/models.py:26 +msgid "Maximum number of domain aliases this user can create" +msgstr "Número máximo de alias de dominio que este usuario puede crear" + +#: limits/models.py:29 +msgid "Maximum number of mailboxes this user can create" +msgstr "Número máximo de mailboxes que este usuario puede crear" + +#: limits/models.py:31 +msgid "Maximum number of mailbox aliases this user can create" +msgstr "Número máximo de alias a mailboxes que este usuario puede crear" + +#: limits/models.py:147 +msgid "undefined" +msgstr "no definido" + +#: limits/models.py:149 +msgid "unlimited" +msgstr "ilimitado" + +#: limits/templates/limits/poolusage.html:3 +msgid "Resources usage" +msgstr "Uso de recursos" diff --git a/modoboa/limits/locale/fr/LC_MESSAGES/django.po b/modoboa/limits/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 000000000..54d701fbb --- /dev/null +++ b/modoboa/limits/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,147 @@ +# Modoboa french translation +# Copyright (C) 2009-2013 +# This file is distributed under the same license as the Modoboa package. +# +# Antoine Nguyen , 2009-2013. +msgid "" +msgstr "" +"Project-Id-Version: modoboa-1.1.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: 2011-01-07 10:00+100\n" +"Last-Translator: Antoine Nguyen \n" +"Language-Team: FR \n" +"Language: Français\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: limits/app_settings.py:18 +msgid "Default limits" +msgstr "Limites par défaut" + +#: limits/app_settings.py:21 limits/models.py:17 +msgid "Domain admins" +msgstr "Admins de domaine" + +#: limits/app_settings.py:24 +msgid "" +"Maximum number of allowed domain administrators for a new administrator. (0 " +"to deny any creation, -1 to allow unlimited creations)" +msgstr "" +"Nombre maximal d'admins de domaine pour un nouvel administrateur. (0 pour " +"interdire toute création, -1 pour autoriser les création illimitées)" + +#: limits/app_settings.py:32 limits/models.py:21 +msgid "Domains" +msgstr "Domaines" + +#: limits/app_settings.py:35 +msgid "" +"Maximum number of allowed domains for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "" +"Nombre maximal de domaines pour un nouvel administrateur. (0 pour interdire " +"toute création, -1 pour autoriser les créations illimitées)" + +#: limits/app_settings.py:42 limits/models.py:24 +msgid "Domain aliases" +msgstr "Alias de domaine" + +#: limits/app_settings.py:45 +msgid "" +"Maximum number of allowed domain aliases for a new administrator. (0 to deny " +"any creation, -1 to allow unlimited creations)" +msgstr "" +"Nombre maximal d'alias de domaine pour un nouvel administrateur. (0 pour " +"interdire toute création, -1 pour autoriser les créations illimitées)" + +#: limits/app_settings.py:53 limits/models.py:28 +msgid "Mailboxes" +msgstr "Boites" + +#: limits/app_settings.py:56 +msgid "" +"Maximum number of allowed mailboxes for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "" +"Nombre maximal de boites pour un nouvel administrateur. (0 pour interdire " +"toute création, -1 pour autoriser les créations illimitées)" + +#: limits/app_settings.py:63 limits/models.py:30 +msgid "Mailbox aliases" +msgstr "Alias de boite." + +#: limits/app_settings.py:66 +msgid "" +"Maximum number of allowed aliases for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "" +"Nombre maximal d'alias de boite pour un nouvel administrateur. (0 pour " +"interdire toute création, -1 pour autoriser les créations illimitées)" + +#: limits/app_settings.py:80 +msgid "Limits" +msgstr "Limites" + +#: limits/forms.py:35 limits/forms.py:42 +msgid "Invalid limit" +msgstr "Limite invalide" + +#: limits/forms.py:65 +msgid "Your resources are not initialized yet" +msgstr "Vos ressources ne sont pas encore initialisées" + +#: limits/forms.py:69 +msgid "You're not allowed to define unlimited values" +msgstr "Vous n'êtes pas autorisé à définir des valeurs illimitées" + +#: limits/general_callbacks.py:28 +msgid "Reseller" +msgstr "Revendeur" + +#: limits/general_callbacks.py:75 +msgid "Resources" +msgstr "Ressources" + +#: limits/lib.py:21 +#, python-format +msgid "%s: limit reached" +msgstr "%s : limite atteinte" + +#: limits/lib.py:31 +msgid "Not enough resources" +msgstr "Pas assez de ressources" + +#: limits/models.py:18 +msgid "Maximum number of domain administrators this user can create" +msgstr "Nombre maximal d'admins de domaine que cet utilisateur peut créer" + +#: limits/models.py:22 +msgid "Maximum number of domains this user can create" +msgstr "Nombre maximal de domaines que cet utilisateur peut créer" + +#: limits/models.py:26 +msgid "Maximum number of domain aliases this user can create" +msgstr "Nombre maximal d'alias de domain que cet utilisateur peut créer" + +#: limits/models.py:29 +msgid "Maximum number of mailboxes this user can create" +msgstr "Nombre maximal de boites que cet utilisateur peut créer" + +#: limits/models.py:31 +msgid "Maximum number of mailbox aliases this user can create" +msgstr "Nombre maximal d'alias de boite que cet utilisateur peut créer" + +#: limits/models.py:147 +msgid "undefined" +msgstr "Indéfini" + +#: limits/models.py:149 +msgid "unlimited" +msgstr "illimité" + +#: limits/templates/limits/poolusage.html:3 +msgid "Resources usage" +msgstr "Utilisation des ressources" diff --git a/modoboa/limits/locale/it/LC_MESSAGES/django.po b/modoboa/limits/locale/it/LC_MESSAGES/django.po new file mode 100644 index 000000000..7ccb23c5d --- /dev/null +++ b/modoboa/limits/locale/it/LC_MESSAGES/django.po @@ -0,0 +1,154 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Giancarlo Gaifas , 2013. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: 2013-12-23 00:56+0100\n" +"Last-Translator: Alberto Berti \n" +"Language-Team: ITALIANO \n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: limits/app_settings.py:18 +msgid "Default limits" +msgstr "Limiti predefiniti" + +#: limits/app_settings.py:21 limits/models.py:17 +msgid "Domain admins" +msgstr "Amministratori di domini" + +#: limits/app_settings.py:24 +#, fuzzy +#| msgid "" +#| "Maximum number of allowed domain administrators for a new administrator" +msgid "" +"Maximum number of allowed domain administrators for a new administrator. (0 " +"to deny any creation, -1 to allow unlimited creations)" +msgstr "" +"Numero massimo di amministratori di domini consentiti per un nuovo " +"amministratore" + +#: limits/app_settings.py:32 limits/models.py:21 +msgid "Domains" +msgstr "Domini" + +#: limits/app_settings.py:35 +#, fuzzy +#| msgid "Maximum number of allowed domains for a new administrator" +msgid "" +"Maximum number of allowed domains for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "Numero massimo di domini consentiti per un nuovo amministratore" + +#: limits/app_settings.py:42 limits/models.py:24 +msgid "Domain aliases" +msgstr "Alias del dominio" + +#: limits/app_settings.py:45 +#, fuzzy +#| msgid "Maximum number of allowed domain aliases for a new administrator" +msgid "" +"Maximum number of allowed domain aliases for a new administrator. (0 to deny " +"any creation, -1 to allow unlimited creations)" +msgstr "" +"Numero massimo di alias di dominio consentiti per un nuovo amministratore" + +#: limits/app_settings.py:53 limits/models.py:28 +msgid "Mailboxes" +msgstr "Caselle di posta" + +#: limits/app_settings.py:56 +#, fuzzy +#| msgid "Maximum number of allowed mailboxes for a new administrator" +msgid "" +"Maximum number of allowed mailboxes for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "" +"Numero massimo di caselle di posta consentite per un nuovo amministratore" + +#: limits/app_settings.py:63 limits/models.py:30 +msgid "Mailbox aliases" +msgstr "Alias delle caselle di posta" + +#: limits/app_settings.py:66 +#, fuzzy +#| msgid "Maximum number of allowed aliases for a new administrator" +msgid "" +"Maximum number of allowed aliases for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "Numero massimo di alias consentiti per un nuovo amministratore" + +#: limits/app_settings.py:80 +msgid "Limits" +msgstr "Limiti" + +#: limits/forms.py:35 limits/forms.py:42 +msgid "Invalid limit" +msgstr "Limite non valido" + +#: limits/forms.py:65 +msgid "Your resources are not initialized yet" +msgstr "Le tue risorse non sono ancora inizializzate" + +#: limits/forms.py:69 +msgid "You're not allowed to define unlimited values" +msgstr "Non ti è consentito definire valori non limitati" + +#: limits/general_callbacks.py:28 +msgid "Reseller" +msgstr "Rivenditore" + +#: limits/general_callbacks.py:75 +msgid "Resources" +msgstr "Risorse" + +#: limits/lib.py:21 +#, python-format +msgid "%s: limit reached" +msgstr "%s: raggiunto il limite" + +#: limits/lib.py:31 +msgid "Not enough resources" +msgstr "Risorse insufficienti" + +#: limits/models.py:18 +msgid "Maximum number of domain administrators this user can create" +msgstr "" +"Numero massimo di amministratori di dominio che questo utente può creare" + +#: limits/models.py:22 +msgid "Maximum number of domains this user can create" +msgstr "Numero massimo di domini che questo utente può creare" + +#: limits/models.py:26 +msgid "Maximum number of domain aliases this user can create" +msgstr "Numero massimo di alias di dominio che questo utente può creare" + +#: limits/models.py:29 +msgid "Maximum number of mailboxes this user can create" +msgstr "Numero massimo di caselle di posta che questo utente può creare" + +#: limits/models.py:31 +msgid "Maximum number of mailbox aliases this user can create" +msgstr "" +"Numero massimo di alias di caselle di posta che questo utente può creare" + +#: limits/models.py:147 +msgid "undefined" +msgstr "indefinito" + +#: limits/models.py:149 +msgid "unlimited" +msgstr "senza limiti" + +#: limits/templates/limits/poolusage.html:3 +msgid "Resources usage" +msgstr "Utilizzo risorse" diff --git a/modoboa/limits/locale/nl_NL/LC_MESSAGES/django.po b/modoboa/limits/locale/nl_NL/LC_MESSAGES/django.po new file mode 100644 index 000000000..321de59ae --- /dev/null +++ b/modoboa/limits/locale/nl_NL/LC_MESSAGES/django.po @@ -0,0 +1,152 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Andrew , 2014 +# TuxBrother , 2014 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: 2014-12-31 11:37+0000\n" +"Last-Translator: TuxBrother \n" +"Language-Team: Dutch (Netherlands) (http://www.transifex.com/projects/p/" +"modoboa/language/nl_NL/)\n" +"Language: nl_NL\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: limits/app_settings.py:18 +msgid "Default limits" +msgstr "Standaard limieten" + +#: limits/app_settings.py:21 limits/models.py:17 +msgid "Domain admins" +msgstr "Domein administrators" + +#: limits/app_settings.py:24 +#, fuzzy +#| msgid "" +#| "Maximum number of allowed domain administrators for a new administrator" +msgid "" +"Maximum number of allowed domain administrators for a new administrator. (0 " +"to deny any creation, -1 to allow unlimited creations)" +msgstr "" +"Maximum aantal toegestane domein administrators voor een nieuwe administrator" + +#: limits/app_settings.py:32 limits/models.py:21 +msgid "Domains" +msgstr "Domeinen" + +#: limits/app_settings.py:35 +#, fuzzy +#| msgid "Maximum number of allowed domains for a new administrator" +msgid "" +"Maximum number of allowed domains for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "Maximum aantal toegestane domeinen voor een nieuwe administrator" + +#: limits/app_settings.py:42 limits/models.py:24 +msgid "Domain aliases" +msgstr "Domein aliassen" + +#: limits/app_settings.py:45 +#, fuzzy +#| msgid "Maximum number of allowed domain aliases for a new administrator" +msgid "" +"Maximum number of allowed domain aliases for a new administrator. (0 to deny " +"any creation, -1 to allow unlimited creations)" +msgstr "Maximum aantal domein aliassen voor een nieuwe administrator" + +#: limits/app_settings.py:53 limits/models.py:28 +msgid "Mailboxes" +msgstr "Mailboxen" + +#: limits/app_settings.py:56 +#, fuzzy +#| msgid "Maximum number of allowed mailboxes for a new administrator" +msgid "" +"Maximum number of allowed mailboxes for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "Maximum aantal toegestane mailboxen voor een nieuwe administrator" + +#: limits/app_settings.py:63 limits/models.py:30 +msgid "Mailbox aliases" +msgstr "Mailbox aliassen" + +#: limits/app_settings.py:66 +#, fuzzy +#| msgid "Maximum number of allowed aliases for a new administrator" +msgid "" +"Maximum number of allowed aliases for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "Maximum aantal aliassen voor een nieuwe administrator" + +#: limits/app_settings.py:80 +msgid "Limits" +msgstr "Limieten" + +#: limits/forms.py:35 limits/forms.py:42 +msgid "Invalid limit" +msgstr "Ongeldige limiet" + +#: limits/forms.py:65 +msgid "Your resources are not initialized yet" +msgstr "Uw capaciteiten zijn nog niet geintialiseerd" + +#: limits/forms.py:69 +msgid "You're not allowed to define unlimited values" +msgstr "U mag geen ongelimiteerde waardes opgeven" + +#: limits/general_callbacks.py:28 +msgid "Reseller" +msgstr "Verkoper" + +#: limits/general_callbacks.py:75 +msgid "Resources" +msgstr "Capaciteiten" + +#: limits/lib.py:21 +#, python-format +msgid "%s: limit reached" +msgstr "%s: limiet bereikt" + +#: limits/lib.py:31 +msgid "Not enough resources" +msgstr "Niet genoeg capaciteit" + +#: limits/models.py:18 +msgid "Maximum number of domain administrators this user can create" +msgstr "Maximum aantal domein administrators die deze gebruiker aan kan maken" + +#: limits/models.py:22 +msgid "Maximum number of domains this user can create" +msgstr "Maximum aantal domeinen die deze gebruiker aan kan maken" + +#: limits/models.py:26 +msgid "Maximum number of domain aliases this user can create" +msgstr "Maximum aantal domein-aliassen die deze gebruiker kan aanmaken" + +#: limits/models.py:29 +msgid "Maximum number of mailboxes this user can create" +msgstr "Maximum aantal mailboxen die deze gebruiker kan aanmaken" + +#: limits/models.py:31 +msgid "Maximum number of mailbox aliases this user can create" +msgstr "Maximum mailbox aliassen die deze gebruiker kan aanmken" + +#: limits/models.py:147 +msgid "undefined" +msgstr "ongedefinieerd" + +#: limits/models.py:149 +msgid "unlimited" +msgstr "ongelimiteerd" + +#: limits/templates/limits/poolusage.html:3 +msgid "Resources usage" +msgstr "Capaciteit gebruik" diff --git a/modoboa/limits/locale/pt_BR/LC_MESSAGES/django.po b/modoboa/limits/locale/pt_BR/LC_MESSAGES/django.po new file mode 100644 index 000000000..ddef6bd55 --- /dev/null +++ b/modoboa/limits/locale/pt_BR/LC_MESSAGES/django.po @@ -0,0 +1,154 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# csmk, 2014 +# Paulino Michelazzo , 2014 +# Rafael Barretto Alves , 2013,2015 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: 2015-08-10 21:56+0000\n" +"Last-Translator: Rafael Barretto Alves \n" +"Language-Team: Portuguese (Brazil) (http://www.transifex.com/tonio/modoboa/" +"language/pt_BR/)\n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: limits/app_settings.py:18 +msgid "Default limits" +msgstr "Limites padrão" + +#: limits/app_settings.py:21 limits/models.py:17 +msgid "Domain admins" +msgstr "Administradores de domínio" + +#: limits/app_settings.py:24 +msgid "" +"Maximum number of allowed domain administrators for a new administrator. (0 " +"to deny any creation, -1 to allow unlimited creations)" +msgstr "" +"Número máximo de administradores de domínio permitido para um novo " +"administrador. (0 a negar qualquer criação, -1 para permitir criações " +"ilimitadas)" + +#: limits/app_settings.py:32 limits/models.py:21 +msgid "Domains" +msgstr "Domínios" + +#: limits/app_settings.py:35 +msgid "" +"Maximum number of allowed domains for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "" +"Número máximo de domínios permitidos para um novo administrador. (0 a negar " +"qualquer criação, -1 para permitir criações ilimitadas)" + +#: limits/app_settings.py:42 limits/models.py:24 +msgid "Domain aliases" +msgstr "Alias de domínios" + +#: limits/app_settings.py:45 +msgid "" +"Maximum number of allowed domain aliases for a new administrator. (0 to deny " +"any creation, -1 to allow unlimited creations)" +msgstr "" +"O número máximo de alias de domínio permitidos para um novo administrador. " +"(0 a negar qualquer criação, -1 para permitir criações ilimitadas)" + +#: limits/app_settings.py:53 limits/models.py:28 +msgid "Mailboxes" +msgstr "Caixas de correios" + +#: limits/app_settings.py:56 +msgid "" +"Maximum number of allowed mailboxes for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "" +"Número máximo de caixas de correio permitido para um novo administrador. (0 " +"a negar qualquer criação, -1 para permitir criações ilimitadas)" + +#: limits/app_settings.py:63 limits/models.py:30 +msgid "Mailbox aliases" +msgstr "Alias de caixas de correio" + +#: limits/app_settings.py:66 +msgid "" +"Maximum number of allowed aliases for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "" +"Número máximo de alias permitidos para um novo administrador. (0 a negar " +"qualquer criação, -1 para permitir criações ilimitadas)" + +#: limits/app_settings.py:80 +msgid "Limits" +msgstr "Limites" + +#: limits/forms.py:35 limits/forms.py:42 +msgid "Invalid limit" +msgstr "Limite inválido" + +#: limits/forms.py:65 +msgid "Your resources are not initialized yet" +msgstr "Seus recursos ainda não foram inicializados" + +#: limits/forms.py:69 +msgid "You're not allowed to define unlimited values" +msgstr "Você não tem permissão para definir valores ilimitados" + +#: limits/general_callbacks.py:28 +msgid "Reseller" +msgstr "Revendedor" + +#: limits/general_callbacks.py:75 +msgid "Resources" +msgstr "Recursos" + +#: limits/lib.py:21 +#, python-format +msgid "%s: limit reached" +msgstr "%s: limite alcançado" + +#: limits/lib.py:31 +msgid "Not enough resources" +msgstr "Sem recursos suficientes" + +#: limits/models.py:18 +msgid "Maximum number of domain administrators this user can create" +msgstr "" +"Número máximo de administradores de domínios que esse usuário pode criar" + +#: limits/models.py:22 +msgid "Maximum number of domains this user can create" +msgstr "Número máximo de domínio que esse usuário pode criar" + +#: limits/models.py:26 +msgid "Maximum number of domain aliases this user can create" +msgstr "Número máximo de aliases de domínio que esse usuário pode criar" + +#: limits/models.py:29 +msgid "Maximum number of mailboxes this user can create" +msgstr "Número máximo de caixas de correios que esse usuário pode criar" + +#: limits/models.py:31 +msgid "Maximum number of mailbox aliases this user can create" +msgstr "" +"Número máximo de aliases de caixas de correio que esse usuário pode criar" + +#: limits/models.py:147 +msgid "undefined" +msgstr "indefinido" + +#: limits/models.py:149 +msgid "unlimited" +msgstr "ilimitado" + +#: limits/templates/limits/poolusage.html:3 +msgid "Resources usage" +msgstr "Uso de recursos" diff --git a/modoboa/limits/locale/pt_PT/LC_MESSAGES/django.po b/modoboa/limits/locale/pt_PT/LC_MESSAGES/django.po new file mode 100644 index 000000000..467ab1280 --- /dev/null +++ b/modoboa/limits/locale/pt_PT/LC_MESSAGES/django.po @@ -0,0 +1,160 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Damien Duponchelle , 2013 +# lusitan , 2013 +# Mike C. , 2012 +# Mike C. , 2013-2014 +# Sandra Ribeiro , 2012-2013 +# Sandra Ribeiro , 2012 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: 2014-12-29 12:51+0000\n" +"Last-Translator: Mike C. \n" +"Language-Team: Portuguese (Portugal) (http://www.transifex.com/projects/p/" +"modoboa/language/pt_PT/)\n" +"Language: pt_PT\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: limits/app_settings.py:18 +msgid "Default limits" +msgstr "Limites predefinidos" + +#: limits/app_settings.py:21 limits/models.py:17 +msgid "Domain admins" +msgstr "Administradores de domínio" + +#: limits/app_settings.py:24 +#, fuzzy +#| msgid "" +#| "Maximum number of allowed domain administrators for a new administrator" +msgid "" +"Maximum number of allowed domain administrators for a new administrator. (0 " +"to deny any creation, -1 to allow unlimited creations)" +msgstr "" +"Número máximo permitido de administradores de domínio para um novo " +"administrador" + +#: limits/app_settings.py:32 limits/models.py:21 +msgid "Domains" +msgstr "Domínios" + +#: limits/app_settings.py:35 +#, fuzzy +#| msgid "Maximum number of allowed domains for a new administrator" +msgid "" +"Maximum number of allowed domains for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "Número máximo permitido de domínios para novo administrador" + +#: limits/app_settings.py:42 limits/models.py:24 +msgid "Domain aliases" +msgstr "Aliases de domínio" + +#: limits/app_settings.py:45 +#, fuzzy +#| msgid "Maximum number of allowed domain aliases for a new administrator" +msgid "" +"Maximum number of allowed domain aliases for a new administrator. (0 to deny " +"any creation, -1 to allow unlimited creations)" +msgstr "" +"Número máximo permitido de aliases de domínio para um novo administrador" + +#: limits/app_settings.py:53 limits/models.py:28 +msgid "Mailboxes" +msgstr "Caixas de Correio" + +#: limits/app_settings.py:56 +#, fuzzy +#| msgid "Maximum number of allowed mailboxes for a new administrator" +msgid "" +"Maximum number of allowed mailboxes for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "Número máximo permitido de caixas de correio para novo administrador" + +#: limits/app_settings.py:63 limits/models.py:30 +msgid "Mailbox aliases" +msgstr "Aliases para caixas de correio" + +#: limits/app_settings.py:66 +#, fuzzy +#| msgid "Maximum number of allowed aliases for a new administrator" +msgid "" +"Maximum number of allowed aliases for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "Número máximo de aliases permitidas para um novo administrador" + +#: limits/app_settings.py:80 +msgid "Limits" +msgstr "Limites" + +#: limits/forms.py:35 limits/forms.py:42 +msgid "Invalid limit" +msgstr "Limite inválido" + +#: limits/forms.py:65 +msgid "Your resources are not initialized yet" +msgstr "Ainda não inicializou os seus conteúdos" + +#: limits/forms.py:69 +msgid "You're not allowed to define unlimited values" +msgstr "Não está autorizado a definir valores ilimitados" + +#: limits/general_callbacks.py:28 +msgid "Reseller" +msgstr "Revendedor" + +#: limits/general_callbacks.py:75 +msgid "Resources" +msgstr "Recursos" + +#: limits/lib.py:21 +#, python-format +msgid "%s: limit reached" +msgstr "%s: limite atingido" + +#: limits/lib.py:31 +msgid "Not enough resources" +msgstr "Recursos insuficientes" + +#: limits/models.py:18 +msgid "Maximum number of domain administrators this user can create" +msgstr "" +"Número máximo de administradores de domínio que este utilizador pode criar" + +#: limits/models.py:22 +msgid "Maximum number of domains this user can create" +msgstr "Número máximo de domínios que este utilizador pode criar" + +#: limits/models.py:26 +msgid "Maximum number of domain aliases this user can create" +msgstr "Número máximo de aliases de domínio que este utilizador pode criar" + +#: limits/models.py:29 +msgid "Maximum number of mailboxes this user can create" +msgstr "Número máximo de caixas de correio que este utilizador por criar" + +#: limits/models.py:31 +msgid "Maximum number of mailbox aliases this user can create" +msgstr "" +"Número máximo de aliases para caixas de correio que este utilizador por criar" + +#: limits/models.py:147 +msgid "undefined" +msgstr "indefinido" + +#: limits/models.py:149 +msgid "unlimited" +msgstr "ilimitado" + +#: limits/templates/limits/poolusage.html:3 +msgid "Resources usage" +msgstr "Utilização de recursos" diff --git a/modoboa/limits/locale/ru/LC_MESSAGES/django.po b/modoboa/limits/locale/ru/LC_MESSAGES/django.po new file mode 100644 index 000000000..8edcd3890 --- /dev/null +++ b/modoboa/limits/locale/ru/LC_MESSAGES/django.po @@ -0,0 +1,158 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: 2013-12-03 09:35+0000\n" +"Last-Translator: Antoine Nguyen \n" +"Language-Team: Russian (http://www.transifex.com/projects/p/modoboa/language/" +"ru/)\n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#: limits/app_settings.py:18 +msgid "Default limits" +msgstr "Лимиты по умолчанию" + +#: limits/app_settings.py:21 limits/models.py:17 +msgid "Domain admins" +msgstr "Администраторы домена" + +#: limits/app_settings.py:24 +#, fuzzy +#| msgid "" +#| "Maximum number of allowed domain administrators for a new administrator" +msgid "" +"Maximum number of allowed domain administrators for a new administrator. (0 " +"to deny any creation, -1 to allow unlimited creations)" +msgstr "" +"Максимальное количество администраторов домена для нового администратора" + +#: limits/app_settings.py:32 limits/models.py:21 +msgid "Domains" +msgstr "Домены" + +#: limits/app_settings.py:35 +#, fuzzy +#| msgid "Maximum number of allowed domains for a new administrator" +msgid "" +"Maximum number of allowed domains for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "Максимальное количество доменов для нового администратора" + +#: limits/app_settings.py:42 limits/models.py:24 +msgid "Domain aliases" +msgstr "Синонимы доменов" + +#: limits/app_settings.py:45 +#, fuzzy +#| msgid "Maximum number of allowed domain aliases for a new administrator" +msgid "" +"Maximum number of allowed domain aliases for a new administrator. (0 to deny " +"any creation, -1 to allow unlimited creations)" +msgstr "Максимальное количество синонимов доменов для нового администратора" + +#: limits/app_settings.py:53 limits/models.py:28 +msgid "Mailboxes" +msgstr "Почтовые ящики" + +#: limits/app_settings.py:56 +#, fuzzy +#| msgid "Maximum number of allowed mailboxes for a new administrator" +msgid "" +"Maximum number of allowed mailboxes for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "Максимальное количество почтовых ящиков для нового администратора" + +#: limits/app_settings.py:63 limits/models.py:30 +msgid "Mailbox aliases" +msgstr "Синонимы почтового ящика" + +#: limits/app_settings.py:66 +#, fuzzy +#| msgid "Maximum number of allowed aliases for a new administrator" +msgid "" +"Maximum number of allowed aliases for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "Максимальное количество синонимов для нового администратора" + +#: limits/app_settings.py:80 +msgid "Limits" +msgstr "Лимиты" + +#: limits/forms.py:35 limits/forms.py:42 +msgid "Invalid limit" +msgstr "Ошибочный лимит" + +#: limits/forms.py:65 +msgid "Your resources are not initialized yet" +msgstr "Ваши ресурсы не инициализированы" + +#: limits/forms.py:69 +msgid "You're not allowed to define unlimited values" +msgstr "Вам не позволено определять нелимитированные значения" + +#: limits/general_callbacks.py:28 +msgid "Reseller" +msgstr "Реселлер" + +#: limits/general_callbacks.py:75 +msgid "Resources" +msgstr "Ресурсы" + +#: limits/lib.py:21 +#, python-format +msgid "%s: limit reached" +msgstr "%s лимит исчерпан" + +#: limits/lib.py:31 +msgid "Not enough resources" +msgstr "Недостаточно ресурсов" + +#: limits/models.py:18 +msgid "Maximum number of domain administrators this user can create" +msgstr "" +"Максимальное количество создаваемых администраторов домена для этого " +"пользователя" + +#: limits/models.py:22 +msgid "Maximum number of domains this user can create" +msgstr "Максимальное количество создаваемых доменов для этого пользователя" + +#: limits/models.py:26 +msgid "Maximum number of domain aliases this user can create" +msgstr "" +"Максимальное количество создаваемых синонимов (алиасов) доменов для этого " +"пользователя" + +#: limits/models.py:29 +msgid "Maximum number of mailboxes this user can create" +msgstr "" +"Максимальное количество создаваемых почтовых ящиков для этого пользователя" + +#: limits/models.py:31 +msgid "Maximum number of mailbox aliases this user can create" +msgstr "" +"Максимальное количество создаваемых синонимов почтовых ящиков для этого " +"пользователя" + +#: limits/models.py:147 +msgid "undefined" +msgstr "Не определено" + +#: limits/models.py:149 +msgid "unlimited" +msgstr "Не лимитировано" + +#: limits/templates/limits/poolusage.html:3 +msgid "Resources usage" +msgstr "Использование ресурсов" diff --git a/modoboa/limits/locale/sv/LC_MESSAGES/django.po b/modoboa/limits/locale/sv/LC_MESSAGES/django.po new file mode 100644 index 000000000..b2b98a957 --- /dev/null +++ b/modoboa/limits/locale/sv/LC_MESSAGES/django.po @@ -0,0 +1,152 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Martin Persson , 2014 +# Olle Gustafsson , 2013 +# Olle Gustafsson , 2013,2015 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: 2015-01-13 15:18+0000\n" +"Last-Translator: Olle Gustafsson \n" +"Language-Team: Swedish (http://www.transifex.com/projects/p/modoboa/language/" +"sv/)\n" +"Language: sv\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: limits/app_settings.py:18 +msgid "Default limits" +msgstr "Standard gränser" + +#: limits/app_settings.py:21 limits/models.py:17 +msgid "Domain admins" +msgstr "Domänadministratörer" + +#: limits/app_settings.py:24 +#, fuzzy +#| msgid "" +#| "Maximum number of allowed domain administrators for a new administrator" +msgid "" +"Maximum number of allowed domain administrators for a new administrator. (0 " +"to deny any creation, -1 to allow unlimited creations)" +msgstr "Maximalt antal domänadministratörer den här användaren kan skapa" + +#: limits/app_settings.py:32 limits/models.py:21 +msgid "Domains" +msgstr "Domäner" + +#: limits/app_settings.py:35 +#, fuzzy +#| msgid "Maximum number of allowed domains for a new administrator" +msgid "" +"Maximum number of allowed domains for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "Maximalt antal domäner för en ny administratör" + +#: limits/app_settings.py:42 limits/models.py:24 +msgid "Domain aliases" +msgstr "Domän alias" + +#: limits/app_settings.py:45 +#, fuzzy +#| msgid "Maximum number of allowed domain aliases for a new administrator" +msgid "" +"Maximum number of allowed domain aliases for a new administrator. (0 to deny " +"any creation, -1 to allow unlimited creations)" +msgstr "Maximalt antal domän alias för en ny administratör" + +#: limits/app_settings.py:53 limits/models.py:28 +msgid "Mailboxes" +msgstr "Brevlådor" + +#: limits/app_settings.py:56 +#, fuzzy +#| msgid "Maximum number of allowed mailboxes for a new administrator" +msgid "" +"Maximum number of allowed mailboxes for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "Maximalt antal tillåtna brevlådor för en ny administratör" + +#: limits/app_settings.py:63 limits/models.py:30 +msgid "Mailbox aliases" +msgstr "Brevlåde alias" + +#: limits/app_settings.py:66 +#, fuzzy +#| msgid "Maximum number of allowed aliases for a new administrator" +msgid "" +"Maximum number of allowed aliases for a new administrator. (0 to deny any " +"creation, -1 to allow unlimited creations)" +msgstr "Maximalt antal tillåtna alias för en ny administratör" + +#: limits/app_settings.py:80 +msgid "Limits" +msgstr "Gräns" + +#: limits/forms.py:35 limits/forms.py:42 +msgid "Invalid limit" +msgstr "Ogiltig gräns" + +#: limits/forms.py:65 +msgid "Your resources are not initialized yet" +msgstr "Dina resurser har ännu inte initierats" + +#: limits/forms.py:69 +msgid "You're not allowed to define unlimited values" +msgstr "Du har inte tillåtelse att definiera obegränsat värden" + +#: limits/general_callbacks.py:28 +msgid "Reseller" +msgstr "Återförsäljare" + +#: limits/general_callbacks.py:75 +msgid "Resources" +msgstr "Resurser" + +#: limits/lib.py:21 +#, python-format +msgid "%s: limit reached" +msgstr "%s: gräns uppnådd" + +#: limits/lib.py:31 +msgid "Not enough resources" +msgstr "Inte tillräckligt med resurser" + +#: limits/models.py:18 +msgid "Maximum number of domain administrators this user can create" +msgstr "Maximalt antal domänadministratörer den här användaren kan skapa" + +#: limits/models.py:22 +msgid "Maximum number of domains this user can create" +msgstr "Maximalt antal domäner den här användaren kan skapa" + +#: limits/models.py:26 +msgid "Maximum number of domain aliases this user can create" +msgstr "Maximalt antal domänen alias den här användaren kan skapa" + +#: limits/models.py:29 +msgid "Maximum number of mailboxes this user can create" +msgstr "Maximalt antal brevlådor den här användaren kan skapa" + +#: limits/models.py:31 +msgid "Maximum number of mailbox aliases this user can create" +msgstr "Maximalt antal brevlådealias den här användaren kan skapa" + +#: limits/models.py:147 +msgid "undefined" +msgstr "odefinierad" + +#: limits/models.py:149 +msgid "unlimited" +msgstr "obegränsad" + +#: limits/templates/limits/poolusage.html:3 +msgid "Resources usage" +msgstr "Resurs användande" diff --git a/modoboa/limits/migrations/0001_initial.py b/modoboa/limits/migrations/0001_initial.py new file mode 100644 index 000000000..468235c9b --- /dev/null +++ b/modoboa/limits/migrations/0001_initial.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Limit', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=255)), + ('curvalue', models.IntegerField(default=0)), + ('maxvalue', models.IntegerField(default=-2)), + ], + options={ + 'db_table': 'limits_limit', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='LimitsPool', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('user', models.OneToOneField(to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'limits_limitspool', + }, + bases=(models.Model,), + ), + migrations.AddField( + model_name='limit', + name='pool', + field=models.ForeignKey(to='limits.LimitsPool'), + preserve_default=True, + ), + migrations.AlterUniqueTogether( + name='limit', + unique_together=set([('name', 'pool')]), + ), + ] diff --git a/modoboa/limits/migrations/__init__.py b/modoboa/limits/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/limits/models.py b/modoboa/limits/models.py new file mode 100644 index 000000000..7a139ed58 --- /dev/null +++ b/modoboa/limits/models.py @@ -0,0 +1,153 @@ +# coding: utf-8 + +"""Models for the limits extensions.""" + +from django.conf import settings +from django.db import models +from django.utils.translation import ugettext as _, ugettext_lazy + +from modoboa.lib import parameters, events +from modoboa.lib.singleton import Singleton + + +class LimitTemplates(Singleton): + + def __init__(self): + self.__templates = [ + ("domain_admins_limit", ugettext_lazy("Domain admins"), + ugettext_lazy("Maximum number of domain administrators this user " + "can create"), + 'Resellers'), + ("domains_limit", ugettext_lazy("Domains"), + ugettext_lazy("Maximum number of domains this user can create"), + 'Resellers'), + ("domain_aliases_limit", ugettext_lazy("Domain aliases"), + ugettext_lazy( + "Maximum number of domain aliases this user can create"), + 'Resellers'), + ("mailboxes_limit", ugettext_lazy("Mailboxes"), + ugettext_lazy("Maximum number of mailboxes this user can create")), + ("mailbox_aliases_limit", ugettext_lazy("Mailbox aliases"), + ugettext_lazy("Maximum number of mailbox aliases this user " + "can create")) + ] + + @property + def templates(self): + return self.__templates + \ + events.raiseQueryEvent('GetExtraLimitTemplates') + + +class LimitsPool(models.Model): + + user = models.OneToOneField(settings.AUTH_USER_MODEL) + + def create_limits(self, creator): + """Create limits for this pool. + + All limits defined into ``LimitTemplates`` will be created. If + the creator is a super administrator, the default maximum values + will be used. + + :param ``User`` creator: user creating this pool + """ + for ltpl in LimitTemplates().templates: + try: + Limit.objects.get(name=ltpl[0], pool=self) + except Limit.DoesNotExist: + maxvalue = ( + int(parameters.get_admin("DEFLT_%s" % ltpl[0].upper())) + if creator.is_superuser else 0 + ) + Limit.objects.create( + name=ltpl[0], pool=self, maxvalue=maxvalue + ) + + def getcurvalue(self, lname): + l = self.limit_set.get(name=lname) + return l.curvalue + + def getmaxvalue(self, lname): + l = self.limit_set.get(name=lname) + return l.maxvalue + + def set_maxvalue(self, lname, value): + l = self.limit_set.get(name=lname) + l.maxvalue = value + l.save() + + def inc_curvalue(self, lname, nb=1): + l = self.limit_set.get(name=lname) + l.curvalue += nb + l.save() + + def dec_curvalue(self, lname, nb=1): + l = self.limit_set.get(name=lname) + if not l.curvalue: + return + l.curvalue -= nb + l.save() + + def dec_limit(self, lname, nb=1): + l = self.limit_set.get(name=lname) + l.curvalue -= nb + if l.maxvalue > -1: + l.maxvalue -= nb + l.save() + + def inc_limit(self, lname, nb=1): + l = self.limit_set.get(name=lname) + l.curvalue += nb + if l.maxvalue > -1: + l.maxvalue += nb + l.save() + + def will_be_reached(self, lname, nb=1): + l = self.limit_set.get(name=lname) + if l.maxvalue <= -1: + return False + if l.curvalue + nb > l.maxvalue: + return True + return False + + def get_limit(self, lname): + try: + return self.limit_set.get(name=lname) + except Limit.DoesNotExist: + return None + + +class Limit(models.Model): + + name = models.CharField(max_length=255) + curvalue = models.IntegerField(default=0) + maxvalue = models.IntegerField(default=-2) + pool = models.ForeignKey(LimitsPool) + + class Meta: + unique_together = (("name", "pool"),) + + @property + def usage(self): + if self.maxvalue < 0: + return -1 + if self.maxvalue == 0: + return 0 + return int(float(self.curvalue) / self.maxvalue * 100) + + @property + def label(self): + for l in LimitTemplates().templates: + if l[0] == self.name: + return l[1] + return "" + + def __str__(self): + if self.maxvalue == -2: + return _("undefined") + if self.maxvalue == -1: + return _("unlimited") + if self.maxvalue == 0: + return "100%" + + return "%d%%" % self.usage diff --git a/modoboa/limits/templates/limits/poolusage.html b/modoboa/limits/templates/limits/poolusage.html new file mode 100644 index 000000000..1665b6d31 --- /dev/null +++ b/modoboa/limits/templates/limits/poolusage.html @@ -0,0 +1,12 @@ +{% load i18n lib_tags %} + diff --git a/modoboa/limits/tests.py b/modoboa/limits/tests.py new file mode 100644 index 000000000..7a602f4f6 --- /dev/null +++ b/modoboa/limits/tests.py @@ -0,0 +1,401 @@ +"""Test cases for the limits extension.""" + +from django.core.urlresolvers import reverse + +from modoboa.admin.app_settings import load_admin_settings +from modoboa.admin.factories import populate_database +from modoboa.admin.models import Alias, Domain +from modoboa.core import load_core_settings +from modoboa.core.factories import UserFactory +from modoboa.core.models import User +from modoboa.lib import parameters +from modoboa.lib.tests import ModoTestCase + +from .app_settings import load_limits_settings +from .models import LimitTemplates + + +class PermissionsTestCase(ModoTestCase): + + def setUp(self): + super(PermissionsTestCase, self).setUp() + populate_database() + + def test_domainadmin_deletes_reseller(self): + """Check if a domain admin can delete a reseller. + + Expected result: no. + """ + values = dict( + username="reseller@test.com", first_name="Reseller", last_name="", + password1="Toto1234", password2="Toto1234", role="Resellers", + is_active=True, email="reseller@test.com", stepid='step2' + ) + self.ajax_post(reverse("admin:account_add"), values) + account = User.objects.get(username="reseller@test.com") + self.clt.logout() + self.clt.login(username="admin@test.com", password="toto") + resp = self.ajax_post( + reverse("admin:account_delete", args=[account.id]), + {}, status=403 + ) + self.assertEqual(resp, "Permission denied") + + +class ResourceTestCase(ModoTestCase): + + def setUp(self): + """Custom setUp method. + + The 'limits' is manually loaded to ensure extra parameters + provided by 'postfix_relay_domains' are properly received. + """ + super(ResourceTestCase, self).setUp() + for tpl in LimitTemplates().templates: + parameters.save_admin( + "DEFLT_{0}".format(tpl[0].upper()), 2, + app="limits" + ) + populate_database() + + def _create_account(self, username, role='SimpleUsers', status=200): + values = dict( + username=username, first_name="Tester", last_name="Toto", + password1="Toto1234", password2="Toto1234", role=role, + quota_act=True, + is_active=True, email=username, stepid='step2', + ) + return self.ajax_post( + reverse("admin:account_add"), values, status + ) + + def _create_alias(self, email, rcpt='user@test.com', status=200): + values = dict( + address=email, recipients=rcpt, enabled=True + ) + return self.ajax_post( + reverse("admin:alias_add"), values, status + ) + + def _create_domain(self, name, status=200, withtpl=False): + values = { + "name": name, "quota": 100, "create_dom_admin": "no", + "create_aliases": "no", "stepid": 'step2', "type": "domain" + } + if withtpl: + values['create_dom_admin'] = 'yes' + values['dom_admin_username'] = 'admin' + values['create_aliases'] = 'yes' + return self.ajax_post( + reverse("admin:domain_add"), values, status + ) + + def _domain_alias_operation(self, optype, domain, name, status=200): + dom = Domain.objects.get(name=domain) + values = { + 'name': dom.name, 'quota': dom.quota, 'enabled': dom.enabled, + "type": "domain" + } + aliases = [alias.name for alias in dom.domainalias_set.all()] + if optype == 'add': + aliases.append(name) + else: + aliases.remove(name) + for cpt, alias in enumerate(aliases): + fname = 'aliases' if not cpt else 'aliases_%d' % cpt + values[fname] = alias + self.ajax_post( + reverse("admin:domain_change", args=[dom.id]), + values, status + ) + + def _check_limit(self, name, curvalue, maxvalue): + l = self.user.limitspool.get_limit('%s_limit' % name) + self.assertEqual(l.curvalue, curvalue) + self.assertEqual(l.maxvalue, maxvalue) + + +class DomainAdminTestCase(ResourceTestCase): + + def setUp(self): + super(DomainAdminTestCase, self).setUp() + self.user = User.objects.get(username='admin@test.com') + self.user.limitspool.set_maxvalue('mailboxes_limit', 2) + self.user.limitspool.set_maxvalue('mailbox_aliases_limit', 2) + self.clt.logout() + self.clt.login(username='admin@test.com', password='toto') + + def test_mailboxes_limit(self): + self._create_account('tester1@test.com') + self._check_limit('mailboxes', 1, 2) + self._create_account('tester2@test.com') + self._check_limit('mailboxes', 2, 2) + self._create_account('tester3@test.com', status=403) + self._check_limit('mailboxes', 2, 2) + self.ajax_post( + reverse('admin:account_delete', + args=[User.objects.get(username='tester2@test.com').id]), + {} + ) + self._check_limit('mailboxes', 1, 2) + + def test_aliases_limit(self): + self._create_alias('alias1@test.com') + self._check_limit('mailbox_aliases', 1, 2) + self._create_alias('alias2@test.com') + self._check_limit('mailbox_aliases', 2, 2) + self._create_alias('alias3@test.com', status=403) + self._check_limit('mailbox_aliases', 2, 2) + self.ajax_post( + reverse('admin:alias_delete') + '?selection=%d' + % Alias.objects.get(address='alias2@test.com').id, + {} + ) + self._check_limit('mailbox_aliases', 1, 2) + + def test_aliases_limit_through_account_form(self): + user = User.objects.get(username='user@test.com') + values = dict( + username=user.username, role=user.group, + is_active=user.is_active, email=user.email, quota_act=True, + aliases="alias1@test.com", aliases_1="alias2@test.com" + ) + self.ajax_post( + reverse("admin:account_change", args=[user.id]), + values + ) + Alias.objects.get(address='alias1@test.com') + self._check_limit('mailbox_aliases', 2, 2) + + +class ResellerTestCase(ResourceTestCase): + + def setUp(self): + super(ResellerTestCase, self).setUp() + self.user = UserFactory.create( + username='reseller', groups=('Resellers',) + ) + self.clt.logout() + self.clt.login(username='reseller', password='toto') + + def test_domains_limit(self): + self._create_domain('domain1.tld') + self._check_limit('domains', 1, 2) + self._create_domain('domain2.tld') + self._check_limit('domains', 2, 2) + self._create_domain('domain3.tld', 403) + self._check_limit('domains', 2, 2) + self.ajax_post( + reverse('admin:domain_delete', + args=[Domain.objects.get(name='domain2.tld').id]), + {} + ) + self._check_limit('domains', 1, 2) + + def test_domain_aliases_limit(self): + self._create_domain('pouet.com') + self._domain_alias_operation('add', 'pouet.com', 'domain-alias1.tld') + self._check_limit('domain_aliases', 1, 2) + self._domain_alias_operation('add', 'pouet.com', 'domain-alias2.tld') + self._check_limit('domain_aliases', 2, 2) + self._domain_alias_operation( + 'add', 'pouet.com', 'domain-alias3.tld', 403 + ) + self._check_limit('domain_aliases', 2, 2) + self._domain_alias_operation( + 'delete', 'pouet.com', 'domain-alias2.tld') + self._check_limit('domain_aliases', 1, 2) + + def test_domain_admins_limit(self): + self._create_domain('domain.tld') + self._create_account('admin1@domain.tld', role='DomainAdmins') + self._check_limit('domain_admins', 1, 2) + self._create_account('admin2@domain.tld', role='DomainAdmins') + self._check_limit('domain_admins', 2, 2) + resp = self._create_account( + 'admin3@domain.tld', + role='DomainAdmins', + status=400) + self.assertEqual( + resp['form_errors']['role'][0], + 'Select a valid choice. DomainAdmins is not one of the available ' + 'choices.' + ) + self._check_limit('domain_admins', 2, 2) + + self.user.limitspool.set_maxvalue('mailboxes_limit', 3) + self._create_account('user1@domain.tld') + user = User.objects.get(username='user1@domain.tld') + values = { + 'username': user.username, 'role': 'DomainAdmins', + 'quota_act': True, 'is_active': user.is_active, + 'email': user.email + } + resp = self.ajax_post( + reverse("admin:account_change", args=[user.id]), + values, status=400 + ) + self.assertEqual( + resp['form_errors']['role'][0], + 'Select a valid choice. DomainAdmins is not one of the available ' + 'choices.' + ) + self._check_limit('domain_admins', 2, 2) + + def test_domain_admin_resource_are_empty(self): + self._create_domain('domain.tld') + self._create_account('admin1@domain.tld', role='DomainAdmins') + domadmin = User.objects.get(username='admin1@domain.tld') + for l in ['mailboxes', 'mailbox_aliases']: + self.assertEqual( + domadmin.limitspool.get_limit('%s_limit' % l).maxvalue, 0 + ) + + def test_domain_admins_limit_from_domain_tpl(self): + self.user.limitspool.set_maxvalue('domains_limit', 3) + self._create_domain('domain1.tld', withtpl=True) + self._create_domain('domain2.tld', withtpl=True) + self._check_limit('domain_admins', 2, 2) + self._check_limit('domains', 2, 3) + self._create_domain('domain3.tld', status=200, withtpl=True) + self._check_limit('domain_admins', 2, 2) + self._check_limit('domains', 3, 3) + + def test_reseller_deletes_domain(self): + """Check if all resources are restored after the deletion. + """ + self._create_domain('domain.tld', withtpl=True) + dom = Domain.objects.get(name="domain.tld") + self.ajax_post( + reverse("admin:domain_delete", args=[dom.id]), + {} + ) + self._check_limit('domains', 0, 2) + self._check_limit('domain_admins', 1, 2) + self._check_limit('mailboxes', 0, 2) + self._check_limit('mailbox_aliases', 0, 2) + + def test_sadmin_removes_ownership(self): + self._create_domain('domain.tld', withtpl=True) + dom = Domain.objects.get(name="domain.tld") + self.clt.logout() + self.clt.login(username='admin', password='password') + self.ajax_get( + "{0}?domid={1}&daid={2}".format( + reverse('admin:permission_remove'), + dom.id, self.user.id + ), {} + ) + self._check_limit('domains', 0, 2) + self._check_limit('domain_admins', 0, 2) + self._check_limit('mailboxes', 0, 2) + self._check_limit('mailbox_aliases', 0, 2) + + def test_allocate_from_pool(self): + self._create_domain('domain.tld') + self._create_account('admin1@domain.tld', role='DomainAdmins') + user = User.objects.get(username='admin1@domain.tld') + + # Give 1 mailbox and 2 aliases to the admin -> should work + values = { + 'username': user.username, 'role': user.group, 'quota_act': True, + 'is_active': user.is_active, 'email': user.email, + 'mailboxes_limit': 1, 'mailbox_aliases_limit': 2 + } + self.ajax_post( + reverse("admin:account_change", args=[user.id]), + values + ) + self._check_limit('mailboxes', 1, 1) + self._check_limit('mailbox_aliases', 0, 0) + + # Delete the admin -> resources should go back to the + # reseller's pool + self.ajax_post( + reverse("admin:account_delete", args=[user.id]), + {} + ) + self._check_limit('mailboxes', 0, 2) + self._check_limit('mailbox_aliases', 0, 2) + + def test_restore_resources(self): + self._create_domain('domain.tld') + dom = Domain.objects.get(name='domain.tld') + self._create_account('admin1@domain.tld', role='DomainAdmins') + user = User.objects.get(username='admin1@domain.tld') + values = { + 'username': user.username, 'role': user.group, 'quota_act': True, + 'is_active': user.is_active, 'email': user.email, + 'mailboxes_limit': 1, 'mailbox_aliases_limit': 2 + } + self.ajax_post( + reverse("admin:account_change", args=[user.id]), + values + ) + dom.add_admin(user) + self.clt.logout() + self.clt.login(username='admin1@domain.tld', password='Toto1234') + self._create_account('user1@domain.tld') + self._create_alias('alias1@domain.tld', 'user1@domain.tld') + self._create_alias('alias2@domain.tld', 'user1@domain.tld') + self.clt.logout() + self.clt.login(username='reseller', password='toto') + # Delete the admin -> resources should go back to the + # reseller's pool + self.ajax_post( + reverse("admin:account_delete", args=[user.id]), + {} + ) + self._check_limit('mailboxes', 1, 2) + self._check_limit('mailbox_aliases', 2, 2) + + def test_change_role(self): + self._create_domain('domain.tld') + self._create_account('admin1@domain.tld', role='DomainAdmins') + user = User.objects.get(username='admin1@domain.tld') + + # Give 1 mailbox and 2 aliases to the admin -> should work + values = { + 'username': user.username, 'role': user.group, 'quota_act': True, + 'is_active': user.is_active, 'email': user.email, + 'mailboxes_limit': 1, 'mailbox_aliases_limit': 2 + } + self.ajax_post( + reverse("admin:account_change", args=[user.id]), + values + ) + self._check_limit('mailboxes', 1, 1) + self._check_limit('mailbox_aliases', 0, 0) + + # Change admin role to SimpleUser -> resources should go back + # to the reseller. + values = { + 'username': user.username, 'role': 'SimpleUsers', + 'quota_act': True, + 'is_active': user.is_active, 'email': user.email, + } + self.ajax_post( + reverse("admin:account_change", args=[user.id]), + values + ) + self._check_limit('mailboxes', 1, 2) + self._check_limit('mailbox_aliases', 0, 2) + + def test_allocate_too_much(self): + self._create_domain('domain.tld') + self._create_account('admin1@domain.tld', role='DomainAdmins') + user = User.objects.get(username='admin1@domain.tld') + + # Give 2 mailboxes and 3 aliases to the admin -> should fail. + values = { + 'username': user.username, 'role': user.group, 'quota_act': True, + 'is_active': user.is_active, 'email': user.email, + 'mailboxes_limit': 2, 'mailbox_aliases_limit': 3 + } + resp = self.ajax_post( + reverse("admin:account_change", args=[user.id]), + values, 424 + ) + self.assertEqual(resp, 'Not enough resources') + self._check_limit('mailboxes', 1, 2) + self._check_limit('mailbox_aliases', 0, 2) diff --git a/modoboa/limits/urls.py b/modoboa/limits/urls.py new file mode 100644 index 000000000..7bcb7fec0 --- /dev/null +++ b/modoboa/limits/urls.py @@ -0,0 +1,6 @@ +from django.conf.urls import patterns + +urlpatterns = patterns( + 'modoboa.limits.views', + +) diff --git a/modoboa/limits/views.py b/modoboa/limits/views.py new file mode 100644 index 000000000..57d631c3f --- /dev/null +++ b/modoboa/limits/views.py @@ -0,0 +1 @@ +# coding: utf-8 diff --git a/modoboa/relaydomains/__init__.py b/modoboa/relaydomains/__init__.py new file mode 100644 index 000000000..0b2987e99 --- /dev/null +++ b/modoboa/relaydomains/__init__.py @@ -0,0 +1 @@ +default_app_config = "modoboa.relaydomains.apps.RelayDomainsConfig" diff --git a/modoboa/relaydomains/app_settings.py b/modoboa/relaydomains/app_settings.py new file mode 100644 index 000000000..300f10983 --- /dev/null +++ b/modoboa/relaydomains/app_settings.py @@ -0,0 +1,45 @@ +""" +Relay domains settings definition. +""" + +from django import forms +from django.utils.translation import ugettext_lazy + +from modoboa.lib import events, parameters +from modoboa.lib.form_utils import SeparatorField + + +EVENTS = [ + "RelayDomainCreated", + "RelayDomainDeleted", + "RelayDomainModified", + "RelayDomainAliasCreated", + "RelayDomainAliasDeleted", + "ExtraRelayDomainForm", + "FillRelayDomainInstances" +] + + +class AdminParametersForm(parameters.AdminParametersForm): + + """Admin parameters definition.""" + + app = "relaydomains" + + general_sep = SeparatorField(label=ugettext_lazy("General")) + + master_cf_path = forms.CharField( + label=ugettext_lazy("Postfix's master.cf path"), + initial="/etc/postfix/master.cf", + help_text=ugettext_lazy('Path to the master.cf configuration file'), + widget=forms.TextInput(attrs={"class": "form-control"}) + ) + + +def load_relaydomains_settings(): + """Load application settings.""" + parameters.register( + AdminParametersForm, ugettext_lazy("Relay domains") + ) + events.declare(EVENTS) + from . import general_callbacks diff --git a/modoboa/relaydomains/apps.py b/modoboa/relaydomains/apps.py new file mode 100644 index 000000000..32c76cb2c --- /dev/null +++ b/modoboa/relaydomains/apps.py @@ -0,0 +1,14 @@ +"""AppConfig for relaydomains.""" + +from django.apps import AppConfig + + +class RelayDomainsConfig(AppConfig): + + """App configuration.""" + + name = "modoboa.relaydomains" + verbose_name = "Modoboa relay domains" + + def ready(self): + from . import handlers diff --git a/modoboa/relaydomains/factories.py b/modoboa/relaydomains/factories.py new file mode 100644 index 000000000..f46b71d0a --- /dev/null +++ b/modoboa/relaydomains/factories.py @@ -0,0 +1,33 @@ +"""Factories.""" + +import factory + + +from modoboa.admin.factories import DomainFactory +from modoboa.core.factories import PermissionFactory + +from . import models + + +class ServiceFactory(factory.DjangoModelFactory): + + """Factory to create Service instances.""" + + class Meta: + model = models.Service + django_get_or_create = ('name', ) + + name = 'dummy' + + +class RelayDomainFactory(PermissionFactory): + + """Factory to create RelayDomain instances.""" + + class Meta: + model = models.RelayDomain + + domain = factory.SubFactory(DomainFactory) + target_host = 'external.host.tld' + verify_recipients = True + service = factory.SubFactory(ServiceFactory) diff --git a/modoboa/relaydomains/forms.py b/modoboa/relaydomains/forms.py new file mode 100644 index 000000000..a9055d7e5 --- /dev/null +++ b/modoboa/relaydomains/forms.py @@ -0,0 +1,50 @@ +""" +Postfix relay domains extension forms. +""" +from django import forms +from django.utils.translation import ugettext_lazy + +from modoboa.lib.form_utils import DomainNameField, WizardStep + +from .models import RelayDomain + + +class RelayDomainWizardStep(WizardStep): + + """A custom wizard step.""" + + def check_access(self, wizard): + """Check if domain is a relay domain.""" + return wizard.steps[0].form.cleaned_data["type"] == "relaydomain" + + +class RelayDomainFormGeneral(forms.ModelForm): + + """RelayDomain form.""" + + name = DomainNameField( + label=ugettext_lazy("Name"), + help_text=ugettext_lazy("The relay domain name") + ) + + class Meta: + model = RelayDomain + exclude = ["domain", "dates"] + widgets = { + "service": forms.Select(attrs={"class": "form-control"}) + } + + def __init__(self, *args, **kwargs): + super(RelayDomainFormGeneral, self).__init__(*args, **kwargs) + self.field_widths = { + "service": 3 + } + + def save(self, *args, **kwargs): + """Custom save method.""" + domain = kwargs.get("domain") + instance = super(RelayDomainFormGeneral, self).save(commit=False) + if domain: + instance.domain = domain + instance.save() + return instance diff --git a/modoboa/relaydomains/general_callbacks.py b/modoboa/relaydomains/general_callbacks.py new file mode 100644 index 000000000..5e4dc1c3a --- /dev/null +++ b/modoboa/relaydomains/general_callbacks.py @@ -0,0 +1,121 @@ +"""General event callbacks.""" + +from django.conf import settings +from django.template import Template, Context +from django.utils.translation import ugettext_lazy, ugettext as _ + +from modoboa.core.extensions import exts_pool +from modoboa.lib import events + +from .forms import RelayDomainWizardStep, RelayDomainFormGeneral + +PERMISSIONS = { + "Resellers": [ + ("relaydomains", "relaydomain", "add_relaydomain"), + ("relaydomains", "relaydomain", "change_relaydomain"), + ("relaydomains", "relaydomain", "delete_relaydomain"), + ("relaydomains", "service", "add_service"), + ("relaydomains", "service", "change_service"), + ("relaydomains", "service", "delete_service") + ] +} + + +@events.observe("GetExtraRolePermissions") +def extra_permissions(rolename): + """Return extra permissions for Resellers.""" + return PERMISSIONS.get(rolename, []) + + +@events.observe('GetStaticContent') +def static_content(caller, st_type, user): + if caller != 'domains' or st_type != 'js': + return [] + + t = Template(""" + +""") + return [t.render(Context({'STATIC_URL': settings.STATIC_URL}))] + + +@events.observe('ExtraDomainFilters') +def extra_domain_filters(): + return ['srvfilter'] + + +@events.observe("ExtraDomainTypes") +def extra_domain_types(): + """Declare the relay domain type.""" + return [("relaydomain", _("Relay domain"))] + + +@events.observe("ExtraDomainWizardSteps") +def extra_wizard_step(): + """Return a step to configure the relay settings.""" + return [RelayDomainWizardStep( + "relay", RelayDomainFormGeneral, _("Relay domain"), + "relaydomains/relaydomain_form.html" + )] + + +@events.observe("ExtraDomainForm") +def extra_domain_form(user, domain): + """Return relay settings for domain edition.""" + if not user.has_perm("relaydomains.change_relaydomain"): + return [] + if domain.type != "relaydomain": + return [] + return [{ + "id": "relaydomain", "title": _("Relay settings"), + "cls": RelayDomainFormGeneral, + "formtpl": "relaydomains/relaydomain_form.html" + }] + + +@events.observe("FillDomainInstances") +def fill_domain_instances(user, domain, instances): + """Fill the relaydomain form with the right instance.""" + if not user.has_perm("relaydomains.change_relaydomain"): + return + if domain.type != "relaydomain": + return + instances["relaydomain"] = domain.relaydomain + + +@events.observe("ExtraDomainQsetFilters") +def extra_domain_entries(domfilter, extrafilters): + """Return extra queryset filters.""" + if domfilter is not None and domfilter and domfilter != 'relaydomain': + return {} + if "srvfilter" in extrafilters and extrafilters["srvfilter"]: + return {"relaydomain__service__name": extrafilters["srvfilter"]} + return {} + + +@events.observe("GetTagsForDomain") +def get_tags_for_domain(domain): + """Return relay domain custom tags.""" + if domain.type != "relaydomain": + return [] + return domain.relaydomain.tags + + +@events.observe('ExtraDomainImportHelp') +def extra_domain_import_help(): + return [ugettext_lazy(""" +
  • relaydomain; name; target host; service; enabled; verify recipients
  • +""")] + + +@events.observe('ImportObject') +def get_import_func(objtype): + from .lib import import_relaydomain + + if objtype == 'relaydomain': + return [import_relaydomain] + return [] diff --git a/modoboa/relaydomains/handlers.py b/modoboa/relaydomains/handlers.py new file mode 100644 index 000000000..16129dfdc --- /dev/null +++ b/modoboa/relaydomains/handlers.py @@ -0,0 +1,25 @@ +"""Django signal handlers for relaydomains.""" + +from django.dispatch import receiver + +from modoboa.admin import models as admin_models +from modoboa.admin.signals import use_external_recipients +from modoboa.lib.email_utils import split_mailbox + +from . import models + + +@receiver(use_external_recipients) +def check_relaydomain_alias(sender, **kwargs): + """Allow the creation of an alias on a relaydomain.""" + recipient = kwargs.get("recipients") + if not recipient: + return + localpart, domain = split_mailbox(recipient) + if not (models.RelayDomain.objects.select_related().filter( + domain__name=domain).exists()): + return False + if (admin_models.Mailbox.objects.select_related("domain").filter( + domain__name=domain, address=localpart).exists()): + return False + return True diff --git a/modoboa/relaydomains/lib.py b/modoboa/relaydomains/lib.py new file mode 100644 index 000000000..eaa2295de --- /dev/null +++ b/modoboa/relaydomains/lib.py @@ -0,0 +1,8 @@ +"""Internal library.""" + +from .models import RelayDomain + + +def import_relaydomain(user, row, formopts): + """Specific code for relay domains import""" + RelayDomain().from_csv(user, row) diff --git a/modoboa/relaydomains/locale/cs_CZ/LC_MESSAGES/django.po b/modoboa/relaydomains/locale/cs_CZ/LC_MESSAGES/django.po new file mode 100644 index 000000000..8ba309447 --- /dev/null +++ b/modoboa/relaydomains/locale/cs_CZ/LC_MESSAGES/django.po @@ -0,0 +1,111 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Miroslav Abrahám , 2013-2015 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: 2015-01-06 10:05+0000\n" +"Last-Translator: Miroslav Abrahám \n" +"Language-Team: Czech (Czech Republic) (http://www.transifex.com/projects/p/" +"modoboa/language/cs_CZ/)\n" +"Language: cs_CZ\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" + +#: relaydomains/app_settings.py:29 +msgid "General" +msgstr "Obecné" + +#: relaydomains/app_settings.py:32 +msgid "Postfix's master.cf path" +msgstr "Cesta k Postfix souboru master.cf" + +#: relaydomains/app_settings.py:34 +msgid "Path to the master.cf configuration file" +msgstr "Cesta ke konfiguračnímu souboru master.cf" + +#: relaydomains/app_settings.py:42 +msgid "Relay domains" +msgstr "Relay domény" + +#: relaydomains/forms.py:26 +msgid "Name" +msgstr "" + +#: relaydomains/forms.py:27 +#, fuzzy +#| msgid "The domain name" +msgid "The relay domain name" +msgstr "Doménové jméno" + +#: relaydomains/general_callbacks.py:54 relaydomains/general_callbacks.py:61 +#, fuzzy +#| msgid "Relay domains" +msgid "Relay domain" +msgstr "Relay domény" + +#: relaydomains/general_callbacks.py:74 +#, fuzzy +#| msgid "Relay domains" +msgid "Relay settings" +msgstr "Relay domény" + +#: relaydomains/general_callbacks.py:110 +#, fuzzy +#| msgid "" +#| "\n" +#| "
  • relaydomain; name; target host; service; enabled; verify " +#| "recipients
  • \n" +#| "
  • relaydomainalias; name; target; enabled
  • \n" +msgid "" +"\n" +"
  • relaydomain; name; target host; service; enabled; verify recipients
  • \n" +msgstr "" +"\n" +"
  • relay doména; název; cílový server; služba; povoleno; ověřit " +"příjemce
  • \n" +"
  • alias relay domény; název; cíl; povoleno
  • \n" + +#: relaydomains/models.py:61 +msgid "name" +msgstr "název" + +#: relaydomains/models.py:62 +msgid "The service name" +msgstr "Název služby" + +#: relaydomains/models.py:84 +msgid "target host" +msgstr "cílový server" + +#: relaydomains/models.py:85 +msgid "Remote destination of this domain" +msgstr "Vzdálený cíl této domény" + +#: relaydomains/models.py:89 +msgid "verify recipients" +msgstr "ověřit příjemce" + +#: relaydomains/models.py:90 +msgid "Check for valid recipients" +msgstr "Kontrola platných příjemců" + +#: relaydomains/models.py:103 +msgid "Relay Domain" +msgstr "Relay doména" + +#: relaydomains/models.py:130 +msgid "Invalid line" +msgstr "Neplatný řádek" + +#: relaydomains/templates/relaydomains/service_field.html:3 +msgid "Scan" +msgstr "Prohledat" diff --git a/modoboa/relaydomains/locale/cs_CZ/LC_MESSAGES/djangojs.po b/modoboa/relaydomains/locale/cs_CZ/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..de11ca8d3 --- /dev/null +++ b/modoboa/relaydomains/locale/cs_CZ/LC_MESSAGES/djangojs.po @@ -0,0 +1,24 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Miroslav Abrahám , 2013,2015 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:00+0100\n" +"PO-Revision-Date: 2015-01-06 10:05+0000\n" +"Last-Translator: Miroslav Abrahám \n" +"Language-Team: Czech (Czech Republic) (http://www.transifex.com/projects/p/" +"modoboa/language/cs_CZ/)\n" +"Language: cs_CZ\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" + +#: static/relaydomains/js/relay_domains.js:45 +msgid "Services updated" +msgstr "Služby aktualizovány" diff --git a/modoboa/relaydomains/locale/de/LC_MESSAGES/django.po b/modoboa/relaydomains/locale/de/LC_MESSAGES/django.po new file mode 100644 index 000000000..7d7bcf223 --- /dev/null +++ b/modoboa/relaydomains/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,112 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Arvedui , 2014 +# Patrick Koetter , 2010 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: 2014-12-03 11:02+0000\n" +"Last-Translator: Arvedui \n" +"Language-Team: German (http://www.transifex.com/projects/p/modoboa/language/" +"de/)\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: relaydomains/app_settings.py:29 +msgid "General" +msgstr "Allgemein" + +#: relaydomains/app_settings.py:32 +msgid "Postfix's master.cf path" +msgstr "Pfad zur Postfix-Datei 'master.cf'" + +#: relaydomains/app_settings.py:34 +msgid "Path to the master.cf configuration file" +msgstr "Pfad zur 'master.cf' Konfigurationsdatei" + +#: relaydomains/app_settings.py:42 +msgid "Relay domains" +msgstr "Relaydomain" + +#: relaydomains/forms.py:26 +msgid "Name" +msgstr "" + +#: relaydomains/forms.py:27 +#, fuzzy +#| msgid "The domain name" +msgid "The relay domain name" +msgstr "Name der Domain" + +#: relaydomains/general_callbacks.py:54 relaydomains/general_callbacks.py:61 +#, fuzzy +#| msgid "Relay domains" +msgid "Relay domain" +msgstr "Relaydomain" + +#: relaydomains/general_callbacks.py:74 +#, fuzzy +#| msgid "Relay domains" +msgid "Relay settings" +msgstr "Relaydomain" + +#: relaydomains/general_callbacks.py:110 +#, fuzzy +#| msgid "" +#| "\n" +#| "
  • relaydomain; name; target host; service; enabled; verify " +#| "recipients
  • \n" +#| "
  • relaydomainalias; name; target; enabled
  • \n" +msgid "" +"\n" +"
  • relaydomain; name; target host; service; enabled; verify recipients
  • \n" +msgstr "" +"\n" +"
  • relaydomain; name; target host; service; enabled; verify recipients
  • \n" +"
  • relaydomainalias; name; target; enabled
  • \n" + +#: relaydomains/models.py:61 +msgid "name" +msgstr "Name" + +#: relaydomains/models.py:62 +msgid "The service name" +msgstr "Der 'Service'-Name" + +#: relaydomains/models.py:84 +msgid "target host" +msgstr "Ziel-Host" + +#: relaydomains/models.py:85 +msgid "Remote destination of this domain" +msgstr "Entfernte Destination für diese Domain" + +#: relaydomains/models.py:89 +msgid "verify recipients" +msgstr "Empfänger verifizieren" + +#: relaydomains/models.py:90 +msgid "Check for valid recipients" +msgstr "Auf gültige Empfänger prüfen" + +#: relaydomains/models.py:103 +msgid "Relay Domain" +msgstr "Relaydomain" + +#: relaydomains/models.py:130 +msgid "Invalid line" +msgstr "Ungültige Zeile" + +#: relaydomains/templates/relaydomains/service_field.html:3 +msgid "Scan" +msgstr "Scanne" diff --git a/modoboa/relaydomains/locale/de/LC_MESSAGES/djangojs.po b/modoboa/relaydomains/locale/de/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..067922374 --- /dev/null +++ b/modoboa/relaydomains/locale/de/LC_MESSAGES/djangojs.po @@ -0,0 +1,25 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Arvedui , 2014 +# Patrick Koetter , 2010 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:00+0100\n" +"PO-Revision-Date: 2014-12-01 12:12+0000\n" +"Last-Translator: Arvedui \n" +"Language-Team: German (http://www.transifex.com/projects/p/modoboa/language/" +"de/)\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: static/relaydomains/js/relay_domains.js:45 +msgid "Services updated" +msgstr "'Service'-Liste aktualisiert" diff --git a/modoboa/relaydomains/locale/en/LC_MESSAGES/django.po b/modoboa/relaydomains/locale/en/LC_MESSAGES/django.po new file mode 100644 index 000000000..55b1f1b11 --- /dev/null +++ b/modoboa/relaydomains/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,93 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: relaydomains/app_settings.py:29 +msgid "General" +msgstr "" + +#: relaydomains/app_settings.py:32 +msgid "Postfix's master.cf path" +msgstr "" + +#: relaydomains/app_settings.py:34 +msgid "Path to the master.cf configuration file" +msgstr "" + +#: relaydomains/app_settings.py:42 +msgid "Relay domains" +msgstr "" + +#: relaydomains/forms.py:26 +msgid "Name" +msgstr "" + +#: relaydomains/forms.py:27 +msgid "The relay domain name" +msgstr "" + +#: relaydomains/general_callbacks.py:54 relaydomains/general_callbacks.py:61 +msgid "Relay domain" +msgstr "" + +#: relaydomains/general_callbacks.py:74 +msgid "Relay settings" +msgstr "" + +#: relaydomains/general_callbacks.py:110 +msgid "" +"\n" +"
  • relaydomain; name; target host; service; enabled; verify recipients
  • \n" +msgstr "" + +#: relaydomains/models.py:61 +msgid "name" +msgstr "" + +#: relaydomains/models.py:62 +msgid "The service name" +msgstr "" + +#: relaydomains/models.py:84 +msgid "target host" +msgstr "" + +#: relaydomains/models.py:85 +msgid "Remote destination of this domain" +msgstr "" + +#: relaydomains/models.py:89 +msgid "verify recipients" +msgstr "" + +#: relaydomains/models.py:90 +msgid "Check for valid recipients" +msgstr "" + +#: relaydomains/models.py:103 +msgid "Relay Domain" +msgstr "" + +#: relaydomains/models.py:130 +msgid "Invalid line" +msgstr "" + +#: relaydomains/templates/relaydomains/service_field.html:3 +msgid "Scan" +msgstr "" diff --git a/modoboa/relaydomains/locale/en/LC_MESSAGES/djangojs.po b/modoboa/relaydomains/locale/en/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..2ea8d4dd8 --- /dev/null +++ b/modoboa/relaydomains/locale/en/LC_MESSAGES/djangojs.po @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:00+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: static/relaydomains/js/relay_domains.js:45 +msgid "Services updated" +msgstr "" diff --git a/modoboa/relaydomains/locale/es/LC_MESSAGES/django.po b/modoboa/relaydomains/locale/es/LC_MESSAGES/django.po new file mode 100644 index 000000000..d0498b9e6 --- /dev/null +++ b/modoboa/relaydomains/locale/es/LC_MESSAGES/django.po @@ -0,0 +1,107 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Pedro Gracia , 2011-2013 +# Pedro Gracia , 2010-2011 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: 2013-10-01 09:56+0000\n" +"Last-Translator: Pedro Gracia \n" +"Language-Team: Spanish (http://www.transifex.com/projects/p/modoboa/language/" +"es/)\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: relaydomains/app_settings.py:29 +msgid "General" +msgstr "General" + +#: relaydomains/app_settings.py:32 +msgid "Postfix's master.cf path" +msgstr "" + +#: relaydomains/app_settings.py:34 +#, fuzzy +msgid "Path to the master.cf configuration file" +msgstr "Ruta del fichero de log" + +#: relaydomains/app_settings.py:42 +#, fuzzy +msgid "Relay domains" +msgstr "Nuevo dominio" + +#: relaydomains/forms.py:26 +msgid "Name" +msgstr "" + +#: relaydomains/forms.py:27 +#, fuzzy +#| msgid "The domain name" +msgid "The relay domain name" +msgstr "Nombre del dominio" + +#: relaydomains/general_callbacks.py:54 relaydomains/general_callbacks.py:61 +#, fuzzy +msgid "Relay domain" +msgstr "Nuevo dominio" + +#: relaydomains/general_callbacks.py:74 +#, fuzzy +msgid "Relay settings" +msgstr "Nuevo dominio" + +#: relaydomains/general_callbacks.py:110 +msgid "" +"\n" +"
  • relaydomain; name; target host; service; enabled; verify recipients
  • \n" +msgstr "" + +#: relaydomains/models.py:61 +msgid "name" +msgstr "nombre" + +#: relaydomains/models.py:62 +#, fuzzy +msgid "The service name" +msgstr "Nombre del alias" + +#: relaydomains/models.py:84 +#, fuzzy +msgid "target host" +msgstr "objetivo" + +#: relaydomains/models.py:85 +#, fuzzy +msgid "Remote destination of this domain" +msgstr "Marcar para activar este dominio" + +#: relaydomains/models.py:89 +#, fuzzy +msgid "verify recipients" +msgstr "Destinatarios" + +#: relaydomains/models.py:90 +msgid "Check for valid recipients" +msgstr "" + +#: relaydomains/models.py:103 +#, fuzzy +msgid "Relay Domain" +msgstr "Nuevo dominio" + +#: relaydomains/models.py:130 +msgid "Invalid line" +msgstr "Línea inválida" + +#: relaydomains/templates/relaydomains/service_field.html:3 +msgid "Scan" +msgstr "" diff --git a/modoboa/relaydomains/locale/es/LC_MESSAGES/djangojs.po b/modoboa/relaydomains/locale/es/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..0c951ea04 --- /dev/null +++ b/modoboa/relaydomains/locale/es/LC_MESSAGES/djangojs.po @@ -0,0 +1,25 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Pedro Gracia , 2011,2013 +# Pedro Gracia , 2010, 2011 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:00+0100\n" +"PO-Revision-Date: 2013-06-03 16:32+0000\n" +"Last-Translator: Pedro Gracia \n" +"Language-Team: Spanish (http://www.transifex.com/projects/p/modoboa/language/" +"es/)\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: static/relaydomains/js/relay_domains.js:45 +msgid "Services updated" +msgstr "" diff --git a/modoboa/relaydomains/locale/fr/LC_MESSAGES/django.po b/modoboa/relaydomains/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 000000000..50a0a21e7 --- /dev/null +++ b/modoboa/relaydomains/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,109 @@ +# Modoboa french translation +# Copyright (C) 2009-2013 +# This file is distributed under the same license as the Modoboa package. +# +# Antoine Nguyen , 2009-2013. +msgid "" +msgstr "" +"Project-Id-Version: modoboa-1.1.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: 2011-01-07 10:00+100\n" +"Last-Translator: Antoine Nguyen \n" +"Language-Team: FR \n" +"Language: Français\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: relaydomains/app_settings.py:29 +msgid "General" +msgstr "Général" + +#: relaydomains/app_settings.py:32 +msgid "Postfix's master.cf path" +msgstr "Chemin d'accès vers le fichier master.cf de Postfix" + +#: relaydomains/app_settings.py:34 +msgid "Path to the master.cf configuration file" +msgstr "Chemin d'accès vers le fichier de configuration master.cf" + +#: relaydomains/app_settings.py:42 +msgid "Relay domains" +msgstr "Domaines relais" + +#: relaydomains/forms.py:26 +msgid "Name" +msgstr "" + +#: relaydomains/forms.py:27 +#, fuzzy +#| msgid "The domain name" +msgid "The relay domain name" +msgstr "Le nom du domaine" + +#: relaydomains/general_callbacks.py:54 relaydomains/general_callbacks.py:61 +#, fuzzy +#| msgid "Relay domains" +msgid "Relay domain" +msgstr "Domaines relais" + +#: relaydomains/general_callbacks.py:74 +#, fuzzy +#| msgid "Relay domains" +msgid "Relay settings" +msgstr "Domaines relais" + +#: relaydomains/general_callbacks.py:110 +#, fuzzy +#| msgid "" +#| "\n" +#| "
  • relaydomain; name; target host; service; enabled; verify " +#| "recipients
  • \n" +#| "
  • relaydomainalias; name; target; enabled
  • \n" +msgid "" +"\n" +"
  • relaydomain; name; target host; service; enabled; verify recipients
  • \n" +msgstr "" +"\n" +"
  • relaydomain; nom; hôte cible; service; activé; vérification des " +"destinataires
  • \n" +"
  • relaydomainalias; nom; cible; activé
  • \n" + +#: relaydomains/models.py:61 +msgid "name" +msgstr "Nom" + +#: relaydomains/models.py:62 +msgid "The service name" +msgstr "Le nom du service" + +#: relaydomains/models.py:84 +msgid "target host" +msgstr "hôte cible" + +#: relaydomains/models.py:85 +msgid "Remote destination of this domain" +msgstr "Adresse de destination de ce domaine" + +#: relaydomains/models.py:89 +msgid "verify recipients" +msgstr "Vérifier les destinataires" + +#: relaydomains/models.py:90 +msgid "Check for valid recipients" +msgstr "Vérifie si les destinataires sont valides (existent)" + +#: relaydomains/models.py:103 +msgid "Relay Domain" +msgstr "Domaine relais" + +#: relaydomains/models.py:130 +msgid "Invalid line" +msgstr "Ligne invalide" + +#: relaydomains/templates/relaydomains/service_field.html:3 +msgid "Scan" +msgstr "Scanner" diff --git a/modoboa/relaydomains/locale/fr/LC_MESSAGES/djangojs.po b/modoboa/relaydomains/locale/fr/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..19419beed --- /dev/null +++ b/modoboa/relaydomains/locale/fr/LC_MESSAGES/djangojs.po @@ -0,0 +1,22 @@ +# Modoboa french translation +# Copyright (C) 2009-2013 +# This file is distributed under the same license as the Modoboa package. +# Antoine Nguyen , 2009-2013. +# +msgid "" +msgstr "" +"Project-Id-Version: modoboa-1.0.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:00+0100\n" +"PO-Revision-Date: 2011-01-07 10:00+0100\n" +"Last-Translator: Antoine Nguyen \n" +"Language-Team: FR \n" +"Language: Français\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: static/relaydomains/js/relay_domains.js:45 +msgid "Services updated" +msgstr "Services mis à jour" diff --git a/modoboa/relaydomains/locale/it/LC_MESSAGES/django.po b/modoboa/relaydomains/locale/it/LC_MESSAGES/django.po new file mode 100644 index 000000000..2e6be7dee --- /dev/null +++ b/modoboa/relaydomains/locale/it/LC_MESSAGES/django.po @@ -0,0 +1,109 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Giancarlo Gaifas , 2013. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: 2013-12-23 00:56+0100\n" +"Last-Translator: Alberto Berti \n" +"Language-Team: ITALIANO \n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: relaydomains/app_settings.py:29 +msgid "General" +msgstr "Generale" + +#: relaydomains/app_settings.py:32 +msgid "Postfix's master.cf path" +msgstr "Percorso del file master.cf di Postfix" + +#: relaydomains/app_settings.py:34 +msgid "Path to the master.cf configuration file" +msgstr "Percorso del file di configurazione master.cf" + +#: relaydomains/app_settings.py:42 +msgid "Relay domains" +msgstr "Domini relè" + +#: relaydomains/forms.py:26 +msgid "Name" +msgstr "" + +#: relaydomains/forms.py:27 +#, fuzzy +#| msgid "The domain name" +msgid "The relay domain name" +msgstr "Il nome del dominio" + +#: relaydomains/general_callbacks.py:54 relaydomains/general_callbacks.py:61 +#, fuzzy +#| msgid "Relay domains" +msgid "Relay domain" +msgstr "Domini relè" + +#: relaydomains/general_callbacks.py:74 +#, fuzzy +#| msgid "Relay domains" +msgid "Relay settings" +msgstr "Domini relè" + +#: relaydomains/general_callbacks.py:110 +#, fuzzy +#| msgid "" +#| "\n" +#| "
  • relaydomain; name; target host; service; enabled; verify " +#| "recipients
  • \n" +#| "
  • relaydomainalias; name; target; enabled
  • \n" +msgid "" +"\n" +"
  • relaydomain; name; target host; service; enabled; verify recipients
  • \n" +msgstr "" +"\n" +"
  • dominio relè; nome; host di destinazione; servizio; abilitato; " +"verifica caselle
  • \n" +"
  • alias dominio relè; nome; destinazione; abilitato
  • \n" + +#: relaydomains/models.py:61 +msgid "name" +msgstr "nome" + +#: relaydomains/models.py:62 +msgid "The service name" +msgstr "Il nome del servizio" + +#: relaydomains/models.py:84 +msgid "target host" +msgstr "destinazione" + +#: relaydomains/models.py:85 +msgid "Remote destination of this domain" +msgstr "Destinazione remota per questo dominio" + +#: relaydomains/models.py:89 +msgid "verify recipients" +msgstr "Verifica destinatari" + +#: relaydomains/models.py:90 +msgid "Check for valid recipients" +msgstr "Verifica validità destinatari" + +#: relaydomains/models.py:103 +msgid "Relay Domain" +msgstr "Dominio relè" + +#: relaydomains/models.py:130 +msgid "Invalid line" +msgstr "Riga non valida" + +#: relaydomains/templates/relaydomains/service_field.html:3 +msgid "Scan" +msgstr "Cerca" diff --git a/modoboa/relaydomains/locale/it/LC_MESSAGES/djangojs.po b/modoboa/relaydomains/locale/it/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..cd4795f9c --- /dev/null +++ b/modoboa/relaydomains/locale/it/LC_MESSAGES/djangojs.po @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:00+0100\n" +"PO-Revision-Date: 2013-12-23 00:58+0100\n" +"Last-Translator: Alberto Berti \n" +"Language-Team: Italiano \n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: static/relaydomains/js/relay_domains.js:45 +msgid "Services updated" +msgstr "Servizi aggiornati" diff --git a/modoboa/relaydomains/locale/nl_NL/LC_MESSAGES/django.po b/modoboa/relaydomains/locale/nl_NL/LC_MESSAGES/django.po new file mode 100644 index 000000000..f1ea6af46 --- /dev/null +++ b/modoboa/relaydomains/locale/nl_NL/LC_MESSAGES/django.po @@ -0,0 +1,112 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Andrew , 2014 +# TuxBrother , 2014 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: 2014-12-31 11:37+0000\n" +"Last-Translator: TuxBrother \n" +"Language-Team: Dutch (Netherlands) (http://www.transifex.com/projects/p/" +"modoboa/language/nl_NL/)\n" +"Language: nl_NL\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: relaydomains/app_settings.py:29 +msgid "General" +msgstr "Algemeen" + +#: relaydomains/app_settings.py:32 +msgid "Postfix's master.cf path" +msgstr "Map waarin Postfix's master.cf staat" + +#: relaydomains/app_settings.py:34 +msgid "Path to the master.cf configuration file" +msgstr "Locatie van Postfix's master.cf" + +#: relaydomains/app_settings.py:42 +msgid "Relay domains" +msgstr "Doorstuur domeinen" + +#: relaydomains/forms.py:26 +msgid "Name" +msgstr "" + +#: relaydomains/forms.py:27 +#, fuzzy +#| msgid "The domain name" +msgid "The relay domain name" +msgstr "De domeinnaam" + +#: relaydomains/general_callbacks.py:54 relaydomains/general_callbacks.py:61 +#, fuzzy +#| msgid "Relay domains" +msgid "Relay domain" +msgstr "Doorstuur domeinen" + +#: relaydomains/general_callbacks.py:74 +#, fuzzy +#| msgid "Relay domains" +msgid "Relay settings" +msgstr "Doorstuur domeinen" + +#: relaydomains/general_callbacks.py:110 +#, fuzzy +#| msgid "" +#| "\n" +#| "
  • relaydomain; name; target host; service; enabled; verify " +#| "recipients
  • \n" +#| "
  • relaydomainalias; name; target; enabled
  • \n" +msgid "" +"\n" +"
  • relaydomain; name; target host; service; enabled; verify recipients
  • \n" +msgstr "" +"\n" +"
  • relaydomain; name; target host; service; enabled; verify recipients
  • \n" +"
  • relaydomainalias; name; target; enabled
  • \n" + +#: relaydomains/models.py:61 +msgid "name" +msgstr "naam" + +#: relaydomains/models.py:62 +msgid "The service name" +msgstr "De servicenaam" + +#: relaydomains/models.py:84 +msgid "target host" +msgstr "doel host" + +#: relaydomains/models.py:85 +msgid "Remote destination of this domain" +msgstr "Externe bestemming van dit domein" + +#: relaydomains/models.py:89 +msgid "verify recipients" +msgstr "controleer ontvangers" + +#: relaydomains/models.py:90 +msgid "Check for valid recipients" +msgstr "Controleer geldigheid ontvangers" + +#: relaydomains/models.py:103 +msgid "Relay Domain" +msgstr "Doorstuur Domein" + +#: relaydomains/models.py:130 +msgid "Invalid line" +msgstr "Verkeerde regel" + +#: relaydomains/templates/relaydomains/service_field.html:3 +msgid "Scan" +msgstr "Scan" diff --git a/modoboa/relaydomains/locale/nl_NL/LC_MESSAGES/djangojs.po b/modoboa/relaydomains/locale/nl_NL/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..22cca2e2e --- /dev/null +++ b/modoboa/relaydomains/locale/nl_NL/LC_MESSAGES/djangojs.po @@ -0,0 +1,25 @@ +# 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 +# TuxBrother , 2014 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:00+0100\n" +"PO-Revision-Date: 2014-12-31 11:42+0000\n" +"Last-Translator: TuxBrother \n" +"Language-Team: Dutch (Netherlands) (http://www.transifex.com/projects/p/" +"modoboa/language/nl_NL/)\n" +"Language: nl_NL\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: static/relaydomains/js/relay_domains.js:45 +msgid "Services updated" +msgstr "Services upgedate" diff --git a/modoboa/relaydomains/locale/pt_BR/LC_MESSAGES/django.po b/modoboa/relaydomains/locale/pt_BR/LC_MESSAGES/django.po new file mode 100644 index 000000000..a5d4d8918 --- /dev/null +++ b/modoboa/relaydomains/locale/pt_BR/LC_MESSAGES/django.po @@ -0,0 +1,113 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# csmk, 2014 +# Paulino Michelazzo , 2014 +# Rafael Barretto Alves , 2013 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: 2014-12-13 09:10+0000\n" +"Last-Translator: Paulino Michelazzo \n" +"Language-Team: Portuguese (Brazil) (http://www.transifex.com/projects/p/" +"modoboa/language/pt_BR/)\n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: relaydomains/app_settings.py:29 +msgid "General" +msgstr "Geral" + +#: relaydomains/app_settings.py:32 +msgid "Postfix's master.cf path" +msgstr "Caminho do master.cf do Postfix" + +#: relaydomains/app_settings.py:34 +msgid "Path to the master.cf configuration file" +msgstr "Caminho para o arquivo de configuração master.cf" + +#: relaydomains/app_settings.py:42 +msgid "Relay domains" +msgstr "Domínios de relay" + +#: relaydomains/forms.py:26 +msgid "Name" +msgstr "" + +#: relaydomains/forms.py:27 +#, fuzzy +#| msgid "The domain name" +msgid "The relay domain name" +msgstr "Nome do domínio" + +#: relaydomains/general_callbacks.py:54 relaydomains/general_callbacks.py:61 +#, fuzzy +#| msgid "Relay domains" +msgid "Relay domain" +msgstr "Domínios de relay" + +#: relaydomains/general_callbacks.py:74 +#, fuzzy +#| msgid "Relay domains" +msgid "Relay settings" +msgstr "Domínios de relay" + +#: relaydomains/general_callbacks.py:110 +#, fuzzy +#| msgid "" +#| "\n" +#| "
  • relaydomain; name; target host; service; enabled; verify " +#| "recipients
  • \n" +#| "
  • relaydomainalias; name; target; enabled
  • \n" +msgid "" +"\n" +"
  • relaydomain; name; target host; service; enabled; verify recipients
  • \n" +msgstr "" +"\n" +"
  • relaydomain; name; target host; service; enabled; verify recipients
  • \n" +"
  • relaydomainalias; name; target; enabled
  • \n" + +#: relaydomains/models.py:61 +msgid "name" +msgstr "nome" + +#: relaydomains/models.py:62 +msgid "The service name" +msgstr "O nome do serviço" + +#: relaydomains/models.py:84 +msgid "target host" +msgstr "host de destino" + +#: relaydomains/models.py:85 +msgid "Remote destination of this domain" +msgstr "Destino remoto para este domínio" + +#: relaydomains/models.py:89 +msgid "verify recipients" +msgstr "verificar destinatários" + +#: relaydomains/models.py:90 +msgid "Check for valid recipients" +msgstr "Verificar destinatários válidos" + +#: relaydomains/models.py:103 +msgid "Relay Domain" +msgstr "Domínio de Relay" + +#: relaydomains/models.py:130 +msgid "Invalid line" +msgstr "Linha inválida" + +#: relaydomains/templates/relaydomains/service_field.html:3 +msgid "Scan" +msgstr "Scan" diff --git a/modoboa/relaydomains/locale/pt_BR/LC_MESSAGES/djangojs.po b/modoboa/relaydomains/locale/pt_BR/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..2eff1e611 --- /dev/null +++ b/modoboa/relaydomains/locale/pt_BR/LC_MESSAGES/djangojs.po @@ -0,0 +1,24 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Paulino Michelazzo , 2014 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:00+0100\n" +"PO-Revision-Date: 2014-12-13 09:14+0000\n" +"Last-Translator: Paulino Michelazzo \n" +"Language-Team: Portuguese (Brazil) (http://www.transifex.com/projects/p/" +"modoboa/language/pt_BR/)\n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: static/relaydomains/js/relay_domains.js:45 +msgid "Services updated" +msgstr "Serviços atualizados" diff --git a/modoboa/relaydomains/locale/pt_PT/LC_MESSAGES/django.po b/modoboa/relaydomains/locale/pt_PT/LC_MESSAGES/django.po new file mode 100644 index 000000000..56914c483 --- /dev/null +++ b/modoboa/relaydomains/locale/pt_PT/LC_MESSAGES/django.po @@ -0,0 +1,104 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Damien Duponchelle , 2013 +# lusitan , 2013 +# Mike C. , 2012 +# Mike C. , 2013-2014 +# Sandra Ribeiro , 2012-2013 +# Sandra Ribeiro , 2012 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: 2014-12-29 12:51+0000\n" +"Last-Translator: Mike C. \n" +"Language-Team: Portuguese (Portugal) (http://www.transifex.com/projects/p/" +"modoboa/language/pt_PT/)\n" +"Language: pt_PT\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: relaydomains/app_settings.py:29 +msgid "General" +msgstr "Geral" + +#: relaydomains/app_settings.py:32 +msgid "Postfix's master.cf path" +msgstr "Caminho para para o ficheiro master.cf Postfix" + +#: relaydomains/app_settings.py:34 +msgid "Path to the master.cf configuration file" +msgstr "Caminho para o ficheiro de configuração master.cf" + +#: relaydomains/app_settings.py:42 +msgid "Relay domains" +msgstr "" + +#: relaydomains/forms.py:26 +msgid "Name" +msgstr "" + +#: relaydomains/forms.py:27 +#, fuzzy +#| msgid "The domain name" +msgid "The relay domain name" +msgstr "O nome do domínio" + +#: relaydomains/general_callbacks.py:54 relaydomains/general_callbacks.py:61 +#, fuzzy +#| msgid "domain" +msgid "Relay domain" +msgstr "domínio" + +#: relaydomains/general_callbacks.py:74 +msgid "Relay settings" +msgstr "" + +#: relaydomains/general_callbacks.py:110 +msgid "" +"\n" +"
  • relaydomain; name; target host; service; enabled; verify recipients
  • \n" +msgstr "" + +#: relaydomains/models.py:61 +msgid "name" +msgstr "nome" + +#: relaydomains/models.py:62 +msgid "The service name" +msgstr "O nome do serviço" + +#: relaydomains/models.py:84 +msgid "target host" +msgstr "endereço de destino" + +#: relaydomains/models.py:85 +msgid "Remote destination of this domain" +msgstr "Endereço de destino remoto deste domínio" + +#: relaydomains/models.py:89 +msgid "verify recipients" +msgstr "verificar destinatários" + +#: relaydomains/models.py:90 +msgid "Check for valid recipients" +msgstr "Analisar se os destinatáris são válidos" + +#: relaydomains/models.py:103 +msgid "Relay Domain" +msgstr "" + +#: relaydomains/models.py:130 +msgid "Invalid line" +msgstr "Linha inválida" + +#: relaydomains/templates/relaydomains/service_field.html:3 +msgid "Scan" +msgstr "Analisar" diff --git a/modoboa/relaydomains/locale/pt_PT/LC_MESSAGES/djangojs.po b/modoboa/relaydomains/locale/pt_PT/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..417e9d10d --- /dev/null +++ b/modoboa/relaydomains/locale/pt_PT/LC_MESSAGES/djangojs.po @@ -0,0 +1,29 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# lusitan , 2013 +# lusitan , 2013 +# Mike C. , 2012 +# Mike C. , 2012 +# Mike C. , 2014 +# Sandra Ribeiro , 2013 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:00+0100\n" +"PO-Revision-Date: 2014-12-29 12:08+0000\n" +"Last-Translator: Mike C. \n" +"Language-Team: Portuguese (Portugal) (http://www.transifex.com/projects/p/" +"modoboa/language/pt_PT/)\n" +"Language: pt_PT\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: static/relaydomains/js/relay_domains.js:45 +msgid "Services updated" +msgstr "Atualizados os serviços" diff --git a/modoboa/relaydomains/locale/ru/LC_MESSAGES/django.po b/modoboa/relaydomains/locale/ru/LC_MESSAGES/django.po new file mode 100644 index 000000000..ebb35bb52 --- /dev/null +++ b/modoboa/relaydomains/locale/ru/LC_MESSAGES/django.po @@ -0,0 +1,97 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: 2013-12-03 09:35+0000\n" +"Last-Translator: Antoine Nguyen \n" +"Language-Team: Russian (http://www.transifex.com/projects/p/modoboa/language/" +"ru/)\n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#: relaydomains/app_settings.py:29 +msgid "General" +msgstr "Основные" + +#: relaydomains/app_settings.py:32 +msgid "Postfix's master.cf path" +msgstr "" + +#: relaydomains/app_settings.py:34 +msgid "Path to the master.cf configuration file" +msgstr "" + +#: relaydomains/app_settings.py:42 +msgid "Relay domains" +msgstr "" + +#: relaydomains/forms.py:26 +msgid "Name" +msgstr "" + +#: relaydomains/forms.py:27 +#, fuzzy +#| msgid "The domain name" +msgid "The relay domain name" +msgstr "Имя домена" + +#: relaydomains/general_callbacks.py:54 relaydomains/general_callbacks.py:61 +msgid "Relay domain" +msgstr "" + +#: relaydomains/general_callbacks.py:74 +msgid "Relay settings" +msgstr "" + +#: relaydomains/general_callbacks.py:110 +msgid "" +"\n" +"
  • relaydomain; name; target host; service; enabled; verify recipients
  • \n" +msgstr "" + +#: relaydomains/models.py:61 +msgid "name" +msgstr "имя" + +#: relaydomains/models.py:62 +msgid "The service name" +msgstr "" + +#: relaydomains/models.py:84 +msgid "target host" +msgstr "" + +#: relaydomains/models.py:85 +msgid "Remote destination of this domain" +msgstr "" + +#: relaydomains/models.py:89 +msgid "verify recipients" +msgstr "" + +#: relaydomains/models.py:90 +msgid "Check for valid recipients" +msgstr "" + +#: relaydomains/models.py:103 +msgid "Relay Domain" +msgstr "" + +#: relaydomains/models.py:130 +msgid "Invalid line" +msgstr "Ошибочная строка" + +#: relaydomains/templates/relaydomains/service_field.html:3 +msgid "Scan" +msgstr "" diff --git a/modoboa/relaydomains/locale/ru/LC_MESSAGES/djangojs.po b/modoboa/relaydomains/locale/ru/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..06da1fd5a --- /dev/null +++ b/modoboa/relaydomains/locale/ru/LC_MESSAGES/djangojs.po @@ -0,0 +1,24 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:00+0100\n" +"PO-Revision-Date: 2013-12-03 09:35+0000\n" +"Last-Translator: Antoine Nguyen \n" +"Language-Team: Russian (http://www.transifex.com/projects/p/modoboa/language/" +"ru/)\n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#: static/relaydomains/js/relay_domains.js:45 +msgid "Services updated" +msgstr "" diff --git a/modoboa/relaydomains/locale/sv/LC_MESSAGES/django.po b/modoboa/relaydomains/locale/sv/LC_MESSAGES/django.po new file mode 100644 index 000000000..ec324dbb6 --- /dev/null +++ b/modoboa/relaydomains/locale/sv/LC_MESSAGES/django.po @@ -0,0 +1,113 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Martin Persson , 2014 +# Olle Gustafsson , 2013 +# Olle Gustafsson , 2013,2015 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:24+0100\n" +"PO-Revision-Date: 2015-01-13 15:18+0000\n" +"Last-Translator: Olle Gustafsson \n" +"Language-Team: Swedish (http://www.transifex.com/projects/p/modoboa/language/" +"sv/)\n" +"Language: sv\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: relaydomains/app_settings.py:29 +msgid "General" +msgstr "Generellt" + +#: relaydomains/app_settings.py:32 +msgid "Postfix's master.cf path" +msgstr "Sökväg till Postfix master.cf" + +#: relaydomains/app_settings.py:34 +msgid "Path to the master.cf configuration file" +msgstr "Sökväg till master.cf konfigureringsfil" + +#: relaydomains/app_settings.py:42 +msgid "Relay domains" +msgstr "Relay domän" + +#: relaydomains/forms.py:26 +msgid "Name" +msgstr "" + +#: relaydomains/forms.py:27 +#, fuzzy +#| msgid "The domain name" +msgid "The relay domain name" +msgstr "Domännamnet" + +#: relaydomains/general_callbacks.py:54 relaydomains/general_callbacks.py:61 +#, fuzzy +#| msgid "Relay domains" +msgid "Relay domain" +msgstr "Relay domän" + +#: relaydomains/general_callbacks.py:74 +#, fuzzy +#| msgid "Relay domains" +msgid "Relay settings" +msgstr "Relay domän" + +#: relaydomains/general_callbacks.py:110 +#, fuzzy +#| msgid "" +#| "\n" +#| "
  • relaydomain; name; target host; service; enabled; verify " +#| "recipients
  • \n" +#| "
  • relaydomainalias; name; target; enabled
  • \n" +msgid "" +"\n" +"
  • relaydomain; name; target host; service; enabled; verify recipients
  • \n" +msgstr "" +"\n" +"
  • relaydomain; name; target host; service; enabled; verify recipients
  • \n" +"
  • relaydomainalias; name; target; enabled
  • \n" + +#: relaydomains/models.py:61 +msgid "name" +msgstr "namn" + +#: relaydomains/models.py:62 +msgid "The service name" +msgstr "Tjänstens namn" + +#: relaydomains/models.py:84 +msgid "target host" +msgstr "målvärd" + +#: relaydomains/models.py:85 +msgid "Remote destination of this domain" +msgstr "Fjärrmål för den här domänen" + +#: relaydomains/models.py:89 +msgid "verify recipients" +msgstr "verifiera mottagare" + +#: relaydomains/models.py:90 +msgid "Check for valid recipients" +msgstr "Kontrollera giltiga mottagare" + +#: relaydomains/models.py:103 +msgid "Relay Domain" +msgstr "Relay domän" + +#: relaydomains/models.py:130 +msgid "Invalid line" +msgstr "Ogiltig rad" + +#: relaydomains/templates/relaydomains/service_field.html:3 +msgid "Scan" +msgstr "Skanna" diff --git a/modoboa/relaydomains/locale/sv/LC_MESSAGES/djangojs.po b/modoboa/relaydomains/locale/sv/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..0671422dc --- /dev/null +++ b/modoboa/relaydomains/locale/sv/LC_MESSAGES/djangojs.po @@ -0,0 +1,25 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Olle Gustafsson , 2013 +# Olle Gustafsson , 2013,2015 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-09 18:00+0100\n" +"PO-Revision-Date: 2015-01-13 15:21+0000\n" +"Last-Translator: Olle Gustafsson \n" +"Language-Team: Swedish (http://www.transifex.com/projects/p/modoboa/language/" +"sv/)\n" +"Language: sv\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: static/relaydomains/js/relay_domains.js:45 +msgid "Services updated" +msgstr "Tjänster uppdaterade" diff --git a/modoboa/relaydomains/migrations/0001_initial.py b/modoboa/relaydomains/migrations/0001_initial.py new file mode 100644 index 000000000..0f6bff791 --- /dev/null +++ b/modoboa/relaydomains/migrations/0001_initial.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('admin', '0002_migrate_from_modoboa_admin'), + ] + + operations = [ + migrations.CreateModel( + name='RelayDomain', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('target_host', models.CharField(help_text='Remote destination of this domain', max_length=255, verbose_name='target host')), + ('verify_recipients', models.BooleanField(default=False, help_text='Check for valid recipients', verbose_name='verify recipients')), + ('dates', models.ForeignKey(to='admin.ObjectDates')), + ('domain', models.OneToOneField(null=True, to='admin.Domain')), + ], + options={ + 'ordering': ['domain__name'], + 'db_table': 'postfix_relay_domains_relaydomain', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Service', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(help_text='The service name', unique=True, max_length=100, verbose_name='name')), + ], + options={ + 'db_table': 'postfix_relay_domains_service', + }, + bases=(models.Model,), + ), + migrations.AddField( + model_name='relaydomain', + name='service', + field=models.ForeignKey(to='relaydomains.Service'), + preserve_default=True, + ), + ] diff --git a/modoboa/relaydomains/migrations/0002_migrate_from_modoboa_admin_relaydomains.py b/modoboa/relaydomains/migrations/0002_migrate_from_modoboa_admin_relaydomains.py new file mode 100644 index 000000000..e100533fc --- /dev/null +++ b/modoboa/relaydomains/migrations/0002_migrate_from_modoboa_admin_relaydomains.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('relaydomains', '0001_initial'), + ] + + operations = [ + ] diff --git a/modoboa/relaydomains/migrations/__init__.py b/modoboa/relaydomains/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/relaydomains/models.py b/modoboa/relaydomains/models.py new file mode 100644 index 000000000..751328718 --- /dev/null +++ b/modoboa/relaydomains/models.py @@ -0,0 +1,143 @@ +"""Relay domain related models.""" + +from django.db import models +from django.db.models.manager import Manager +from django.utils.translation import ugettext as _, ugettext_lazy + +import reversion + +from modoboa.admin import models as admin_models +from modoboa.lib import parameters +from modoboa.lib.exceptions import BadRequest + + +class RelayDomainManager(Manager): + + def get_for_admin(self, admin): + """Return the relay domains belonging to this admin. + + The result is a ``QuerySet`` object, so this function can be used + to fill ``ModelChoiceField`` objects. + """ + if admin.is_superuser: + return self.get_query_set() + return self.get_query_set().filter(owners__user=admin) + + +class ServiceManager(Manager): + + def load_from_master_cf(self): + """Load services from the master.cf file. + + Parse the configuration file to update the service table. New + entries are saved and outdated ones (ie. present in the + database but not in the file) are removed. + """ + with open(parameters.get_admin('MASTER_CF_PATH')) as fp: + content = fp.read() + services = [] + for line in content.split('\n'): + if not line or line.startswith('#'): + continue + parts = line.strip().split() + if len(parts) != 8: + continue + if parts[7] != 'smtp': + continue + srv, created = self.get_or_create(name=parts[0]) + services.append(parts[0]) + to_delete = [] + for service in self.all(): + if service.name not in services: + to_delete.append(service.name) + Service.objects.filter(name__in=to_delete).delete() + + +class Service(models.Model): + + """Postfix service.""" + + name = models.CharField( + ugettext_lazy('name'), max_length=100, unique=True, + help_text=ugettext_lazy('The service name') + ) + + objects = ServiceManager() + + class Meta: + db_table = "postfix_relay_domains_service" + + def __str__(self): + return self.name + + +class RelayDomain(admin_models.AdminObject): + + """Relay domain. + + A relay domain differs from a usual domaine because its final + destination is not reached yet. It must be accepted by the MTA but + it will then be transfered to another one. + """ + domain = models.OneToOneField("admin.Domain", null=True) + target_host = models.CharField( + ugettext_lazy('target host'), max_length=255, + help_text=ugettext_lazy('Remote destination of this domain') + ) + service = models.ForeignKey(Service) + verify_recipients = models.BooleanField( + ugettext_lazy('verify recipients'), + help_text=ugettext_lazy('Check for valid recipients'), + default=False + ) + + objects = RelayDomainManager() + + class Meta: + ordering = ["domain__name"] + db_table = "postfix_relay_domains_relaydomain" + + @property + def tags(self): + return [ + {"name": "relaydomain", "label": _("Relay Domain"), "type": "dom"}, + {"name": self.service.name, "label": "%s:" % self.service.name, + "type": "srv", "color": "info"} + ] + + def __str__(self): + return self.domain.name + + def to_csv(self, csvwriter): + """Export this relay domain to CSV. + + :param csvwriter: + """ + csvwriter.writerow( + ["relaydomain", self.domain.name, self.target_host, + self.service.name, self.domain.enabled, self.verify_recipients] + ) + for dalias in self.domain.domainalias_set.all(): + dalias.to_csv(csvwriter) + + def from_csv(self, user, row): + """Import a relay domain from CSV. + + :param user: user importing the relay domain + :param str row: relay domain definition + """ + if len(row) != 6: + raise BadRequest(_("Invalid line")) + self.domain = admin_models.Domain( + name=row[1].strip(), type="relaydomain", + enabled=(row[4].strip() in ["True", "1", "yes", "y"]) + ) + self.domain.save(creator=user) + self.target_host = row[2].strip() + self.service, created = Service.objects.get_or_create( + name=row[3].strip()) + self.verify_recipients = (row[5].strip() in ["True", "1", "yes", "y"]) + self.save(creator=user) + + +reversion.register(RelayDomain) diff --git a/modoboa/relaydomains/postfix_maps.py b/modoboa/relaydomains/postfix_maps.py new file mode 100644 index 000000000..e38a9707f --- /dev/null +++ b/modoboa/relaydomains/postfix_maps.py @@ -0,0 +1,124 @@ +"""Map file definitions for postfix.""" + +from modoboa.core.commands.postfix_maps import registry + + +class RelayDomainsMap(object): + + """Map file to list all relay domains.""" + + filename = "sql-relaydomains.cf" + mysql = ( + "SELECT name FROM admin_domain " + "WHERE name='%s' AND type='relaydomain' AND enabled=1" + ) + postgres = ( + "SELECT name FROM admin_domain " + "WHERE name='%s' AND type='relaydomain' AND enabled" + ) + sqlite = ( + "SELECT name FROM admin_domain " + "WHERE name='%s' AND type='relaydomain' AND enabled=1" + ) + + +class RelayDomainsTransportMap(object): + + """A transport map for relay domains.""" + + filename = "sql-relaydomains-transport.cf" + mysql = ( + "SELECT CONCAT(srv.name, ':[', rdom.target_host, ']') " + "FROM postfix_relay_domains_service AS srv " + "INNER JOIN postfix_relay_domains_relaydomain AS rdom " + "ON rdom.service_id=srv.id " + "INNER JOIN admin_domain AS dom ON rdom.domain_id=dom.id " + "WHERE dom.enabled=1 AND dom.name='%s'" + ) + postgres = ( + "SELECT srv.name || ':[' || rdom.target_host || ']' " + "FROM postfix_relay_domains_service AS srv " + "INNER JOIN postfix_relay_domains_relaydomain AS rdom " + "ON rdom.service_id=srv.id " + "INNER JOIN admin_domain AS dom ON rdom.domain_id=dom.id " + "WHERE dom.enabled AND dom.name='%s'" + ) + sqlite = ( + "SELECT srv.name || ':[' || rdom.target_host || ']' " + "FROM postfix_relay_domains_service AS srv " + "INNER JOIN postfix_relay_domains_relaydomain AS rdom " + "ON rdom.service_id=srv.id " + "INNER JOIN admin_domain AS dom ON rdom.domain_id=dom.id " + "WHERE dom.enabled=1 AND dom.name='%s'" + ) + + +class SplitedDomainsTransportMap(object): + + """A transport map for splited domains. + + (ie. ones with both local and remote mailboxes) + """ + + filename = "sql-spliteddomains-transport.cf" + mysql = ( + "SELECT 'lmtp:unix:private/dovecot-lmtp' " + "FROM postfix_relay_domains_relaydomain AS rdom " + "INNER JOIN admin_domain AS dom " + "ON rdom.domain_id=dom.id " + "INNER JOIN admin_mailbox AS mbox ON dom.id=mbox.domain_id " + "INNER JOIN core_user AS u ON mbox.user_id=u.id " + "WHERE dom.enabled=1 AND dom.name='%d' AND u.is_active=1 " + "AND mbox.address='%u'" + ) + postgres = ( + "SELECT 'lmtp:unix:private/dovecot-lmtp' " + "FROM postfix_relay_domains_relaydomain AS rdom " + "INNER JOIN admin_domain AS dom " + "ON rdom.domain_id=dom.id " + "INNER JOIN admin_mailbox AS mbox ON dom.id=mbox.domain_id " + "INNER JOIN core_user AS u ON mbox.user_id=u.id " + "WHERE dom.enabled AND dom.name='%d' AND u.is_active " + "AND mbox.address='%u'" + ) + sqlite = ( + "SELECT 'lmtp:unix:private/dovecot-lmtp' " + "FROM postfix_relay_domains_relaydomain AS rdom " + "INNER JOIN admin_domain AS dom " + "ON rdom.domain_id=dom.id " + "INNER JOIN admin_mailbox AS mbox ON dom.id=mbox.domain_id " + "INNER JOIN core_user AS u ON mbox.user_id=u.id " + "WHERE dom.enabled=1 AND dom.name='%d' AND u.is_active=1 " + "AND mbox.address='%u'" + ) + + +class RelayRecipientVerification(object): + + """A map file to enable recipient verification.""" + + filename = "sql-relay-recipient-verification.cf" + mysql = ( + "SELECT 'reject_unverified_recipient' " + "FROM postfix_relay_domains_relaydomain AS rdom " + "INNER JOIN admin_domain AS dom ON rdom.domain_id=dom.id " + "WHERE rdom.verify_recipients=1 AND dom.name='%d'" + ) + postgres = ( + "SELECT 'reject_unverified_recipient' " + "FROM postfix_relay_domains_relaydomain AS rdom " + "INNER JOIN admin_domain AS dom ON rdom.domain_id=dom.id " + "WHERE rdom.verify_recipients AND dom.name='%d'" + ) + sqlite = ( + "SELECT 'reject_unverified_recipient' " + "FROM postfix_relay_domains_relaydomain AS rdom " + "INNER JOIN admin_domain AS dom ON rdom.domain_id=dom.id " + "WHERE rdom.verify_recipients=1 AND dom.name='%d'" + ) + + +registry.add_files([ + RelayDomainsMap, RelayDomainsTransportMap, SplitedDomainsTransportMap, + RelayRecipientVerification +]) diff --git a/modoboa/relaydomains/static/relaydomains/js/relay_domains.js b/modoboa/relaydomains/static/relaydomains/js/relay_domains.js new file mode 100644 index 000000000..807b53d6f --- /dev/null +++ b/modoboa/relaydomains/static/relaydomains/js/relay_domains.js @@ -0,0 +1,70 @@ +/** + * @module relay_domains + * @desc Utility class to manage relay domains. + */ + +var RelayDomains = function(options) { + this.initialize(options); +}; + +RelayDomains.prototype = { + constructor: RelayDomains, + + defaults: { + }, + + initialize: function(options) { + this.options = $.extend({}, this.defaults, options); + admin.register_tag_handler("srv", this.srv_tag_handler); + $(document).bind('domform_init', $.proxy(this.domain_form_init_cb, this)); + }, + + domain_form_init_cb: function() { + $('#btnscan').click(this.launch_scan); + }, + + launch_scan: function(e) { + var $link = get_target(e); + + e.preventDefault(); + $.ajax({ + type: "POST", + url: $link.attr("href"), + global: false + }).done(function(data) { + var $widget = $('#id_service'); + + $widget.empty(); + $.each(data, function(label, id) { + $widget.append($('