mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
Merge branch 'master' into refacto/vuejs_interface
This commit is contained in:
+3
-1
@@ -23,5 +23,7 @@ node_modules
|
||||
/frontend/webpack-stats.json
|
||||
|
||||
# TOX stuff
|
||||
.eggs/
|
||||
test_project/modoboa.db
|
||||
|
||||
/.project
|
||||
/.pydevproject
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
python:
|
||||
install:
|
||||
- requirements: doc/requirements.txt
|
||||
+10
-8
@@ -1,9 +1,9 @@
|
||||
language: python
|
||||
python:
|
||||
- '2.7'
|
||||
- '3.4'
|
||||
- '3.5'
|
||||
- '3.6'
|
||||
- '3.7'
|
||||
- '3.8'
|
||||
|
||||
env:
|
||||
global:
|
||||
@@ -15,23 +15,25 @@ env:
|
||||
sudo: false
|
||||
|
||||
addons:
|
||||
postgresql: '9.3'
|
||||
postgresql: '9.6'
|
||||
mysql: '5.5'
|
||||
apt:
|
||||
packages:
|
||||
- ldap-utils
|
||||
- postfix
|
||||
- slapd
|
||||
- librrd-dev
|
||||
- rrdtool
|
||||
|
||||
cache: pip
|
||||
|
||||
services:
|
||||
- postgres
|
||||
- mysql
|
||||
- redis-server
|
||||
|
||||
before_install:
|
||||
- pip install codecov
|
||||
- pip install codacy-coverage
|
||||
|
||||
install:
|
||||
- pip install -r requirements.txt
|
||||
@@ -50,12 +52,12 @@ before_script:
|
||||
script:
|
||||
- python ./tests.py
|
||||
- cd test_project
|
||||
- coverage run --source ../modoboa manage.py test modoboa.core modoboa.lib modoboa.admin modoboa.limits modoboa.transport modoboa.relaydomains
|
||||
- coverage run manage.py test modoboa.core modoboa.lib modoboa.admin modoboa.limits modoboa.transport modoboa.relaydomains modoboa.dnstools modoboa.ldapsync modoboa.policyd modoboa.maillog
|
||||
|
||||
after_success:
|
||||
- coverage xml
|
||||
- coverage combine
|
||||
- coverage report
|
||||
- codecov
|
||||
- python-codacy-coverage -r coverage.xml
|
||||
|
||||
before_deploy:
|
||||
- cd ../modoboa
|
||||
@@ -71,5 +73,5 @@ deploy:
|
||||
distributions: sdist bdist_wheel
|
||||
on:
|
||||
tags: true
|
||||
python: '3.6'
|
||||
python: '3.8'
|
||||
condition: "$DB = POSTGRESQL"
|
||||
|
||||
+10
-3
@@ -1,14 +1,21 @@
|
||||
FROM python:3.6-alpine
|
||||
FROM python:3.6-alpine as base
|
||||
|
||||
MAINTAINER Antoine Nguyen <tonio@ngyn.org>
|
||||
|
||||
RUN apk add --update python3-dev libffi-dev gcc musl-dev libxml2-dev libxslt-dev \
|
||||
RUN apk add --update openssl python3-dev libffi-dev gcc musl-dev libxml2-dev libxslt-dev \
|
||||
libressl-dev jpeg-dev \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
WORKDIR /tmp
|
||||
COPY requirements.txt /tmp
|
||||
RUN pip install -r requirements.txt
|
||||
COPY test-requirements.txt /tmp
|
||||
RUN pip install --user -r requirements.txt --user -r test-requirements.txt
|
||||
|
||||
FROM python:3.6-alpine
|
||||
COPY --from=base /root/.local /root/.local
|
||||
ENV PATH=/root/.local/bin:$PATH
|
||||
RUN apk add --no-cache --update libxml2-dev libxslt-dev\
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
RUN mkdir /code
|
||||
WORKDIR /code
|
||||
|
||||
+13
-5
@@ -2,7 +2,7 @@
|
||||
`Modoboa <http://modoboa.org/>`_
|
||||
################################
|
||||
|
||||
|travis| |codecov| |codacy| |latest-version|
|
||||
|travis| |codecov| |latest-version|
|
||||
|
||||
Modoboa is a mail hosting and management platform including a modern
|
||||
and simplified Web User Interface. It provides useful components such
|
||||
@@ -17,7 +17,7 @@ is used as a central point of communication between all components.
|
||||
Modoboa is developed with modularity in mind, expanding it is really
|
||||
easy. Actually, all current features are extensions.
|
||||
|
||||
It is written in Python and uses the `Django
|
||||
It is written in Python 3 and uses the `Django
|
||||
<https://www.djangoproject.com>`_, `jQuery <http://jquery.com>`_ and
|
||||
`Bootstrap <http://getbootstrap.com/>`_
|
||||
frameworks.
|
||||
@@ -49,6 +49,16 @@ Demo Installation
|
||||
|
||||
If you want to try out Modoboa, check out our `demo installation <https://demo.modoboa.org/>`_.
|
||||
|
||||
************
|
||||
Getting help
|
||||
************
|
||||
|
||||
If you have any question, you can use the following ways to get help:
|
||||
|
||||
* IRC: join the ``#modoboa`` channel on Freenode
|
||||
* Mailing list: join the `modoboa-users <https://groups.google.com/forum/#!forum/modoboa-users>`_ group on Google Groups
|
||||
* Github: open an issue if you found a bug
|
||||
|
||||
*************
|
||||
External code
|
||||
*************
|
||||
@@ -57,7 +67,7 @@ The following external libraries are provided with Modoboa:
|
||||
|
||||
* `jQuery version 1.9.1 <http://www.jquery.org/>`_
|
||||
* `jQuery-UI 1.10+ <http://jqueryui.com/>`_
|
||||
* `Bootstrap version 3.3.5 <http://getbootstrap.com/>`_
|
||||
* `Bootstrap version 3.3.7 <http://getbootstrap.com/>`_
|
||||
* `Bootstrap datetimepicker <http://eonasdan.github.io/bootstrap-datetimepicker/>`_
|
||||
|
||||
.. |latest-version| image:: https://img.shields.io/pypi/v/modoboa.svg
|
||||
@@ -67,5 +77,3 @@ The following external libraries are provided with Modoboa:
|
||||
:target: https://travis-ci.org/modoboa/modoboa
|
||||
.. |codecov| image:: http://codecov.io/github/modoboa/modoboa/coverage.svg?branch=master
|
||||
:target: http://codecov.io/github/modoboa/modoboa?branch=master
|
||||
.. |codacy| image:: https://api.codacy.com/project/badge/Grade/82fd10127ac3468899ca75f04e9862b6
|
||||
:target: https://www.codacy.com/app/modoboa/modoboa?utm_source=github.com&utm_medium=referral&utm_content=modoboa/modoboa&utm_campaign=Badge_Grade
|
||||
@@ -1,7 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
# coding: utf-8
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from modoboa.core.commands import handle_command_line
|
||||
|
||||
|
||||
+10
-3
@@ -11,11 +11,18 @@
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from unittest.mock import Mock as MagicMock
|
||||
import os
|
||||
import sys
|
||||
|
||||
# import sys
|
||||
|
||||
class Mock(MagicMock):
|
||||
@classmethod
|
||||
def __getattr__(cls, name):
|
||||
return MagicMock()
|
||||
|
||||
MOCK_MODULES = ['rrdtool']
|
||||
sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
|
||||
+59
-3
@@ -14,7 +14,7 @@ are two available levels:
|
||||
|
||||
* User level: per user customization. Available at *User > Settings >
|
||||
Preferences*
|
||||
|
||||
|
||||
Regardless level, parameters are displayed using tabs, each tab
|
||||
corresponding to one application.
|
||||
|
||||
@@ -297,6 +297,7 @@ like this::
|
||||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
'modoboa.lib.authbackends.LDAPBackend',
|
||||
# 'modoboa.lib.authbackends.LDAPSecondaryBackend', # Useful for a fallback mechanism
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
)
|
||||
|
||||
@@ -386,7 +387,7 @@ Modoboa should connect to your LDAP server. They are described just below:
|
||||
+--------------------+---------------------------------+--------------------+
|
||||
|
||||
|
||||
If you need additional parameters, you will find a detailled
|
||||
If you need additional parameters, you will find a detailed
|
||||
documentation `here <http://packages.python.org/django-auth-ldap/>`_.
|
||||
|
||||
Once the authentication is properly configured, the users defined in
|
||||
@@ -405,7 +406,7 @@ group. In this case, the username is not necessarily an email address.
|
||||
Users will also be able to update their LDAP password directly from
|
||||
Modoboa.
|
||||
|
||||
.. note::
|
||||
.. note::
|
||||
|
||||
Modoboa doesn't provide any synchronization mechanism once a user
|
||||
is registered into the database. Any modification done from the
|
||||
@@ -438,6 +439,61 @@ SMTP server location can be customized using the following settings:
|
||||
AUTH_SMTP_SERVER_PORT = 25
|
||||
AUTH_SMTP_SECURED_MODE = None # 'ssl' or 'starttls' are accepted
|
||||
|
||||
|
||||
.. _ldap_sync:
|
||||
|
||||
********************
|
||||
LDAP synchronization
|
||||
********************
|
||||
|
||||
Modoboa can synchronize accounts with an LDAP directory (tested with
|
||||
OpenLDAP) but this feature is not enabled by default. To activate it,
|
||||
add ``modoboa.ldapsync`` to ``MODOBOA_APPS`` in the
|
||||
:file:`settings.py` file::
|
||||
|
||||
MODOBOA_APPS = (
|
||||
'modoboa',
|
||||
'modoboa.core',
|
||||
'modoboa.lib',
|
||||
'modoboa.admin',
|
||||
'modoboa.transport',
|
||||
'modoboa.relaydomains',
|
||||
'modoboa.limits',
|
||||
'modoboa.parameters',
|
||||
'modoboa.dnstools',
|
||||
'modoboa.ldapsync',
|
||||
)
|
||||
|
||||
and enable it from the admin panel.
|
||||
|
||||
.. warning::
|
||||
|
||||
Make sure to install additional :ref:`requirements <ldap_auth>`
|
||||
otherwise it won't work.
|
||||
|
||||
The following parameters are available and must be filled:
|
||||
|
||||
+--------------------+---------------------------------+--------------------+
|
||||
|Name |Description |Default value |
|
||||
+====================+=================================+====================+
|
||||
|Enable LDAP |Control LDAP synchronization |no |
|
||||
|synchronization |state | |
|
||||
| | | |
|
||||
+--------------------+---------------------------------+--------------------+
|
||||
|Bind DN |The DN of a user with write | |
|
||||
| |permission to create/update | |
|
||||
| |accounts | |
|
||||
+--------------------+---------------------------------+--------------------+
|
||||
|Bind password |The associated password | |
|
||||
| | | |
|
||||
| | | |
|
||||
+--------------------+---------------------------------+--------------------+
|
||||
|Account DN template |The template used to build | |
|
||||
| |account DNs (must contain a | |
|
||||
| |%(user)s placeholder | |
|
||||
+--------------------+---------------------------------+--------------------+
|
||||
|
||||
|
||||
********************
|
||||
Database maintenance
|
||||
********************
|
||||
|
||||
@@ -5,23 +5,35 @@ Useful tips
|
||||
You would like to work on Modoboa but you don't know where to start?
|
||||
You're at the right place! Browse this page to learn useful tips.
|
||||
|
||||
Docker
|
||||
======
|
||||
|
||||
A docker image is available for developers. To use it, you must
|
||||
install `docker <https://docs.docker.com/install/>`_ and
|
||||
`docker-compose <https://docs.docker.com/compose/install/>`_ first.
|
||||
|
||||
Then, just run the following command::
|
||||
|
||||
$ docker-compose up
|
||||
|
||||
It will start the docker environment and make a Modoboa instance
|
||||
available at ``http://localhost:8000``.
|
||||
|
||||
If you don't want to use docker or need a more complex development
|
||||
setup, go to the next section.
|
||||
|
||||
.. _venv_for_dev:
|
||||
|
||||
Prepare a virtual environment
|
||||
=============================
|
||||
|
||||
A `virtual environment
|
||||
<http://virtualenv.readthedocs.org/en/latest/>`_ is a good way to
|
||||
<https://docs.python.org/fr/3/library/venv.html>`_ is a good way to
|
||||
setup a development environment on your machine.
|
||||
|
||||
.. note::
|
||||
|
||||
``virtualenv`` is available on all major distributions, just
|
||||
install it using your favorite packages manager.
|
||||
|
||||
To do so, run the following commands::
|
||||
|
||||
$ virtualenv <path>
|
||||
$ python3 -m venv <path>
|
||||
$ source <path>/bin/activate
|
||||
$ git clone https://github.com/modoboa/modoboa.git
|
||||
$ cd modoboa
|
||||
@@ -102,7 +114,7 @@ If you need to modify the documenation and want to see the result, you
|
||||
can build it as follows:
|
||||
|
||||
.. sourcecode:: bash
|
||||
|
||||
|
||||
> tox -e doc
|
||||
> firefox .tox/doc/tmp/html/index.html
|
||||
|
||||
|
||||
@@ -5,4 +5,5 @@ Contributors
|
||||
|
||||
- `Antidot <http://antidot.com>`_
|
||||
- `Bearstech <http://bearstech.com>`_
|
||||
- `CAP-REL <https://cap-rel.fr/>`_
|
||||
- `Dalnix <https://www.dalnix.se/>`_
|
||||
|
||||
@@ -9,6 +9,13 @@ You will need a Server to perform well with at least:
|
||||
* CPU: 2
|
||||
* RAM: 2GB
|
||||
* Disk: 10GB
|
||||
* Python 3
|
||||
|
||||
.. note:: **👀 Heads up for modoboa versions 1.15 and later**
|
||||
|
||||
Python 2 support has been dropped with modoboa version 1.15.
|
||||
If you still have Python 2 installed on your system either uninstall it
|
||||
or force modoboa user to run with Python 3.
|
||||
|
||||
Recommended way
|
||||
===============
|
||||
|
||||
@@ -193,6 +193,49 @@ SQLite users
|
||||
|
||||
Replace values between ``<>`` with yours.
|
||||
|
||||
|
||||
LDAP
|
||||
====
|
||||
To make the LDAP authentication work, edit the :file:`conf.d/10-auth.conf` and
|
||||
uncomment the following line at the end::
|
||||
|
||||
!include auth-ldap.conf.ext
|
||||
|
||||
Then edit the :file:`conf.d/auth-ldap.conf.ext` and edit the passdb section as following.
|
||||
You should comment the userdb section, which will be managed by SQL with modoboa database.::
|
||||
|
||||
passdb {
|
||||
driver = ldap
|
||||
|
||||
# Path for LDAP configuration file, see example-config/dovecot-ldap.conf.ext
|
||||
args = /etc/dovecot/dovecot-ldap.conf.ext
|
||||
}
|
||||
|
||||
#userdb {
|
||||
#driver = ldap
|
||||
#args = /etc/dovecot/dovecot-ldap.conf.ext
|
||||
|
||||
# Default fields can be used to specify defaults that LDAP may override
|
||||
#default_fields = home=/home/virtual/%u
|
||||
#}
|
||||
|
||||
Your own dovecot LDAP configuration file is now :file:`/etc/dovecot/dovecot-ldap.conf.ext`.
|
||||
You can add your default LDAP conf in it, following the `official documentation <https://doc.dovecot.org/configuration_manual/authentication/ldap/>`_.
|
||||
|
||||
Synchronize dovecot LDAP conf with modoboa LDAP conf
|
||||
----------------------------------------------------
|
||||
|
||||
To make dovecot LDAP configuration synchronized with modoboa LDAP configuration, you should create a dedicated dovecot conf file.
|
||||
At the end of your dovecot configuration file (:file:`dovecot-ldap.conf.ext`), add the following line::
|
||||
|
||||
!include_try dovecot-modoboa.conf.ext
|
||||
|
||||
Then, set modoboa parameter *Enable Dovecot LDAP sync* to *Yes*.
|
||||
Then set the *Dovecot LDAP config file* following the previous step (*/etc/dovecot/dovecot-modoboa.conf.ext* in the example)
|
||||
|
||||
The last step is to add the command **update_dovecot_conf** to the cron job of modoboa.
|
||||
Then, each time your modoboa LDAP configuration is updated, your dovecot LDAP configuration will also be.
|
||||
|
||||
LMTP
|
||||
====
|
||||
|
||||
|
||||
@@ -10,56 +10,63 @@ This section describes the installation of the web interface (a
|
||||
Prepare the system
|
||||
------------------
|
||||
|
||||
First of all, we recommand the following context:
|
||||
First of all, we recommend the following context:
|
||||
|
||||
* Use a dedicated system user
|
||||
* Use a `virtualenv <http://www.virtualenv.org/en/latest/>`_ to
|
||||
install the application because it will isolate it (and its
|
||||
* A dedicated system user
|
||||
* A `virtual environment
|
||||
<https://packaging.python.org/tutorials/installing-packages/#creating-virtual-environments>`_
|
||||
to install the application because it will isolate it (and its
|
||||
dependencies) from the rest of your system
|
||||
|
||||
The following example illustrates how to realize this (Debian like system):
|
||||
|
||||
.. sourcecode:: bash
|
||||
The following example illustrates how to realize this on Debian-based
|
||||
distributions using `virtualenv <https://virtualenv.pypa.io/en/stable/>`_:
|
||||
|
||||
> sudo apt-get install python-virtualenv python-pip
|
||||
> sudo useradd modoboa
|
||||
> sudo -i modoboa
|
||||
> virtualenv env
|
||||
> source env/bin/activate
|
||||
(env)> pip install -U pip
|
||||
.. code-block:: console
|
||||
|
||||
# apt-get install virtualenv python3-pip
|
||||
# useradd modoboa # create a dedicated user
|
||||
# su -l modoboa # log in as the newly created user
|
||||
$ virtualenv --python python3 ./env # create the virtual environment
|
||||
$ source ./env/bin/activate # activate the virtual environment
|
||||
|
||||
Modoboa depends on external tools and some of them require compilation
|
||||
so you need a compiler and a few C libraries. Make sure to install the
|
||||
following system packages according to your distribution:
|
||||
|
||||
+------------------------------+
|
||||
|Debian / Ubuntu |
|
||||
+==============================+
|
||||
|build-essential python-dev |
|
||||
|libxml2-dev libxslt-dev |
|
||||
|libjpeg-dev librrd-dev |
|
||||
|rrdtool libffi-dev |
|
||||
| |
|
||||
+------------------------------+
|
||||
+-------------------------------+
|
||||
| Debian / Ubuntu |
|
||||
+===============================+
|
||||
| build-essential python3-dev |
|
||||
| libxml2-dev libxslt-dev |
|
||||
| libjpeg-dev librrd-dev |
|
||||
| rrdtool libffi-dev libssl-dev |
|
||||
+-------------------------------+
|
||||
|
||||
+------------------------------+
|
||||
|CentOS |
|
||||
+==============================+
|
||||
|gcc gcc-c++ python-devel |
|
||||
|libxml2-devel libxslt-devel |
|
||||
|libjpeg-turbo-devel |
|
||||
|rrdtool-devel rrdtool |
|
||||
|libffi-devel |
|
||||
+------------------------------+
|
||||
+-----------------------------+
|
||||
| CentOS |
|
||||
+=============================+
|
||||
| gcc gcc-c++ python3-devel |
|
||||
| libxml2-devel libxslt-devel |
|
||||
| libjpeg-turbo-devel |
|
||||
| rrdtool-devel rrdtool |
|
||||
| libffi-devel |
|
||||
+-----------------------------+
|
||||
|
||||
Then, install Modoboa:
|
||||
.. note::
|
||||
|
||||
.. sourcecode:: bash
|
||||
Alternatively, you could rely on your distribution packages for the Modoboa
|
||||
dependencies which require compilation - e.g. ``psycopg2`` - if the version
|
||||
is compatible. In this case, you have to create your virtual environment
|
||||
with the ``--system-site-packages`` option.
|
||||
|
||||
(env)> pip install modoboa
|
||||
Then, install Modoboa by running:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
(env)$ pip install modoboa
|
||||
|
||||
.. _database:
|
||||
|
||||
|
||||
Database
|
||||
--------
|
||||
|
||||
@@ -73,54 +80,76 @@ Thanks to Django, Modoboa is compatible with the following databases:
|
||||
|
||||
* PostgreSQL
|
||||
* MySQL / MariaDB
|
||||
* SQLite
|
||||
* SQLite
|
||||
|
||||
Since the last one does not require particular actions, only the first
|
||||
two ones are described.
|
||||
two ones are described. You should also read the notes for those database
|
||||
backends on the `official Django documentation
|
||||
<https://docs.djangoproject.com/en/stable/ref/databases/>`_.
|
||||
|
||||
PostgreSQL
|
||||
**********
|
||||
|
||||
Install the corresponding Python binding:
|
||||
|
||||
.. sourcecode:: bash
|
||||
.. code-block:: console
|
||||
|
||||
(env)> pip install psycopg2
|
||||
(env)$ pip install psycopg2
|
||||
|
||||
Then, create a user and a database:
|
||||
.. note::
|
||||
|
||||
.. sourcecode:: bash
|
||||
Alternatively, you can install the ``python3-psycopg2`` package instead on
|
||||
Debian-based distributions if your virtual environment was created with
|
||||
``--system-site-packages`` option.
|
||||
|
||||
> sudo -i postgres
|
||||
>
|
||||
Then, create a user and a database. For example, to create the ``modoboa``
|
||||
database owned by a ``modoboa`` user, run the following commands on your
|
||||
PostgreSQL server:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
# sudo -l -u postgres createuser --no-createdb modoboa
|
||||
# sudo -l -u postgres createdb --owner=modoboa modoboa
|
||||
|
||||
MySQL / MariaDB
|
||||
***************
|
||||
|
||||
Install the corresponding Python binding:
|
||||
|
||||
.. sourcecode:: bash
|
||||
.. code-block:: console
|
||||
|
||||
(env)> pip install mysqlclient
|
||||
(env)$ pip install mysqlclient
|
||||
|
||||
.. note::
|
||||
|
||||
Alternatively, you can install the ``python3-mysqldb`` package instead on
|
||||
Debian-based distributions if your virtual environment was created with
|
||||
``--system-site-packages`` option.
|
||||
|
||||
.. note::
|
||||
|
||||
MariaDB 10.2 (and newer) require mysqlclient 1.3.11 (or newer).
|
||||
|
||||
Then, create a user and a database:
|
||||
Then, create a user and a database. For example, to create the ``modoboa``
|
||||
database owned by a ``modoboa`` user, run the following SQL commands:
|
||||
|
||||
.. sourcecode:: bash
|
||||
.. code-block:: mysql
|
||||
|
||||
> mysqladmin -u root -p create modoboa
|
||||
CREATE DATABASE modoboa;
|
||||
CREATE USER 'modoboa'@'localhost' IDENTIFIED BY 'my-strong-password-here';
|
||||
GRANT ALL PRIVILEGES ON modoboa.* TO 'modoboa'@'localhost';
|
||||
|
||||
Deploy an instance
|
||||
------------------
|
||||
|
||||
``modoboa-admin.py``, a command line tool, lets you deploy a
|
||||
*ready-to-use* Modoboa site using only one instruction::
|
||||
``modoboa-admin.py`` is a command line tool that lets you deploy a
|
||||
*ready-to-use* Modoboa site. To create a new instance into ``./instance``,
|
||||
you just have to run the following command:
|
||||
|
||||
(env)> modoboa-admin.py deploy instance --collectstatic \
|
||||
--domain <hostname of your server> --dburl default:database-url
|
||||
.. code-block:: console
|
||||
|
||||
(env)$ modoboa-admin.py deploy instance --collectstatic \
|
||||
--domain <hostname of your server> --dburl default:<database url>
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -160,12 +189,14 @@ done.
|
||||
|
||||
If you need a **silent installation** (e.g. if you're using
|
||||
Salt-Stack, Ansible or whatever), it's possible to supply the database
|
||||
credentials as commandline arguments.
|
||||
credentials as command line arguments.
|
||||
|
||||
You can consult the complete option list by running the following
|
||||
command::
|
||||
command:
|
||||
|
||||
$ modoboa-admin.py help deploy
|
||||
.. code-block:: console
|
||||
|
||||
(env)$ modoboa-admin.py help deploy
|
||||
|
||||
Cron jobs
|
||||
---------
|
||||
@@ -174,35 +205,63 @@ A few recurring jobs must be configured to make Modoboa works as
|
||||
expected.
|
||||
|
||||
Create a new file, for example :file:`/etc/cron.d/modoboa` and put the
|
||||
following content inside::
|
||||
following content inside:
|
||||
|
||||
#
|
||||
# Modoboa specific cron jobs
|
||||
#
|
||||
PYTHON=<PATH TO PYTHON BINARY>
|
||||
INSTANCE=<PATH TO MODOBOA INSTANCE>
|
||||
.. sourcecode:: bash
|
||||
|
||||
# Operations on mailboxes
|
||||
* * * * * vmail $PYTHON $INSTANCE/manage.py handle_mailbox_operations
|
||||
#
|
||||
# Modoboa specific cron jobs
|
||||
#
|
||||
PYTHON=<path to Python binary inside the virtual environment>
|
||||
INSTANCE=<path to Modoboa instance>
|
||||
|
||||
# Sessions table cleanup
|
||||
0 0 * * * root $PYTHON $INSTANCE/manage.py clearsessions
|
||||
# Operations on mailboxes
|
||||
* * * * * vmail $PYTHON $INSTANCE/manage.py handle_mailbox_operations
|
||||
|
||||
# Logs table cleanup
|
||||
0 0 * * * root $PYTHON $INSTANCE/manage.py cleanlogs
|
||||
# Generate DKIM keys (they will belong to the user running this job)
|
||||
* * * * * root umask 077 && $PYTHON $INSTANCE/manage.py modo manage_dkim_keys
|
||||
|
||||
# Logs parsing
|
||||
*/5 * * * * root $PYTHON $INSTANCE/manage.py logparser &> /dev/null
|
||||
0 * * * * root $PYTHON $INSTANCE/manage.py update_statistics
|
||||
# Sessions table cleanup
|
||||
0 0 * * * modoboa $PYTHON $INSTANCE/manage.py clearsessions
|
||||
# Logs table cleanup
|
||||
0 0 * * * modoboa $PYTHON $INSTANCE/manage.py cleanlogs
|
||||
# DNSBL checks
|
||||
*/30 * * * * modoboa $PYTHON $INSTANCE/manage.py modo check_mx
|
||||
# Public API communication
|
||||
0 * * * * modoboa $PYTHON $INSTANCE/manage.py communicate_with_public_api
|
||||
|
||||
# DNSBL checks
|
||||
*/30 * * * * root $PYTHON $INSTANCE/manage.py modo check_mx
|
||||
.. _policy_daemon:
|
||||
|
||||
# Public API communication
|
||||
0 * * * * root $PYTHON $INSTANCE/manage.py communicate_with_public_api
|
||||
Policy daemon
|
||||
-------------
|
||||
|
||||
# Generate DKIM keys (they will belong to the user running this job)
|
||||
* * * * * root umask 077 && $PYTHON $INSTANCE/manage.py modo manage_dkim_keys
|
||||
Modoboa comes with a built-in `Policy Daemon for Postfix <http://www.postfix.org/SMTPD_POLICY_README.html>`_. Current features are:
|
||||
|
||||
* Define daily sending limits for domains and/or accounts
|
||||
|
||||
A `redis server <https://redis.io/>`_ is required to run this new daemon.
|
||||
|
||||
You can launch it manually using the following command:
|
||||
|
||||
.. sourcecode:: bash
|
||||
|
||||
(env)> python manage.py policy_daemon
|
||||
|
||||
But we recommend an automatic start using ``systemd`` or
|
||||
``supervisor``. Here is a configuration example for ``supervisor``:
|
||||
|
||||
.. sourcecode:: ini
|
||||
|
||||
[program:policyd]
|
||||
autostart=true
|
||||
autorestart=true
|
||||
command=/srv/modoboa/env/bin/python /srv/modoboa/instance/manage.py policy_daemon
|
||||
directory=/srv/modoboa
|
||||
redirect_stderr=true
|
||||
user=modoboa
|
||||
numprocs=1
|
||||
|
||||
It will listen by default on ``127.0.0.1`` and port ``9999``. The
|
||||
policy daemon won't do anything unless you tell :ref:`postfix <policyd_config>` to use it.
|
||||
|
||||
Now you can continue to the :ref:`webserver` section.
|
||||
|
||||
@@ -65,3 +65,23 @@ Use the following configuration in the :file:`/etc/postfix/main.cf` file
|
||||
Replace ``<driver>`` by the name of the database you use.
|
||||
|
||||
Restart Postfix.
|
||||
|
||||
.. _policyd_config:
|
||||
|
||||
Policy daemon
|
||||
-------------
|
||||
|
||||
If you want to enable the built-in policy daemon, add the following
|
||||
content to the :file:`/etc/postfix/main.cf` file::
|
||||
|
||||
smtpd_recipient_restrictions =
|
||||
# ...
|
||||
check_policy_service inet:127.0.0.1:9999
|
||||
# ...
|
||||
|
||||
And reload postfix.
|
||||
|
||||
.. note::
|
||||
|
||||
The ``check_policy_service`` line must be placed before the
|
||||
``permit_mynetworks`` one, otherwise the daemon won't be called.
|
||||
|
||||
@@ -50,7 +50,7 @@ This is just one possible configuration.
|
||||
To use mod_wsgi daemon mode, add the two following directives just
|
||||
under ``WSGIScriptAlias``::
|
||||
|
||||
WSGIDaemonProcess example.com python-path=<modoboa_instance>:<virtualenv path>/lib/python2.7/site-packages
|
||||
WSGIDaemonProcess example.com python-path=<modoboa_instance>:<virtualenv path>/lib/python3.7/site-packages
|
||||
WSGIProcessGroup example.com
|
||||
|
||||
Replace values between ``<>`` with yours. If you don't use a
|
||||
|
||||
+138
-1
@@ -62,7 +62,7 @@ use them:
|
||||
|
||||
.. sourcecode:: bash
|
||||
|
||||
> sudo -i <modoboa_user>
|
||||
> sudo -u <modoboa_user> -i
|
||||
> source <virtuenv_path>/bin/activate
|
||||
|
||||
Then, run the following commands:
|
||||
@@ -91,6 +91,143 @@ Rebuild Virtual Environment
|
||||
Specific instructions
|
||||
*********************
|
||||
|
||||
1.16.0
|
||||
======
|
||||
|
||||
A new :ref:`policy daemon <policy_daemon>` has been added.
|
||||
|
||||
Make sure to have a Redis instance running on your server.
|
||||
|
||||
Add ``'modoboa.policyd'`` to ``MODOBOA_APPS``:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
MODOBOA_APPS = (
|
||||
'modoboa',
|
||||
'modoboa.core',
|
||||
'modoboa.lib',
|
||||
'modoboa.admin',
|
||||
'modoboa.transport',
|
||||
'modoboa.relaydomains',
|
||||
'modoboa.limits',
|
||||
'modoboa.parameters',
|
||||
'modoboa.dnstools',
|
||||
'modoboa.policyd'
|
||||
)
|
||||
|
||||
Add the following settings to your ``settings.py`` file:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
REDIS_HOST = '<IP or hostname here>'
|
||||
REDIS_PORT = 6379
|
||||
REDIS_QUOTA_DB = 0
|
||||
REDIS_URL = 'redis://{}:{}/{}'.format(REDIS_HOST, REDIS_PORT, REDIS_QUOTA_DB)
|
||||
|
||||
Once done, you can start the policy daemon using the following commands:
|
||||
|
||||
.. sourcecode:: bash
|
||||
|
||||
> python manage.py policy_daemon
|
||||
|
||||
Don't forget to configure :ref:`_policyd_config <postfix>` if you want
|
||||
to use this feature.
|
||||
|
||||
The ``modoboa-stats`` plugin has been merged into the core.
|
||||
|
||||
Add ``'modoboa.maillog'`` to ``MODOBOA_APPS``:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
MODOBOA_APPS = (
|
||||
'modoboa',
|
||||
'modoboa.core',
|
||||
'modoboa.lib',
|
||||
'modoboa.admin',
|
||||
'modoboa.transport',
|
||||
'modoboa.relaydomains',
|
||||
'modoboa.limits',
|
||||
'modoboa.parameters',
|
||||
'modoboa.dnstools',
|
||||
'modoboa.policyd',
|
||||
'modoboa.maillog',
|
||||
)
|
||||
|
||||
And remove any reference to ``modoboa_stats`` in this same variable.
|
||||
|
||||
1.15.0
|
||||
======
|
||||
|
||||
This version drops Python 2 support so don't forget to update all the
|
||||
extensions you use.
|
||||
|
||||
.. warning::
|
||||
|
||||
If you upgrade an existing python 2 installation, you will need to
|
||||
create a new Python 3 virtual environment. You can remove the
|
||||
existing virtual environment and replace it by the new one so you
|
||||
won't have to modify your configuration files.
|
||||
|
||||
Add the following new setting:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
DISABLE_DASHBOARD_EXTERNAL_QUERIES = False
|
||||
|
||||
Reload uwsgi/gunicorn/apache depending on your setup.
|
||||
|
||||
Finally, Make sure to use root privileges and run the following
|
||||
command:
|
||||
|
||||
.. sourcecode:: bash
|
||||
|
||||
> python manage.py generate_postfix_maps --destdir <directory>
|
||||
|
||||
Then, reload postfix.
|
||||
|
||||
|
||||
1.14.0
|
||||
======
|
||||
|
||||
This release introduces an optional LDAP synchronization process. If
|
||||
you want to use it, please follow the :ref:`dedicated procedure <ldap_sync>`.
|
||||
|
||||
1.13.1
|
||||
======
|
||||
|
||||
Upgrade postfix maps files as follows:
|
||||
|
||||
.. sourcecode:: bash
|
||||
|
||||
> python manage.py generate_postfix_maps --destdir <path> --force-overwrite
|
||||
|
||||
|
||||
1.13.0
|
||||
======
|
||||
|
||||
Add ``'modoboa.dnstools'`` to ``MODOBOA_APPS``:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
MODOBOA_APPS = (
|
||||
'modoboa',
|
||||
'modoboa.core',
|
||||
'modoboa.lib',
|
||||
'modoboa.admin',
|
||||
'modoboa.transport',
|
||||
'modoboa.relaydomains',
|
||||
'modoboa.limits',
|
||||
'modoboa.parameters',
|
||||
'modoboa.dnstools',
|
||||
)
|
||||
|
||||
Add the following new settings:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
CSRF_COOKIE_SECURE = True
|
||||
SESSION_COOKIE_SECURE = True
|
||||
|
||||
modoboa-postfix-autoreply 1.5.0
|
||||
===============================
|
||||
|
||||
|
||||
Generated
+3481
-2580
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Modoboa - Mail hosting made simple."""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from pkg_resources import DistributionNotFound, get_distribution
|
||||
|
||||
try:
|
||||
|
||||
@@ -1,5 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
default_app_config = "modoboa.admin.apps.AdminConfig"
|
||||
|
||||
+40
-9
@@ -1,20 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Admin API."""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django import http
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from rest_framework import filters, status, viewsets
|
||||
from rest_framework.decorators import detail_route, list_route
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ParseError
|
||||
from rest_framework.permissions import DjangoModelPermissions, IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from modoboa.core import models as core_models
|
||||
from . import models, serializers
|
||||
from modoboa.core import sms_backends
|
||||
|
||||
from . import lib, models, serializers
|
||||
|
||||
|
||||
class DomainViewSet(viewsets.ModelViewSet):
|
||||
@@ -69,7 +68,8 @@ class AccountViewSet(viewsets.ModelViewSet):
|
||||
action_dict = {
|
||||
"list": serializers.AccountSerializer,
|
||||
"retrieve": serializers.AccountSerializer,
|
||||
"password": serializers.AccountPasswordSerializer
|
||||
"password": serializers.AccountPasswordSerializer,
|
||||
"reset_password": serializers.ResetPasswordSerializer,
|
||||
}
|
||||
return action_dict.get(
|
||||
self.action, serializers.WritableAccountSerializer)
|
||||
@@ -86,7 +86,7 @@ class AccountViewSet(viewsets.ModelViewSet):
|
||||
queryset = queryset.filter(mailbox__domain__name=domain)
|
||||
return queryset
|
||||
|
||||
@detail_route(methods=["put"])
|
||||
@action(methods=["put"], detail=True)
|
||||
def password(self, request, pk=None):
|
||||
"""Change account password."""
|
||||
try:
|
||||
@@ -100,7 +100,7 @@ class AccountViewSet(viewsets.ModelViewSet):
|
||||
return Response(
|
||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@list_route()
|
||||
@action(detail=False)
|
||||
def exists(self, request):
|
||||
"""Check if account exists.
|
||||
|
||||
@@ -119,6 +119,37 @@ class AccountViewSet(viewsets.ModelViewSet):
|
||||
serializer = serializers.AccountExistsSerializer(data)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(methods=["post"], detail=False)
|
||||
def reset_password(self, request):
|
||||
"""Reset account password and send a new one by SMS."""
|
||||
sms_password_recovery = (
|
||||
request.localconfig.parameters
|
||||
.get_value("sms_password_recovery", app="core")
|
||||
)
|
||||
if not sms_password_recovery:
|
||||
return Response(status=404)
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
user = core_models.User.objects.filter(
|
||||
email=serializer.validated_data["email"]).first()
|
||||
if not user or not user.phone_number:
|
||||
return Response(status=404)
|
||||
backend = sms_backends.get_active_backend(
|
||||
request.localconfig.parameters)
|
||||
if not backend:
|
||||
return Response(status=404)
|
||||
password = lib.make_password()
|
||||
content = _("Here is your new Modoboa password: {}").format(
|
||||
password)
|
||||
if not backend.send(content, [str(user.phone_number)]):
|
||||
body = {"status": "ko"}
|
||||
else:
|
||||
# SMS was sent, now we can set the new password.
|
||||
body = {"status": "ok"}
|
||||
user.set_password(password)
|
||||
user.save(update_fields=["password"])
|
||||
return Response(body)
|
||||
|
||||
|
||||
class AliasViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Modoboa admin settings."""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import collections
|
||||
import os
|
||||
|
||||
@@ -11,6 +7,7 @@ from django import forms
|
||||
from django.conf import settings
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
from django.forms.fields import GenericIPAddressField
|
||||
|
||||
from modoboa.lib.form_utils import SeparatorField, YesNoField
|
||||
from modoboa.lib.sysutils import exec_cmd
|
||||
@@ -52,6 +49,39 @@ class AdminParametersForm(param_forms.AdminParametersForm):
|
||||
)
|
||||
)
|
||||
|
||||
enable_spf_checks = YesNoField(
|
||||
label=ugettext_lazy("Enable SPF checks"),
|
||||
initial=True,
|
||||
help_text=ugettext_lazy(
|
||||
"Check if every domain has a valid SPF record"
|
||||
)
|
||||
)
|
||||
|
||||
enable_dkim_checks = YesNoField(
|
||||
label=ugettext_lazy("Enable DKIM checks"),
|
||||
initial=True,
|
||||
help_text=ugettext_lazy(
|
||||
"Check if every domain with DKIM signin enabled has a valid DNS "
|
||||
"record"
|
||||
)
|
||||
)
|
||||
|
||||
enable_dmarc_checks = YesNoField(
|
||||
label=ugettext_lazy("Enable DMARC checks"),
|
||||
initial=True,
|
||||
help_text=ugettext_lazy(
|
||||
"Check if every domain has a valid DMARC record"
|
||||
)
|
||||
)
|
||||
|
||||
enable_autoconfig_checks = YesNoField(
|
||||
label=ugettext_lazy("Enable autoconfig checks"),
|
||||
initial=True,
|
||||
help_text=ugettext_lazy(
|
||||
"Check if every domain has a valid records for autoconfiguration"
|
||||
)
|
||||
)
|
||||
|
||||
enable_dnsbl_checks = YesNoField(
|
||||
label=ugettext_lazy("Enable DNSBL checks"),
|
||||
initial=True,
|
||||
@@ -60,6 +90,14 @@ class AdminParametersForm(param_forms.AdminParametersForm):
|
||||
)
|
||||
)
|
||||
|
||||
custom_dns_server = GenericIPAddressField(
|
||||
label=ugettext_lazy("Custom DNS server"),
|
||||
required=False,
|
||||
help_text=ugettext_lazy(
|
||||
"Use a custom DNS server instead of local server configuration"
|
||||
)
|
||||
)
|
||||
|
||||
dkim_keys_storage_dir = forms.CharField(
|
||||
label=ugettext_lazy("DKIM keys storage directory"),
|
||||
initial="",
|
||||
@@ -80,6 +118,24 @@ class AdminParametersForm(param_forms.AdminParametersForm):
|
||||
)
|
||||
)
|
||||
|
||||
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."
|
||||
)
|
||||
)
|
||||
|
||||
default_domain_message_limit = forms.IntegerField(
|
||||
label=ugettext_lazy("Default domain sending limit"),
|
||||
required=False,
|
||||
help_text=ugettext_lazy(
|
||||
"Number of messages freshly created domains will be "
|
||||
"allowed to send per day. Leave empty for no limit."
|
||||
)
|
||||
)
|
||||
|
||||
mbsep = SeparatorField(label=ugettext_lazy("Mailboxes"))
|
||||
|
||||
handle_mailboxes = YesNoField(
|
||||
@@ -99,21 +155,21 @@ class AdminParametersForm(param_forms.AdminParametersForm):
|
||||
)
|
||||
)
|
||||
|
||||
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."
|
||||
)
|
||||
)
|
||||
|
||||
default_mailbox_quota = forms.IntegerField(
|
||||
label=ugettext_lazy("Default mailbox quota"),
|
||||
initial=0,
|
||||
help_text=ugettext_lazy(
|
||||
"Default mailbox quota (in MB) applied to freshly created domains "
|
||||
"with no value specified. A value of 0 means no quota."
|
||||
"Default mailbox quota (in MB) applied to freshly created "
|
||||
"mailboxes with no value specified. A value of 0 means no quota."
|
||||
)
|
||||
)
|
||||
|
||||
default_mailbox_message_limit = forms.IntegerField(
|
||||
label=ugettext_lazy("Default mailbox sending limit"),
|
||||
required=False,
|
||||
help_text=ugettext_lazy(
|
||||
"Number of messages freshly created mailboxes will be "
|
||||
"allowed to send per day. Leave empty for no limit."
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""AppConfig for admin."""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
from .app_settings import load_admin_settings
|
||||
|
||||
@@ -1,88 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Admin constants."""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
DNSBL_PROVIDERS = [
|
||||
"aspews.ext.sorbs.net",
|
||||
"b.barracudacentral.org",
|
||||
"bad.psky.me",
|
||||
"bl.deadbeef.com",
|
||||
"bl.emailbasura.org",
|
||||
"bl.spamcop.net",
|
||||
"blackholes.five-ten-sg.com",
|
||||
"blacklist.woody.ch",
|
||||
"bogons.cymru.com",
|
||||
"cbl.abuseat.org",
|
||||
"cdl.anti-spam.org.cn",
|
||||
"combined.abuse.ch",
|
||||
"combined.rbl.msrbl.net",
|
||||
"db.wpbl.info",
|
||||
"dnsbl-1.uceprotect.net",
|
||||
"dnsbl-2.uceprotect.net",
|
||||
"dnsbl-3.uceprotect.net",
|
||||
"dnsbl.inps.de",
|
||||
"dnsbl.sorbs.net",
|
||||
"drone.abuse.ch",
|
||||
"duinv.aupads.org",
|
||||
"dul.dnsbl.sorbs.net",
|
||||
"dul.ru",
|
||||
"dyna.spamrats.com",
|
||||
"dynip.rothen.com",
|
||||
"http.dnsbl.sorbs.net"
|
||||
"images.rbl.msrbl.net",
|
||||
"ips.backscatterer.org",
|
||||
"ix.dnsbl.manitu.net",
|
||||
"korea.services.net",
|
||||
"misc.dnsbl.sorbs.net",
|
||||
"noptr.spamrats.com",
|
||||
"ohps.dnsbl.net.au",
|
||||
"omrs.dnsbl.net.au",
|
||||
"orvedb.aupads.org",
|
||||
"osps.dnsbl.net.au",
|
||||
"osrs.dnsbl.net.au",
|
||||
"owfs.dnsbl.net.au",
|
||||
"owps.dnsbl.net.au"
|
||||
"pbl.spamhaus.org",
|
||||
"phishing.rbl.msrbl.net",
|
||||
"probes.dnsbl.net.au"
|
||||
"proxy.bl.gweep.ca",
|
||||
"proxy.block.transip.nl",
|
||||
"psbl.surriel.com",
|
||||
"rbl.interserver.net",
|
||||
"rdts.dnsbl.net.au",
|
||||
"relays.bl.gweep.ca",
|
||||
"relays.bl.kundenserver.de",
|
||||
"relays.nether.net",
|
||||
"residential.block.transip.nl",
|
||||
"ricn.dnsbl.net.au",
|
||||
"rmst.dnsbl.net.au",
|
||||
"sbl.spamhaus.org",
|
||||
"short.rbl.jp",
|
||||
"smtp.dnsbl.sorbs.net",
|
||||
"socks.dnsbl.sorbs.net",
|
||||
"spam.abuse.ch",
|
||||
"spam.dnsbl.sorbs.net",
|
||||
"spam.rbl.msrbl.net",
|
||||
"spam.spamrats.com",
|
||||
"spamlist.or.kr",
|
||||
"spamrbl.imp.ch",
|
||||
"t3direct.dnsbl.net.au",
|
||||
"tor.dnsbl.sectoor.de",
|
||||
"torserver.tor.dnsbl.sectoor.de",
|
||||
"ubl.lashback.com",
|
||||
"ubl.unsubscore.com",
|
||||
"virbl.bit.nl",
|
||||
"virus.rbl.jp",
|
||||
"virus.rbl.msrbl.net",
|
||||
"web.dnsbl.sorbs.net",
|
||||
"wormrbl.imp.ch",
|
||||
"xbl.spamhaus.org",
|
||||
"zen.spamhaus.org",
|
||||
"zombie.dnsbl.sorbs.net",
|
||||
]
|
||||
|
||||
# Do not run tests for these domains.
|
||||
@@ -104,3 +34,11 @@ DKIM_KEY_LENGTHS = [
|
||||
(2048, "2048"),
|
||||
(4096, "4096"),
|
||||
]
|
||||
|
||||
ALARM_OPENED = 1
|
||||
ALARM_CLOSED = 2
|
||||
|
||||
ALARM_STATUSES = [
|
||||
(ALARM_OPENED, _("Opened")),
|
||||
(ALARM_CLOSED, _("Closed")),
|
||||
]
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Fixtures factories.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import factory
|
||||
|
||||
from modoboa.core.factories import PermissionFactory, UserFactory
|
||||
@@ -20,6 +16,7 @@ class DomainFactory(PermissionFactory):
|
||||
model = models.Domain
|
||||
django_get_or_create = ("name", )
|
||||
|
||||
name = factory.Sequence(lambda n: "domain{}.test".format(n))
|
||||
type = "domain" # NOQA:A003
|
||||
quota = 0
|
||||
default_mailbox_quota = 10
|
||||
@@ -74,6 +71,16 @@ class SenderAddressFactory(factory.django.DjangoModelFactory):
|
||||
model = models.SenderAddress
|
||||
|
||||
|
||||
class AlarmFactory(factory.django.DjangoModelFactory):
|
||||
"""Factory for Alarm model."""
|
||||
|
||||
domain = factory.SubFactory(DomainFactory)
|
||||
mailbox = factory.SubFactory(MailboxFactory)
|
||||
|
||||
class Meta:
|
||||
model = models.Alarm
|
||||
|
||||
|
||||
def populate_database():
|
||||
"""Create test data.
|
||||
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .account import (
|
||||
AccountForm, AccountFormGeneral, AccountFormMail, AccountPermissionsForm,
|
||||
AccountWizard
|
||||
@@ -10,7 +6,7 @@ from .alias import AliasForm
|
||||
from .domain import (
|
||||
DomainForm, DomainFormGeneral, DomainFormOptions, DomainWizard
|
||||
)
|
||||
from .export import ExportDataForm, ExportDomainsForm, ExportIdentitiesForm
|
||||
from .export import ExportDataForm
|
||||
from .forward import ForwardForm
|
||||
from .import_ import ImportDataForm, ImportIdentitiesForm
|
||||
|
||||
@@ -18,6 +14,6 @@ __all__ = [
|
||||
"DomainFormGeneral", "DomainFormOptions", "DomainForm",
|
||||
"AccountFormGeneral", "AccountFormMail", "AccountPermissionsForm",
|
||||
"AccountForm", "AliasForm", "ImportDataForm", "ImportIdentitiesForm",
|
||||
"ExportDataForm", "ExportDomainsForm", "ExportIdentitiesForm",
|
||||
"ExportDataForm",
|
||||
"ForwardForm", "DomainWizard", "AccountWizard"
|
||||
]
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Forms related to accounts management."""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import OrderedDict
|
||||
from functools import reduce
|
||||
|
||||
@@ -23,7 +19,7 @@ from modoboa.lib.form_utils import (
|
||||
)
|
||||
from modoboa.lib.permissions import get_account_roles
|
||||
from modoboa.lib.validators import validate_utf8_email
|
||||
from modoboa.lib.web_utils import render_to_json_response
|
||||
from modoboa.lib.web_utils import render_to_json_response, size2integer
|
||||
from modoboa.parameters import tools as param_tools
|
||||
from .. import lib, models, signals
|
||||
|
||||
@@ -229,16 +225,26 @@ class AccountFormMail(forms.Form, DynamicForm):
|
||||
required=False,
|
||||
initial=False
|
||||
)
|
||||
quota = forms.IntegerField(
|
||||
quota = forms.CharField(
|
||||
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)."),
|
||||
help_text=_(
|
||||
"Quota for this mailbox, can be expressed in KB, MB (default) or "
|
||||
"GB. 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)
|
||||
message_limit = forms.IntegerField(
|
||||
label=ugettext_lazy("Message sending limit"),
|
||||
required=False,
|
||||
min_value=0,
|
||||
help_text=ugettext_lazy(
|
||||
"Number of messages this mailbox can send per day")
|
||||
)
|
||||
aliases = lib_fields.UTF8AndEmptyUserEmailField(
|
||||
label=ugettext_lazy("Alias(es)"),
|
||||
required=False,
|
||||
@@ -262,10 +268,11 @@ class AccountFormMail(forms.Form, DynamicForm):
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
self.mb = kwargs.pop("instance", None)
|
||||
self.user = user
|
||||
super(AccountFormMail, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.field_widths = {
|
||||
"quota": 3
|
||||
}
|
||||
params = dict(param_tools.get_global_parameters("admin"))
|
||||
if self.mb is not None:
|
||||
self.fields["email"].required = True
|
||||
qset = self.mb.aliasrecipient_set.filter(alias__internal=False)
|
||||
@@ -283,13 +290,17 @@ class AccountFormMail(forms.Form, DynamicForm):
|
||||
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
|
||||
if self.mb.message_limit:
|
||||
self.fields["message_limit"].initial = self.mb.message_limit
|
||||
self.fields["create_alias_with_old_address"].initial = (
|
||||
param_tools.get_global_parameter(
|
||||
"create_alias_on_mbox_rename")
|
||||
params["create_alias_on_mbox_rename"]
|
||||
)
|
||||
else:
|
||||
del self.fields["create_alias_with_old_address"]
|
||||
self.fields["quota_act"].initial = True
|
||||
if params["default_mailbox_message_limit"] is not None:
|
||||
self.fields["message_limit"].initial = (
|
||||
params["default_mailbox_message_limit"])
|
||||
|
||||
if len(args) and isinstance(args[0], QueryDict):
|
||||
self._load_from_qdict(
|
||||
@@ -317,6 +328,10 @@ class AccountFormMail(forms.Form, DynamicForm):
|
||||
raise forms.ValidationError(inst)
|
||||
return email
|
||||
|
||||
def clean_quota(self):
|
||||
"""Convert quota to Bytes."""
|
||||
return size2integer(self.cleaned_data["quota"], output_unit="MB")
|
||||
|
||||
def clean(self):
|
||||
"""Custom fields validation.
|
||||
|
||||
@@ -364,7 +379,9 @@ class AccountFormMail(forms.Form, DynamicForm):
|
||||
self.__class__, context=user, klass=models.Mailbox)
|
||||
self.mb = models.Mailbox(
|
||||
address=self.locpart, domain=self.domain, user=account,
|
||||
use_domain_quota=self.cleaned_data["quota_act"])
|
||||
use_domain_quota=self.cleaned_data["quota_act"],
|
||||
message_limit=self.cleaned_data.get("message_limit")
|
||||
)
|
||||
override_rules = (
|
||||
user.is_superuser or
|
||||
user.has_perm("admin.add_domain") and
|
||||
@@ -473,6 +490,8 @@ class AccountPermissionsForm(forms.Form, DynamicForm):
|
||||
args[0], "domains", lib_fields.DomainNameField)
|
||||
|
||||
def save(self):
|
||||
if self.account.role == "SimpleUsers":
|
||||
return
|
||||
current_domains = [
|
||||
dom.name for dom in
|
||||
models.Domain.objects.get_for_admin(self.account)
|
||||
@@ -487,8 +506,7 @@ class AccountPermissionsForm(forms.Form, DynamicForm):
|
||||
domain.add_admin(self.account)
|
||||
|
||||
for domain in models.Domain.objects.get_for_admin(self.account):
|
||||
if not filter(lambda name: self.cleaned_data[name] == domain.name,
|
||||
self.cleaned_data.keys()):
|
||||
if domain.name not in self.cleaned_data.values():
|
||||
domain.remove_admin(self.account)
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Forms related to aliases management."""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from django import forms
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Forms related to domains management."""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from functools import reduce
|
||||
|
||||
from django import forms
|
||||
@@ -19,7 +15,7 @@ from modoboa.lib.fields import DomainNameField
|
||||
from modoboa.lib.form_utils import (
|
||||
DynamicForm, TabForms, WizardForm, WizardStep, YesNoField
|
||||
)
|
||||
from modoboa.lib.web_utils import render_to_json_response
|
||||
from modoboa.lib.web_utils import render_to_json_response, size2integer
|
||||
from modoboa.parameters import tools as param_tools
|
||||
from .. import constants, lib, signals
|
||||
from ..models import Alias, Domain, DomainAlias, Mailbox
|
||||
@@ -40,23 +36,43 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm):
|
||||
)
|
||||
)
|
||||
dkim_key_selector = forms.CharField(
|
||||
label=ugettext_lazy("Key selector"), required=False)
|
||||
label=ugettext_lazy("Key selector"), initial="modoboa", required=False)
|
||||
quota = forms.CharField(
|
||||
label=ugettext_lazy("Quota"),
|
||||
initial=0,
|
||||
help_text=ugettext_lazy(
|
||||
"Quota shared between mailboxes. Can be expressed in KB, "
|
||||
"MB (default) or GB. A value of 0 means no quota."
|
||||
)
|
||||
)
|
||||
default_mailbox_quota = forms.CharField(
|
||||
label=ugettext_lazy("Default mailbox quota"),
|
||||
initial=0,
|
||||
help_text=ugettext_lazy(
|
||||
"Default quota applied to mailboxes. Can be expressed in KB, MB "
|
||||
"(default) or GB. A value of 0 means no quota."
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Domain
|
||||
fields = (
|
||||
"name", "type", "quota", "default_mailbox_quota", "aliases",
|
||||
"enabled", "enable_dns_checks", "enable_dkim",
|
||||
"name", "type", "quota", "default_mailbox_quota", "message_limit",
|
||||
"aliases", "enabled", "enable_dns_checks", "enable_dkim",
|
||||
"dkim_key_selector", "dkim_key_length"
|
||||
)
|
||||
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
self.oldname = None
|
||||
if "instance" in kwargs:
|
||||
self.old_dkim_key_length = kwargs["instance"].dkim_key_length
|
||||
self.oldname = kwargs["instance"].name
|
||||
super(DomainFormGeneral, self).__init__(*args, **kwargs)
|
||||
params = dict(param_tools.get_global_parameters("admin"))
|
||||
self.fields["quota"].initial = params["default_domain_quota"]
|
||||
if params["default_domain_message_limit"] is not None:
|
||||
self.fields["message_limit"].initial = (
|
||||
params["default_domain_message_limit"])
|
||||
self.fields["default_mailbox_quota"].initial = (
|
||||
params["default_mailbox_quota"])
|
||||
self.fields["type"].choices = constants.DOMAIN_TYPES
|
||||
@@ -104,6 +120,15 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm):
|
||||
_("DKIM keys storage directory not configured"))
|
||||
return enabled
|
||||
|
||||
def clean_quota(self):
|
||||
"""Return proper quota value."""
|
||||
return size2integer(self.cleaned_data["quota"], output_unit="MB")
|
||||
|
||||
def clean_default_mailbox_quota(self):
|
||||
"""Return proper quota value."""
|
||||
return size2integer(
|
||||
self.cleaned_data["default_mailbox_quota"], output_unit="MB")
|
||||
|
||||
def clean(self):
|
||||
"""Custom fields validation.
|
||||
|
||||
@@ -374,7 +399,8 @@ class DomainForm(TabForms):
|
||||
|
||||
"""
|
||||
if "general" in self.instances:
|
||||
self.instances["general"].oldname = self.instances["general"].name
|
||||
instance = self.instances["general"]
|
||||
instance.oldname = instance.name
|
||||
return super(DomainForm, self).is_valid()
|
||||
|
||||
def save(self):
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
"""Export related forms."""
|
||||
|
||||
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,
|
||||
@@ -28,22 +20,3 @@ class ExportDataForm(forms.Form):
|
||||
if self.cleaned_data["sepchar"] == "":
|
||||
return ";"
|
||||
return self.cleaned_data["sepchar"]
|
||||
|
||||
def clean_filename(self):
|
||||
if self.cleaned_data["filename"] == "":
|
||||
return self.fields["filename"].initial
|
||||
return 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"
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Forms related to forwards management."""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django import forms
|
||||
from django.core import validators
|
||||
from django.utils.translation import ugettext_lazy
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Forms related to import operations."""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Django signal handlers for admin."""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import signals
|
||||
from django.dispatch import receiver
|
||||
@@ -258,7 +254,7 @@ def admin_menu(sender, location, user, **kwargs):
|
||||
if location != "top_menu":
|
||||
return []
|
||||
entries = []
|
||||
if user.has_perm("admin.view_domains"):
|
||||
if user.has_perm("admin.view_domain"):
|
||||
entries += [
|
||||
{"name": "domains",
|
||||
"url": reverse("admin:domain_list"),
|
||||
|
||||
+55
-38
@@ -1,13 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Internal library for admin."""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import ipaddress
|
||||
import logging
|
||||
import random
|
||||
import socket
|
||||
import string
|
||||
from functools import wraps
|
||||
from itertools import chain
|
||||
@@ -24,6 +19,7 @@ from modoboa.core import signals as core_signals
|
||||
from modoboa.core.models import User
|
||||
from modoboa.lib.exceptions import PermDeniedException
|
||||
from modoboa.parameters import tools as param_tools
|
||||
|
||||
from . import signals
|
||||
from .models import Alias, Domain, DomainAlias
|
||||
|
||||
@@ -171,49 +167,70 @@ def import_dlist(user, row, formopts):
|
||||
_import_alias(user, row)
|
||||
|
||||
|
||||
def get_domain_mx_list(domain):
|
||||
"""Return a list of MX IP address for domain."""
|
||||
result = []
|
||||
def get_dns_resolver():
|
||||
"""Return a DNS resolver object."""
|
||||
dns_server = param_tools.get_global_parameter("custom_dns_server")
|
||||
if dns_server:
|
||||
resolver = dns.resolver.Resolver()
|
||||
resolver.nameservers = [dns_server]
|
||||
else:
|
||||
resolver = dns.resolver
|
||||
return resolver
|
||||
|
||||
|
||||
def get_dns_records(name, typ, resolver=None):
|
||||
"""Retrieve DNS records for given name and type."""
|
||||
logger = logging.getLogger("modoboa.admin")
|
||||
if not resolver:
|
||||
resolver = get_dns_resolver()
|
||||
try:
|
||||
dns_answers = dns.resolver.query(domain, "MX")
|
||||
dns_answers = resolver.query(name, typ)
|
||||
except dns.resolver.NXDOMAIN as e:
|
||||
logger.error(_("No DNS records found for %s") % domain, exc_info=e)
|
||||
logger.error(_("No DNS record found for %s") % name, exc_info=e)
|
||||
except dns.resolver.NoAnswer as e:
|
||||
logger.error(_("No MX record for %s") % domain, exc_info=e)
|
||||
logger.error(
|
||||
_("No %(type)s record for %(name)s") % {"type": typ, "name": name},
|
||||
exc_info=e
|
||||
)
|
||||
except dns.resolver.NoNameservers as e:
|
||||
logger.error(_("No working name servers found"), exc_info=e)
|
||||
except dns.resolver.Timeout as e:
|
||||
logger.warning(
|
||||
_("DNS resolution timeout, unable to query %s at the moment") %
|
||||
domain, exc_info=e)
|
||||
name, exc_info=e)
|
||||
else:
|
||||
for dns_answer in dns_answers:
|
||||
try:
|
||||
mx_domain = dns_answer.exchange.to_unicode(
|
||||
omit_final_dot=True, idna_codec=IDNA_2008_UTS_46)
|
||||
ip_answers = socket.getaddrinfo(
|
||||
mx_domain, 25, 0, socket.SOCK_STREAM)
|
||||
except socket.gaierror as e:
|
||||
logger.warning(
|
||||
_("Unable to lookup ip addresses for %(domain)s; "
|
||||
"%(error)s")
|
||||
% {"domain": mx_domain, "error": str(e)},
|
||||
exc_info=e)
|
||||
else:
|
||||
for ip_answer in ip_answers:
|
||||
try:
|
||||
mx_ip = ipaddress.ip_address(
|
||||
smart_text(ip_answer[4][0]))
|
||||
except ValueError as e:
|
||||
logger.warning(
|
||||
_("Invalid IP address format for %(domain)s; "
|
||||
"%(addr)s")
|
||||
% {"domain": mx_domain,
|
||||
"addr": smart_text(ip_answer[4][0])},
|
||||
exc_info=e)
|
||||
else:
|
||||
result.append((mx_domain, mx_ip))
|
||||
return dns_answers
|
||||
return None
|
||||
|
||||
|
||||
def get_domain_mx_list(domain):
|
||||
"""Return a list of MX IP address for domain."""
|
||||
result = []
|
||||
logger = logging.getLogger("modoboa.admin")
|
||||
resolver = get_dns_resolver()
|
||||
dns_answers = get_dns_records(domain, "MX", resolver)
|
||||
if dns_answers is None:
|
||||
return result
|
||||
for dns_answer in dns_answers:
|
||||
mx_domain = dns_answer.exchange.to_unicode(
|
||||
omit_final_dot=True, idna_codec=IDNA_2008_UTS_46)
|
||||
for rtype in ["A", "AAAA"]:
|
||||
ip_answers = get_dns_records(mx_domain, rtype, resolver)
|
||||
if not ip_answers:
|
||||
continue
|
||||
for ip_answer in ip_answers:
|
||||
try:
|
||||
address_smart = smart_text(ip_answer.address)
|
||||
mx_ip = ipaddress.ip_address(address_smart)
|
||||
except ValueError as e:
|
||||
logger.warning(
|
||||
_("Invalid IP address format for "
|
||||
"{domain}; {addr}").format(
|
||||
domain=mx_domain,
|
||||
addr=smart_text(ip_answer.address)
|
||||
), exc_info=e)
|
||||
else:
|
||||
result.append((mx_domain, mx_ip))
|
||||
return result
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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:
|
||||
# Irriep Nala Novram <allannkorh@yahoo.fr>, 2019
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Modoboa\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-03-09 16:48+0100\n"
|
||||
"PO-Revision-Date: 2019-03-08 09:15+0000\n"
|
||||
"Last-Translator: Irriep Nala Novram <allannkorh@yahoo.fr>\n"
|
||||
"Language-Team: Breton (http://www.transifex.com/tonio/modoboa/language/br/)\n"
|
||||
"Language: br\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=5; plural=((n%10 == 1) && (n%100 != 11) && (n%100 !"
|
||||
"=71) && (n%100 !=91) ? 0 :(n%10 == 2) && (n%100 != 12) && (n%100 !=72) && (n"
|
||||
"%100 !=92) ? 1 :(n%10 ==3 || n%10==4 || n%10==9) && (n%100 < 10 || n% 100 > "
|
||||
"19) && (n%100 < 70 || n%100 > 79) && (n%100 < 90 || n%100 > 99) ? 2 :(n != 0 "
|
||||
"&& n % 1000000 == 0) ? 3 : 4);\n"
|
||||
|
||||
#: admin/static/admin/js/admin.js:129 admin/static/admin/js/admin.js:174
|
||||
msgid "No more domain to show"
|
||||
msgstr "Domani ebet ken da ziskouez"
|
||||
|
||||
#: admin/static/admin/js/admin.js:149
|
||||
msgid "Do not delete domain directory"
|
||||
msgstr "Na zilamit ket ar c'havlec'h domanioù"
|
||||
|
||||
#: admin/static/admin/js/admin.js:151
|
||||
msgid "This operation will remove ALL data associated to this domain."
|
||||
msgstr "An oberiadenn-mañ a zilamo an HOLL roadennoù stag ouzh an domani-mañ."
|
||||
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
msgid ""
|
||||
"This operation will remove all data associated to this domain, excepting "
|
||||
"accounts."
|
||||
msgstr ""
|
||||
"An oberiadenn-mañ a zilamo an HOLL roadennoù stag ouzh an domani-mañ, paneve "
|
||||
"ar c'hontoù."
|
||||
|
||||
#: admin/static/admin/js/admin.js:177 admin/static/admin/js/admin.js:433
|
||||
msgid "No more quota to show"
|
||||
msgstr "Kota ebet ken da ziskouez"
|
||||
|
||||
#: admin/static/admin/js/admin.js:306
|
||||
msgid "No domain administrator defined"
|
||||
msgstr "Merour domani ebet termenet"
|
||||
|
||||
#: admin/static/admin/js/admin.js:362 admin/static/admin/js/admin.js:430
|
||||
msgid "No more identity to show"
|
||||
msgstr "Identelezh ebet ken da ziskouez"
|
||||
|
||||
#: admin/static/admin/js/admin.js:404
|
||||
msgid "Do not delete mailbox directory"
|
||||
msgstr "Na zilamit ket kavlec'h ar voest-posteloù"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,53 +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:
|
||||
# Miroslav Abrahám <miris@kunago.com>, 2013,2015
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Modoboa\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2016-11-04 22:08+0100\n"
|
||||
"POT-Creation-Date: 2020-03-09 16:48+0100\n"
|
||||
"PO-Revision-Date: 2017-09-22 09:50+0000\n"
|
||||
"Last-Translator: Antoine Nguyen <tonio@ngyn.org>\n"
|
||||
"Language-Team: Czech (Czech Republic) (http://www.transifex.com/tonio/modoboa/language/cs_CZ/)\n"
|
||||
"Language-Team: Czech (Czech Republic) (http://www.transifex.com/tonio/"
|
||||
"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"
|
||||
"Language: cs_CZ\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n "
|
||||
"<= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n"
|
||||
|
||||
#: admin/static/admin/js/admin.js:131
|
||||
#: admin/static/admin/js/admin.js:129 admin/static/admin/js/admin.js:174
|
||||
msgid "No more domain to show"
|
||||
msgstr "Žádná další doména k zobrazení"
|
||||
|
||||
#: admin/static/admin/js/admin.js:150
|
||||
#: admin/static/admin/js/admin.js:149
|
||||
msgid "Do not delete domain directory"
|
||||
msgstr "Nemazat adresář domény"
|
||||
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
#: admin/static/admin/js/admin.js:151
|
||||
msgid "This operation will remove ALL data associated to this domain."
|
||||
msgstr "Tento krok odstraní VŠECHNA data související s touto doménou."
|
||||
|
||||
#: admin/static/admin/js/admin.js:153
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
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ů."
|
||||
msgstr ""
|
||||
"Tento krok odstraní VŠECHNA data související s touto doménou, vyjma účtů."
|
||||
|
||||
#: admin/static/admin/js/admin.js:236
|
||||
#: admin/static/admin/js/admin.js:177 admin/static/admin/js/admin.js:433
|
||||
msgid "No more quota to show"
|
||||
msgstr "Žádná další kvóta k zobrazení"
|
||||
|
||||
#: admin/static/admin/js/admin.js:306
|
||||
msgid "No domain administrator defined"
|
||||
msgstr "Není určen správce domény"
|
||||
|
||||
#: admin/static/admin/js/admin.js:291 admin/static/admin/js/admin.js:359
|
||||
#: admin/static/admin/js/admin.js:362 admin/static/admin/js/admin.js:430
|
||||
msgid "No more identity to show"
|
||||
msgstr "Žádná další identita k zobrazení"
|
||||
|
||||
#: admin/static/admin/js/admin.js:333
|
||||
#: admin/static/admin/js/admin.js:404
|
||||
msgid "Do not delete mailbox directory"
|
||||
msgstr "Nemazat adresář schránky"
|
||||
|
||||
#: admin/static/admin/js/admin.js:362
|
||||
msgid "No more quota to show"
|
||||
msgstr "Žádná další kvóta k zobrazení"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,30 +9,29 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Modoboa\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2016-11-04 22:08+0100\n"
|
||||
"PO-Revision-Date: 2014-12-01 12:12+0000\n"
|
||||
"Last-Translator: Arvedui <arvedui@posteo.de>\n"
|
||||
"Language-Team: German (http://www.transifex.com/projects/p/modoboa/language/"
|
||||
"de/)\n"
|
||||
"POT-Creation-Date: 2020-03-09 16:48+0100\n"
|
||||
"PO-Revision-Date: 2017-09-22 09:50+0000\n"
|
||||
"Last-Translator: Antoine Nguyen <tonio@ngyn.org>\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/static/admin/js/admin.js:131
|
||||
#: admin/static/admin/js/admin.js:129 admin/static/admin/js/admin.js:174
|
||||
msgid "No more domain to show"
|
||||
msgstr "Keine weiteren Domains"
|
||||
|
||||
#: admin/static/admin/js/admin.js:150
|
||||
#: admin/static/admin/js/admin.js:149
|
||||
msgid "Do not delete domain directory"
|
||||
msgstr "Verzeichnis der Domain nicht löschen"
|
||||
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
#: admin/static/admin/js/admin.js:151
|
||||
msgid "This operation will remove ALL data associated to this domain."
|
||||
msgstr "Diese Aktion wird ALLE mit der Domain verbundenen Daten löschen."
|
||||
|
||||
#: admin/static/admin/js/admin.js:153
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
msgid ""
|
||||
"This operation will remove all data associated to this domain, excepting "
|
||||
"accounts."
|
||||
@@ -40,18 +39,18 @@ msgstr ""
|
||||
"Diese Aktion wird ALLE mit der Domain verbundenen Daten, außer Mailkonten, "
|
||||
"löschen."
|
||||
|
||||
#: admin/static/admin/js/admin.js:236
|
||||
#: admin/static/admin/js/admin.js:177 admin/static/admin/js/admin.js:433
|
||||
msgid "No more quota to show"
|
||||
msgstr "Keine weiteren Quotas"
|
||||
|
||||
#: admin/static/admin/js/admin.js:306
|
||||
msgid "No domain administrator defined"
|
||||
msgstr "Domain-Administrator fehlt"
|
||||
|
||||
#: admin/static/admin/js/admin.js:291 admin/static/admin/js/admin.js:359
|
||||
#: admin/static/admin/js/admin.js:362 admin/static/admin/js/admin.js:430
|
||||
msgid "No more identity to show"
|
||||
msgstr "Keine weiteren Identitäten"
|
||||
|
||||
#: admin/static/admin/js/admin.js:333
|
||||
#: admin/static/admin/js/admin.js:404
|
||||
msgid "Do not delete mailbox directory"
|
||||
msgstr "Verzeichnis der Mailbox nicht löschen"
|
||||
|
||||
#: admin/static/admin/js/admin.js:362
|
||||
msgid "No more quota to show"
|
||||
msgstr "Keine weiteren Quotas"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
#
|
||||
# Translators:
|
||||
# Giannis Kapetanakis <bilias@edu.physics.uoc.gr>, 2016
|
||||
# Kostas Moumouris <moumouriskostas@gmail.com>, 2017
|
||||
@@ -9,46 +9,51 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Modoboa\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2016-11-04 22:08+0100\n"
|
||||
"POT-Creation-Date: 2020-03-09 16:48+0100\n"
|
||||
"PO-Revision-Date: 2017-10-05 20:01+0000\n"
|
||||
"Last-Translator: Kostas Moumouris <moumouriskostas@gmail.com>\n"
|
||||
"Language-Team: Greek (Greece) (http://www.transifex.com/tonio/modoboa/language/el_GR/)\n"
|
||||
"Language-Team: Greek (Greece) (http://www.transifex.com/tonio/modoboa/"
|
||||
"language/el_GR/)\n"
|
||||
"Language: el_GR\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: el_GR\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: admin/static/admin/js/admin.js:131
|
||||
#: admin/static/admin/js/admin.js:129 admin/static/admin/js/admin.js:174
|
||||
msgid "No more domain to show"
|
||||
msgstr "Δεν υπάρχουν άλλα domains για εμφάνιση"
|
||||
|
||||
#: admin/static/admin/js/admin.js:150
|
||||
#: admin/static/admin/js/admin.js:149
|
||||
msgid "Do not delete domain directory"
|
||||
msgstr "Να μην διαγραφεί ο κατάλογος αρχείων του domain "
|
||||
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
#: admin/static/admin/js/admin.js:151
|
||||
msgid "This operation will remove ALL data associated to this domain."
|
||||
msgstr "Αυτή η λειτουργία θα καταργήσει ΟΛΑ τα δεδομένα που σχετίζονται με αυτό το domain."
|
||||
msgstr ""
|
||||
"Αυτή η λειτουργία θα καταργήσει ΟΛΑ τα δεδομένα που σχετίζονται με αυτό το "
|
||||
"domain."
|
||||
|
||||
#: admin/static/admin/js/admin.js:153
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
msgid ""
|
||||
"This operation will remove all data associated to this domain, excepting "
|
||||
"accounts."
|
||||
msgstr "Αυτή η λειτουργία θα καταργήσει όλα τα δεδομένα που σχετίζονται με αυτό το domain, εκτός από τους λογαριασμούς χρηστών."
|
||||
msgstr ""
|
||||
"Αυτή η λειτουργία θα καταργήσει όλα τα δεδομένα που σχετίζονται με αυτό το "
|
||||
"domain, εκτός από τους λογαριασμούς χρηστών."
|
||||
|
||||
#: admin/static/admin/js/admin.js:236
|
||||
#: admin/static/admin/js/admin.js:177 admin/static/admin/js/admin.js:433
|
||||
msgid "No more quota to show"
|
||||
msgstr "Δεν υπάρχει κανένα quota"
|
||||
|
||||
#: admin/static/admin/js/admin.js:306
|
||||
msgid "No domain administrator defined"
|
||||
msgstr "Δεν έχει οριστεί διαχειριστής domain"
|
||||
|
||||
#: admin/static/admin/js/admin.js:291 admin/static/admin/js/admin.js:359
|
||||
#: admin/static/admin/js/admin.js:362 admin/static/admin/js/admin.js:430
|
||||
msgid "No more identity to show"
|
||||
msgstr "Δεν υπάρχουν άλλες ταυτότητες για εμφάνιση"
|
||||
|
||||
#: admin/static/admin/js/admin.js:333
|
||||
#: admin/static/admin/js/admin.js:404
|
||||
msgid "Do not delete mailbox directory"
|
||||
msgstr "Να μην διαγραφεί ο κατάλογος αρχείων του γραμματοκιβωτίου "
|
||||
|
||||
#: admin/static/admin/js/admin.js:362
|
||||
msgid "No more quota to show"
|
||||
msgstr "Δεν υπάρχει κανένα quota"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2016-11-04 22:08+0100\n"
|
||||
"POT-Creation-Date: 2020-03-09 16:48+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -17,36 +17,36 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: admin/static/admin/js/admin.js:131
|
||||
#: admin/static/admin/js/admin.js:129 admin/static/admin/js/admin.js:174
|
||||
msgid "No more domain to show"
|
||||
msgstr ""
|
||||
|
||||
#: admin/static/admin/js/admin.js:150
|
||||
#: admin/static/admin/js/admin.js:149
|
||||
msgid "Do not delete domain directory"
|
||||
msgstr ""
|
||||
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
#: admin/static/admin/js/admin.js:151
|
||||
msgid "This operation will remove ALL data associated to this domain."
|
||||
msgstr ""
|
||||
|
||||
#: admin/static/admin/js/admin.js:153
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
msgid ""
|
||||
"This operation will remove all data associated to this domain, excepting "
|
||||
"accounts."
|
||||
msgstr ""
|
||||
|
||||
#: admin/static/admin/js/admin.js:236
|
||||
#: admin/static/admin/js/admin.js:177 admin/static/admin/js/admin.js:433
|
||||
msgid "No more quota to show"
|
||||
msgstr ""
|
||||
|
||||
#: admin/static/admin/js/admin.js:306
|
||||
msgid "No domain administrator defined"
|
||||
msgstr ""
|
||||
|
||||
#: admin/static/admin/js/admin.js:291 admin/static/admin/js/admin.js:359
|
||||
#: admin/static/admin/js/admin.js:362 admin/static/admin/js/admin.js:430
|
||||
msgid "No more identity to show"
|
||||
msgstr ""
|
||||
|
||||
#: admin/static/admin/js/admin.js:333
|
||||
#: admin/static/admin/js/admin.js:404
|
||||
msgid "Do not delete mailbox directory"
|
||||
msgstr ""
|
||||
|
||||
#: admin/static/admin/js/admin.js:362
|
||||
msgid "No more quota to show"
|
||||
msgstr ""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
# 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 <lasarux@neuroomante.com>, 2011,2013
|
||||
# Pedro Gracia <pedro.gracia@impulzia.com>, 2010-2011
|
||||
@@ -10,46 +10,49 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Modoboa\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2016-11-04 22:08+0100\n"
|
||||
"POT-Creation-Date: 2020-03-09 16:48+0100\n"
|
||||
"PO-Revision-Date: 2017-02-21 00:55+0000\n"
|
||||
"Last-Translator: samuel santos <sags9201@gmail.com>\n"
|
||||
"Language-Team: Spanish (http://www.transifex.com/tonio/modoboa/language/es/)\n"
|
||||
"Language-Team: Spanish (http://www.transifex.com/tonio/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"
|
||||
"Language: es\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: admin/static/admin/js/admin.js:131
|
||||
#: admin/static/admin/js/admin.js:129 admin/static/admin/js/admin.js:174
|
||||
msgid "No more domain to show"
|
||||
msgstr "Sin dominios que mostrar"
|
||||
|
||||
#: admin/static/admin/js/admin.js:150
|
||||
#: admin/static/admin/js/admin.js:149
|
||||
msgid "Do not delete domain directory"
|
||||
msgstr "No eleminar el directorio del dominio"
|
||||
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
#: admin/static/admin/js/admin.js:151
|
||||
msgid "This operation will remove ALL data associated to this domain."
|
||||
msgstr "Esta operación eliminará TODOS los datos asociados a este dominio."
|
||||
|
||||
#: admin/static/admin/js/admin.js:153
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
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."
|
||||
msgstr ""
|
||||
"Esta operación eliminará todos los datos asociados a este dominio, excepto "
|
||||
"las cuentas."
|
||||
|
||||
#: admin/static/admin/js/admin.js:236
|
||||
#: admin/static/admin/js/admin.js:177 admin/static/admin/js/admin.js:433
|
||||
msgid "No more quota to show"
|
||||
msgstr "Sin cuota para mostrar"
|
||||
|
||||
#: admin/static/admin/js/admin.js:306
|
||||
msgid "No domain administrator defined"
|
||||
msgstr "No hay un administrador de dominio definido"
|
||||
|
||||
#: admin/static/admin/js/admin.js:291 admin/static/admin/js/admin.js:359
|
||||
#: admin/static/admin/js/admin.js:362 admin/static/admin/js/admin.js:430
|
||||
msgid "No more identity to show"
|
||||
msgstr "Sin identidades que mostrar"
|
||||
|
||||
#: admin/static/admin/js/admin.js:333
|
||||
#: admin/static/admin/js/admin.js:404
|
||||
msgid "Do not delete mailbox directory"
|
||||
msgstr "No eliminar el directorio del mailbox"
|
||||
|
||||
#: admin/static/admin/js/admin.js:362
|
||||
msgid "No more quota to show"
|
||||
msgstr "Sin cuota para mostrar"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,52 +1,54 @@
|
||||
# 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: 2016-11-04 22:08+0100\n"
|
||||
"POT-Creation-Date: 2020-03-09 16:48+0100\n"
|
||||
"PO-Revision-Date: 2017-09-22 09:50+0000\n"
|
||||
"Last-Translator: Antoine Nguyen <tonio@ngyn.org>\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"
|
||||
"Language: fr\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: admin/static/admin/js/admin.js:131
|
||||
#: admin/static/admin/js/admin.js:129 admin/static/admin/js/admin.js:174
|
||||
msgid "No more domain to show"
|
||||
msgstr "Plus de domaine à afficher"
|
||||
|
||||
#: admin/static/admin/js/admin.js:150
|
||||
#: admin/static/admin/js/admin.js:149
|
||||
msgid "Do not delete domain directory"
|
||||
msgstr "Ne pas supprimer le répertoire du domaine"
|
||||
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
#: admin/static/admin/js/admin.js:151
|
||||
msgid "This operation will remove ALL data associated to this domain."
|
||||
msgstr "Cette opération supprimera TOUTES les données associées à ce domaine."
|
||||
|
||||
#: admin/static/admin/js/admin.js:153
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
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'exception des comptes."
|
||||
msgstr ""
|
||||
"Cette opération supprimera toutes les données associées à ce domaine à "
|
||||
"l'exception des comptes."
|
||||
|
||||
#: admin/static/admin/js/admin.js:236
|
||||
#: admin/static/admin/js/admin.js:177 admin/static/admin/js/admin.js:433
|
||||
msgid "No more quota to show"
|
||||
msgstr "Plus de quota à afficher"
|
||||
|
||||
#: admin/static/admin/js/admin.js:306
|
||||
msgid "No domain administrator defined"
|
||||
msgstr "Aucun administrateur de domaine défini"
|
||||
|
||||
#: admin/static/admin/js/admin.js:291 admin/static/admin/js/admin.js:359
|
||||
#: admin/static/admin/js/admin.js:362 admin/static/admin/js/admin.js:430
|
||||
msgid "No more identity to show"
|
||||
msgstr "Plus d'identité à afficher"
|
||||
|
||||
#: admin/static/admin/js/admin.js:333
|
||||
#: admin/static/admin/js/admin.js:404
|
||||
msgid "Do not delete mailbox directory"
|
||||
msgstr "Ne pas supprimer le répertoire de la boite aux lettres"
|
||||
|
||||
#: admin/static/admin/js/admin.js:362
|
||||
msgid "No more quota to show"
|
||||
msgstr "Plus de quota à afficher"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Modoboa\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2016-11-04 22:08+0100\n"
|
||||
"PO-Revision-Date: 2015-10-11 11:19+0000\n"
|
||||
"POT-Creation-Date: 2020-03-09 16:48+0100\n"
|
||||
"PO-Revision-Date: 2017-09-22 09:50+0000\n"
|
||||
"Last-Translator: Rocco <mors10@hotmail.com>\n"
|
||||
"Language-Team: Italian (http://www.transifex.com/tonio/modoboa/language/"
|
||||
"it/)\n"
|
||||
@@ -19,19 +19,19 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: admin/static/admin/js/admin.js:131
|
||||
#: admin/static/admin/js/admin.js:129 admin/static/admin/js/admin.js:174
|
||||
msgid "No more domain to show"
|
||||
msgstr "Non ci sono altri domini da mostrare"
|
||||
|
||||
#: admin/static/admin/js/admin.js:150
|
||||
#: admin/static/admin/js/admin.js:149
|
||||
msgid "Do not delete domain directory"
|
||||
msgstr "Non eliminare la cartella dei domini"
|
||||
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
#: admin/static/admin/js/admin.js:151
|
||||
msgid "This operation will remove ALL data associated to this domain."
|
||||
msgstr "Questa operazione eliminerà TUTTI i dati associati a questo dominio."
|
||||
|
||||
#: admin/static/admin/js/admin.js:153
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
msgid ""
|
||||
"This operation will remove all data associated to this domain, excepting "
|
||||
"accounts."
|
||||
@@ -39,18 +39,18 @@ msgstr ""
|
||||
"Questa operazione eliminerà tutti i dati associati a questo dominio tranne "
|
||||
"gli account"
|
||||
|
||||
#: admin/static/admin/js/admin.js:236
|
||||
#: admin/static/admin/js/admin.js:177 admin/static/admin/js/admin.js:433
|
||||
msgid "No more quota to show"
|
||||
msgstr "Non ci sono altre quote da mostrare"
|
||||
|
||||
#: admin/static/admin/js/admin.js:306
|
||||
msgid "No domain administrator defined"
|
||||
msgstr "Non è stato definito un amministratore di dominio"
|
||||
|
||||
#: admin/static/admin/js/admin.js:291 admin/static/admin/js/admin.js:359
|
||||
#: admin/static/admin/js/admin.js:362 admin/static/admin/js/admin.js:430
|
||||
msgid "No more identity to show"
|
||||
msgstr "Non ci sono altre identità da mostrare"
|
||||
|
||||
#: admin/static/admin/js/admin.js:333
|
||||
#: admin/static/admin/js/admin.js:404
|
||||
msgid "Do not delete mailbox directory"
|
||||
msgstr "Non eliminare la cartella della casella posta"
|
||||
|
||||
#: admin/static/admin/js/admin.js:362
|
||||
msgid "No more quota to show"
|
||||
msgstr "Non ci sono altre quote da mostrare"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Modoboa\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2016-11-04 22:08+0100\n"
|
||||
"POT-Creation-Date: 2020-03-09 16:48+0100\n"
|
||||
"PO-Revision-Date: 2016-06-07 07:01+0000\n"
|
||||
"Last-Translator: cwatanab <chihiro.watanabe@live.jp>\n"
|
||||
"Language-Team: Japanese (Japan) (http://www.transifex.com/tonio/modoboa/"
|
||||
@@ -19,19 +19,19 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#: admin/static/admin/js/admin.js:131
|
||||
#: admin/static/admin/js/admin.js:129 admin/static/admin/js/admin.js:174
|
||||
msgid "No more domain to show"
|
||||
msgstr "表示するドメインはありません。"
|
||||
|
||||
#: admin/static/admin/js/admin.js:150
|
||||
#: admin/static/admin/js/admin.js:149
|
||||
msgid "Do not delete domain directory"
|
||||
msgstr "ドメインディレクトリを削除しないでください"
|
||||
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
#: admin/static/admin/js/admin.js:151
|
||||
msgid "This operation will remove ALL data associated to this domain."
|
||||
msgstr "この操作は、このドメインに関連するすべてのデータを削除します。"
|
||||
|
||||
#: admin/static/admin/js/admin.js:153
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
msgid ""
|
||||
"This operation will remove all data associated to this domain, excepting "
|
||||
"accounts."
|
||||
@@ -39,18 +39,18 @@ msgstr ""
|
||||
"この操作は、アカウントを除いたこのドメインに関連するすべてのデータを削除しま"
|
||||
"す。"
|
||||
|
||||
#: admin/static/admin/js/admin.js:236
|
||||
#: admin/static/admin/js/admin.js:177 admin/static/admin/js/admin.js:433
|
||||
msgid "No more quota to show"
|
||||
msgstr "表示するクォータはありません"
|
||||
|
||||
#: admin/static/admin/js/admin.js:306
|
||||
msgid "No domain administrator defined"
|
||||
msgstr "ドメイン管理者が指定されていません"
|
||||
|
||||
#: admin/static/admin/js/admin.js:291 admin/static/admin/js/admin.js:359
|
||||
#: admin/static/admin/js/admin.js:362 admin/static/admin/js/admin.js:430
|
||||
msgid "No more identity to show"
|
||||
msgstr "表示する ID はありません"
|
||||
|
||||
#: admin/static/admin/js/admin.js:333
|
||||
#: admin/static/admin/js/admin.js:404
|
||||
msgid "Do not delete mailbox directory"
|
||||
msgstr "メールボックスディレクトリを削除しないでください"
|
||||
|
||||
#: admin/static/admin/js/admin.js:362
|
||||
msgid "No more quota to show"
|
||||
msgstr "表示するクォータはありません"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,57 +3,56 @@
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
# Harrie v.d. Vegt <hvdvegt@gmail.com>, 2016
|
||||
# TuxBrother <vrijwilligerswerk@wij.rs>, 2014
|
||||
# TuxBrother <vrijwilligerswerk@wij.rs>, 2014
|
||||
# Tuxis Internet Engineering V.O.F. <info@tuxis.nl>, 2017
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Modoboa\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2016-11-04 22:08+0100\n"
|
||||
"PO-Revision-Date: 2014-12-31 11:42+0000\n"
|
||||
"Last-Translator: TuxBrother <vrijwilligerswerk@wij.rs>\n"
|
||||
"Language-Team: Dutch (Netherlands) (http://www.transifex.com/projects/p/"
|
||||
"modoboa/language/nl_NL/)\n"
|
||||
"POT-Creation-Date: 2020-03-09 16:48+0100\n"
|
||||
"PO-Revision-Date: 2017-09-23 21:20+0000\n"
|
||||
"Last-Translator: Tuxis Internet Engineering V.O.F. <info@tuxis.nl>\n"
|
||||
"Language-Team: Dutch (Netherlands) (http://www.transifex.com/tonio/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/static/admin/js/admin.js:131
|
||||
#: admin/static/admin/js/admin.js:129 admin/static/admin/js/admin.js:174
|
||||
msgid "No more domain to show"
|
||||
msgstr "Geen domein meer om te weergeven"
|
||||
msgstr "Geen domeinen meer om weer te geven"
|
||||
|
||||
#: admin/static/admin/js/admin.js:150
|
||||
#: admin/static/admin/js/admin.js:149
|
||||
msgid "Do not delete domain directory"
|
||||
msgstr "De map van dit domein niet verwijderen (Zal de data ervan behouden)"
|
||||
msgstr "De map van dit domein niet verwijderen"
|
||||
|
||||
#: admin/static/admin/js/admin.js:151
|
||||
msgid "This operation will remove ALL data associated to this domain."
|
||||
msgstr "Deze actie zal ALLE data verwijderen die bij dit domein hoort."
|
||||
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
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?"
|
||||
|
||||
#: admin/static/admin/js/admin.js:153
|
||||
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."
|
||||
"accounts."
|
||||
|
||||
#: admin/static/admin/js/admin.js:236
|
||||
msgid "No domain administrator defined"
|
||||
msgstr "Geen administrator toegewezen"
|
||||
|
||||
#: admin/static/admin/js/admin.js:291 admin/static/admin/js/admin.js:359
|
||||
msgid "No more identity to show"
|
||||
msgstr "Geen identiteiten meer om te weergeven"
|
||||
|
||||
#: admin/static/admin/js/admin.js:333
|
||||
msgid "Do not delete mailbox directory"
|
||||
msgstr "Verwijder de map van deze mailbox niet (zal de data ervan behouden)"
|
||||
|
||||
#: admin/static/admin/js/admin.js:362
|
||||
#: admin/static/admin/js/admin.js:177 admin/static/admin/js/admin.js:433
|
||||
msgid "No more quota to show"
|
||||
msgstr "Geen quota meer om te weergeven"
|
||||
msgstr "Geen quotum meer om weer te geven"
|
||||
|
||||
#: admin/static/admin/js/admin.js:306
|
||||
msgid "No domain administrator defined"
|
||||
msgstr "Geen domeinbeheerder toegewezen"
|
||||
|
||||
#: admin/static/admin/js/admin.js:362 admin/static/admin/js/admin.js:430
|
||||
msgid "No more identity to show"
|
||||
msgstr "Geen identiteiten meer om weer te geven"
|
||||
|
||||
#: admin/static/admin/js/admin.js:404
|
||||
msgid "Do not delete mailbox directory"
|
||||
msgstr "Verwijder de map van deze mailbox niet"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,53 +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:
|
||||
# sin88 <sin@sin.waw.pl>, 2016
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Modoboa\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2016-11-04 22:08+0100\n"
|
||||
"POT-Creation-Date: 2020-03-09 16:48+0100\n"
|
||||
"PO-Revision-Date: 2017-09-22 09:50+0000\n"
|
||||
"Last-Translator: sin88 <sin@sin.waw.pl>\n"
|
||||
"Language-Team: Polish (Poland) (http://www.transifex.com/tonio/modoboa/language/pl_PL/)\n"
|
||||
"Language-Team: Polish (Poland) (http://www.transifex.com/tonio/modoboa/"
|
||||
"language/pl_PL/)\n"
|
||||
"Language: pl_PL\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: pl_PL\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n"
|
||||
"%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n"
|
||||
"%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
|
||||
|
||||
#: admin/static/admin/js/admin.js:131
|
||||
#: admin/static/admin/js/admin.js:129 admin/static/admin/js/admin.js:174
|
||||
msgid "No more domain to show"
|
||||
msgstr "Brak domen do pokazania"
|
||||
|
||||
#: admin/static/admin/js/admin.js:150
|
||||
#: admin/static/admin/js/admin.js:149
|
||||
msgid "Do not delete domain directory"
|
||||
msgstr "Nie usuwaj katalogu domeny"
|
||||
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
#: admin/static/admin/js/admin.js:151
|
||||
msgid "This operation will remove ALL data associated to this domain."
|
||||
msgstr "Ta operacja usunie WSZYSTKIE dane powiązane z tą domeną."
|
||||
|
||||
#: admin/static/admin/js/admin.js:153
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
msgid ""
|
||||
"This operation will remove all data associated to this domain, excepting "
|
||||
"accounts."
|
||||
msgstr "Ta operacja usunie wszystkie dane powiązana z tą domeną za wyjątkiem kont."
|
||||
msgstr ""
|
||||
"Ta operacja usunie wszystkie dane powiązana z tą domeną za wyjątkiem kont."
|
||||
|
||||
#: admin/static/admin/js/admin.js:236
|
||||
#: admin/static/admin/js/admin.js:177 admin/static/admin/js/admin.js:433
|
||||
msgid "No more quota to show"
|
||||
msgstr "Brak limitów do wyświetlenia"
|
||||
|
||||
#: admin/static/admin/js/admin.js:306
|
||||
msgid "No domain administrator defined"
|
||||
msgstr "Nie zdefiniowano administratora domeny"
|
||||
|
||||
#: admin/static/admin/js/admin.js:291 admin/static/admin/js/admin.js:359
|
||||
#: admin/static/admin/js/admin.js:362 admin/static/admin/js/admin.js:430
|
||||
msgid "No more identity to show"
|
||||
msgstr "Brak tożsamości do wyświetlenia"
|
||||
|
||||
#: admin/static/admin/js/admin.js:333
|
||||
#: admin/static/admin/js/admin.js:404
|
||||
msgid "Do not delete mailbox directory"
|
||||
msgstr "Nie usuwaj katalogu skrzynki pocztowej"
|
||||
|
||||
#: admin/static/admin/js/admin.js:362
|
||||
msgid "No more quota to show"
|
||||
msgstr "Brak limitów do wyświetlenia"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,53 +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 <paulino@michelazzo.com.br>, 2014
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Modoboa\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2016-11-04 22:08+0100\n"
|
||||
"POT-Creation-Date: 2020-03-09 16:48+0100\n"
|
||||
"PO-Revision-Date: 2017-09-23 21:20+0000\n"
|
||||
"Last-Translator: Rafael Barretto <ch3yd@hotmail.com>\n"
|
||||
"Language-Team: Portuguese (Brazil) (http://www.transifex.com/tonio/modoboa/language/pt_BR/)\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"
|
||||
"Language: pt_BR\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: admin/static/admin/js/admin.js:131
|
||||
#: admin/static/admin/js/admin.js:129 admin/static/admin/js/admin.js:174
|
||||
msgid "No more domain to show"
|
||||
msgstr "Nenhum domínio a mais para mostrar"
|
||||
|
||||
#: admin/static/admin/js/admin.js:150
|
||||
#: admin/static/admin/js/admin.js:149
|
||||
msgid "Do not delete domain directory"
|
||||
msgstr "Não remova o diretório de domínio"
|
||||
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
#: admin/static/admin/js/admin.js:151
|
||||
msgid "This operation will remove ALL data associated to this domain."
|
||||
msgstr "Esta operação removerá TODOS os dados associados com este domínio."
|
||||
|
||||
#: admin/static/admin/js/admin.js:153
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
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."
|
||||
msgstr ""
|
||||
"Esta operação removerá todos os dados associados com este domínio, exceto "
|
||||
"contas."
|
||||
|
||||
#: admin/static/admin/js/admin.js:236
|
||||
#: admin/static/admin/js/admin.js:177 admin/static/admin/js/admin.js:433
|
||||
msgid "No more quota to show"
|
||||
msgstr "Nenhuma quota a mais para mostrar"
|
||||
|
||||
#: admin/static/admin/js/admin.js:306
|
||||
msgid "No domain administrator defined"
|
||||
msgstr "Nenhum administrador de domínio definido"
|
||||
|
||||
#: admin/static/admin/js/admin.js:291 admin/static/admin/js/admin.js:359
|
||||
#: admin/static/admin/js/admin.js:362 admin/static/admin/js/admin.js:430
|
||||
msgid "No more identity to show"
|
||||
msgstr "Nenhuma identidade a mais para mostrar"
|
||||
|
||||
#: admin/static/admin/js/admin.js:333
|
||||
#: admin/static/admin/js/admin.js:404
|
||||
msgid "Do not delete mailbox directory"
|
||||
msgstr "Não remova o diretório de caixas de mensagens"
|
||||
|
||||
#: admin/static/admin/js/admin.js:362
|
||||
msgid "No more quota to show"
|
||||
msgstr "Nenhuma quota a mais para mostrar"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Modoboa\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2016-11-04 22:08+0100\n"
|
||||
"POT-Creation-Date: 2020-03-09 16:48+0100\n"
|
||||
"PO-Revision-Date: 2014-12-29 12:08+0000\n"
|
||||
"Last-Translator: Mike C. <miguelmclara@gmail.com>\n"
|
||||
"Language-Team: Portuguese (Portugal) (http://www.transifex.com/projects/p/"
|
||||
@@ -24,19 +24,19 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: admin/static/admin/js/admin.js:131
|
||||
#: admin/static/admin/js/admin.js:129 admin/static/admin/js/admin.js:174
|
||||
msgid "No more domain to show"
|
||||
msgstr "Não existem mais dominios para mostrar"
|
||||
|
||||
#: admin/static/admin/js/admin.js:150
|
||||
#: admin/static/admin/js/admin.js:149
|
||||
msgid "Do not delete domain directory"
|
||||
msgstr "Não excluir o diretório do domínio"
|
||||
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
#: admin/static/admin/js/admin.js:151
|
||||
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."
|
||||
|
||||
#: admin/static/admin/js/admin.js:153
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
msgid ""
|
||||
"This operation will remove all data associated to this domain, excepting "
|
||||
"accounts."
|
||||
@@ -44,18 +44,18 @@ msgstr ""
|
||||
"Esta operação irá remover TODOS os dados associados a este domínio, excepto "
|
||||
"as contas."
|
||||
|
||||
#: admin/static/admin/js/admin.js:236
|
||||
#: admin/static/admin/js/admin.js:177 admin/static/admin/js/admin.js:433
|
||||
msgid "No more quota to show"
|
||||
msgstr "Não existem mais quotas para mostrar"
|
||||
|
||||
#: admin/static/admin/js/admin.js:306
|
||||
msgid "No domain administrator defined"
|
||||
msgstr "Não está definido nenhum administrador para o domínio"
|
||||
|
||||
#: admin/static/admin/js/admin.js:291 admin/static/admin/js/admin.js:359
|
||||
#: admin/static/admin/js/admin.js:362 admin/static/admin/js/admin.js:430
|
||||
msgid "No more identity to show"
|
||||
msgstr "Não existem mais identidades para mostrar"
|
||||
|
||||
#: admin/static/admin/js/admin.js:333
|
||||
#: admin/static/admin/js/admin.js:404
|
||||
msgid "Do not delete mailbox directory"
|
||||
msgstr "Não excluir o diretório da caixa de correio"
|
||||
|
||||
#: admin/static/admin/js/admin.js:362
|
||||
msgid "No more quota to show"
|
||||
msgstr "Não existem mais quotas para mostrar"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,53 +1,58 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
#
|
||||
# Translators:
|
||||
# Andrei-Ilie Olteanu <andrei@flashsoft.ro>, 2018
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Modoboa\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2016-11-04 22:08+0100\n"
|
||||
"POT-Creation-Date: 2020-03-09 16:48+0100\n"
|
||||
"PO-Revision-Date: 2018-06-12 11:38+0000\n"
|
||||
"Last-Translator: Andrei-Ilie Olteanu <andrei@flashsoft.ro>\n"
|
||||
"Language-Team: Romanian (Romania) (http://www.transifex.com/tonio/modoboa/language/ro_RO/)\n"
|
||||
"Language-Team: Romanian (Romania) (http://www.transifex.com/tonio/modoboa/"
|
||||
"language/ro_RO/)\n"
|
||||
"Language: ro_RO\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: ro_RO\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?"
|
||||
"2:1));\n"
|
||||
|
||||
#: admin/static/admin/js/admin.js:131
|
||||
#: admin/static/admin/js/admin.js:129 admin/static/admin/js/admin.js:174
|
||||
msgid "No more domain to show"
|
||||
msgstr "Nu mai sunt domenii de afișat"
|
||||
|
||||
#: admin/static/admin/js/admin.js:150
|
||||
#: admin/static/admin/js/admin.js:149
|
||||
msgid "Do not delete domain directory"
|
||||
msgstr "Nu sterge directorul pentru domeniu"
|
||||
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
#: admin/static/admin/js/admin.js:151
|
||||
msgid "This operation will remove ALL data associated to this domain."
|
||||
msgstr "Această operațiune va șterge toată informația asociată cu acest domeniu."
|
||||
msgstr ""
|
||||
"Această operațiune va șterge toată informația asociată cu acest domeniu."
|
||||
|
||||
#: admin/static/admin/js/admin.js:153
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
msgid ""
|
||||
"This operation will remove all data associated to this domain, excepting "
|
||||
"accounts."
|
||||
msgstr "Această operațiune va șterge toată informația asociată cu acest domeniu, cu excepția conturilor."
|
||||
msgstr ""
|
||||
"Această operațiune va șterge toată informația asociată cu acest domeniu, cu "
|
||||
"excepția conturilor."
|
||||
|
||||
#: admin/static/admin/js/admin.js:236
|
||||
#: admin/static/admin/js/admin.js:177 admin/static/admin/js/admin.js:433
|
||||
msgid "No more quota to show"
|
||||
msgstr "Nu mai există cote de afișat"
|
||||
|
||||
#: admin/static/admin/js/admin.js:306
|
||||
msgid "No domain administrator defined"
|
||||
msgstr "Nu există configurat un administartor de domeniu"
|
||||
|
||||
#: admin/static/admin/js/admin.js:291 admin/static/admin/js/admin.js:359
|
||||
#: admin/static/admin/js/admin.js:362 admin/static/admin/js/admin.js:430
|
||||
msgid "No more identity to show"
|
||||
msgstr "Nu mai sunt identități de afișat"
|
||||
|
||||
#: admin/static/admin/js/admin.js:333
|
||||
#: admin/static/admin/js/admin.js:404
|
||||
msgid "Do not delete mailbox directory"
|
||||
msgstr "Nu sterge directorul mailbox"
|
||||
|
||||
#: admin/static/admin/js/admin.js:362
|
||||
msgid "No more quota to show"
|
||||
msgstr "Nu mai există cote de afișat"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,35 +3,38 @@
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
# Ravil Kadyrbayev, 2015
|
||||
# Yuriy Matskanyuk <umackanuk@gmail.com>, 2018
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Modoboa\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2016-11-04 22:08+0100\n"
|
||||
"PO-Revision-Date: 2013-12-03 09:35+0000\n"
|
||||
"Last-Translator: Antoine Nguyen <tonio@ngyn.org>\n"
|
||||
"Language-Team: Russian (http://www.transifex.com/projects/p/modoboa/language/"
|
||||
"POT-Creation-Date: 2020-03-09 16:48+0100\n"
|
||||
"PO-Revision-Date: 2018-11-27 17:54+0000\n"
|
||||
"Last-Translator: Yuriy Matskanyuk <umackanuk@gmail.com>\n"
|
||||
"Language-Team: Russian (http://www.transifex.com/tonio/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"
|
||||
"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
|
||||
"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n"
|
||||
"%100>=11 && n%100<=14)? 2 : 3);\n"
|
||||
|
||||
#: admin/static/admin/js/admin.js:131
|
||||
#: admin/static/admin/js/admin.js:129 admin/static/admin/js/admin.js:174
|
||||
msgid "No more domain to show"
|
||||
msgstr ""
|
||||
msgstr "Нет больше доменов для показа"
|
||||
|
||||
#: admin/static/admin/js/admin.js:150
|
||||
#: admin/static/admin/js/admin.js:149
|
||||
msgid "Do not delete domain directory"
|
||||
msgstr "Не удаляйте каталог домена"
|
||||
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
#: admin/static/admin/js/admin.js:151
|
||||
msgid "This operation will remove ALL data associated to this domain."
|
||||
msgstr "Эта операция удалит ВСЕ данные, связанные с этим доменом."
|
||||
|
||||
#: admin/static/admin/js/admin.js:153
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
msgid ""
|
||||
"This operation will remove all data associated to this domain, excepting "
|
||||
"accounts."
|
||||
@@ -39,18 +42,18 @@ msgstr ""
|
||||
"Эта операция удалит ВСЕ данные, связанные с этим доменом, за исключением "
|
||||
"аккаунтов."
|
||||
|
||||
#: admin/static/admin/js/admin.js:236
|
||||
#: admin/static/admin/js/admin.js:177 admin/static/admin/js/admin.js:433
|
||||
msgid "No more quota to show"
|
||||
msgstr "Нет больше квоты для показа"
|
||||
|
||||
#: admin/static/admin/js/admin.js:306
|
||||
msgid "No domain administrator defined"
|
||||
msgstr "Не указан администратор домена"
|
||||
|
||||
#: admin/static/admin/js/admin.js:291 admin/static/admin/js/admin.js:359
|
||||
#: admin/static/admin/js/admin.js:362 admin/static/admin/js/admin.js:430
|
||||
msgid "No more identity to show"
|
||||
msgstr ""
|
||||
msgstr "Нет личности для показа"
|
||||
|
||||
#: admin/static/admin/js/admin.js:333
|
||||
#: admin/static/admin/js/admin.js:404
|
||||
msgid "Do not delete mailbox directory"
|
||||
msgstr "Не удаляйте каталог почтовых ящиков"
|
||||
|
||||
#: admin/static/admin/js/admin.js:362
|
||||
msgid "No more quota to show"
|
||||
msgstr ""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,53 +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:
|
||||
# Olle Gustafsson <olle@dalnix.se>, 2013,2015
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Modoboa\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2016-11-04 22:08+0100\n"
|
||||
"POT-Creation-Date: 2020-03-09 16:48+0100\n"
|
||||
"PO-Revision-Date: 2017-09-22 09:50+0000\n"
|
||||
"Last-Translator: Antoine Nguyen <tonio@ngyn.org>\n"
|
||||
"Language-Team: Swedish (http://www.transifex.com/tonio/modoboa/language/sv/)\n"
|
||||
"Language-Team: Swedish (http://www.transifex.com/tonio/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"
|
||||
"Language: sv\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: admin/static/admin/js/admin.js:131
|
||||
#: admin/static/admin/js/admin.js:129 admin/static/admin/js/admin.js:174
|
||||
msgid "No more domain to show"
|
||||
msgstr "Inga fler domäner att visa"
|
||||
|
||||
#: admin/static/admin/js/admin.js:150
|
||||
#: admin/static/admin/js/admin.js:149
|
||||
msgid "Do not delete domain directory"
|
||||
msgstr "Radera inte domän katalog"
|
||||
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
#: admin/static/admin/js/admin.js:151
|
||||
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."
|
||||
|
||||
#: admin/static/admin/js/admin.js:153
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
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."
|
||||
msgstr ""
|
||||
"Den här operationen raderar all data som är associerad med denna domän, "
|
||||
"förutom konton."
|
||||
|
||||
#: admin/static/admin/js/admin.js:236
|
||||
#: admin/static/admin/js/admin.js:177 admin/static/admin/js/admin.js:433
|
||||
msgid "No more quota to show"
|
||||
msgstr "Ingen mer kvota att visa"
|
||||
|
||||
#: admin/static/admin/js/admin.js:306
|
||||
msgid "No domain administrator defined"
|
||||
msgstr "Ingen domänadministratör definierad"
|
||||
|
||||
#: admin/static/admin/js/admin.js:291 admin/static/admin/js/admin.js:359
|
||||
#: admin/static/admin/js/admin.js:362 admin/static/admin/js/admin.js:430
|
||||
msgid "No more identity to show"
|
||||
msgstr "Inga fler konton att visa"
|
||||
|
||||
#: admin/static/admin/js/admin.js:333
|
||||
#: admin/static/admin/js/admin.js:404
|
||||
msgid "Do not delete mailbox directory"
|
||||
msgstr "Radera inte brevlådans katalog"
|
||||
|
||||
#: admin/static/admin/js/admin.js:362
|
||||
msgid "No more quota to show"
|
||||
msgstr "Ingen mer kvota att visa"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
# Ediz Duman <neps1192@gmail.com>, 2016
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Modoboa\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-03-09 16:48+0100\n"
|
||||
"PO-Revision-Date: 2017-09-22 09:50+0000\n"
|
||||
"Last-Translator: Ediz Duman <neps1192@gmail.com>\n"
|
||||
"Language-Team: Turkish (Turkey) (http://www.transifex.com/tonio/modoboa/"
|
||||
"language/tr_TR/)\n"
|
||||
"Language: tr_TR\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/static/admin/js/admin.js:129 admin/static/admin/js/admin.js:174
|
||||
msgid "No more domain to show"
|
||||
msgstr "Gösterilecek başka domain yok"
|
||||
|
||||
#: admin/static/admin/js/admin.js:149
|
||||
msgid "Do not delete domain directory"
|
||||
msgstr "Domain dizini silmeyin"
|
||||
|
||||
#: admin/static/admin/js/admin.js:151
|
||||
msgid "This operation will remove ALL data associated to this domain."
|
||||
msgstr "Bu işlem, bu domain ilişkili TÜM verileri kaldıracaktır."
|
||||
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
msgid ""
|
||||
"This operation will remove all data associated to this domain, excepting "
|
||||
"accounts."
|
||||
msgstr "Bu işlem hesap dışında bu domain ilişkili tüm verileri kaldıracaktır."
|
||||
|
||||
#: admin/static/admin/js/admin.js:177 admin/static/admin/js/admin.js:433
|
||||
msgid "No more quota to show"
|
||||
msgstr "Gösterilecek kota yok"
|
||||
|
||||
#: admin/static/admin/js/admin.js:306
|
||||
msgid "No domain administrator defined"
|
||||
msgstr "domain yöneticisi tanımlanmadı"
|
||||
|
||||
#: admin/static/admin/js/admin.js:362 admin/static/admin/js/admin.js:430
|
||||
msgid "No more identity to show"
|
||||
msgstr "Gösterilecek daha fazla kimlik yok"
|
||||
|
||||
#: admin/static/admin/js/admin.js:404
|
||||
msgid "Do not delete mailbox directory"
|
||||
msgstr "Posta kutusu dizini silmeyin"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
#
|
||||
# Translators:
|
||||
# akong <akong@aspa.idv.tw>, 2018
|
||||
# Radium <ztl8702@126.com>, 2017
|
||||
@@ -9,46 +9,47 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Modoboa\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2016-11-04 22:08+0100\n"
|
||||
"POT-Creation-Date: 2020-03-09 16:48+0100\n"
|
||||
"PO-Revision-Date: 2018-06-29 00:40+0000\n"
|
||||
"Last-Translator: akong <akong@aspa.idv.tw>\n"
|
||||
"Language-Team: Chinese (Taiwan) (http://www.transifex.com/tonio/modoboa/language/zh_TW/)\n"
|
||||
"Language-Team: Chinese (Taiwan) (http://www.transifex.com/tonio/modoboa/"
|
||||
"language/zh_TW/)\n"
|
||||
"Language: zh_TW\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: zh_TW\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#: admin/static/admin/js/admin.js:131
|
||||
#: admin/static/admin/js/admin.js:129 admin/static/admin/js/admin.js:174
|
||||
msgid "No more domain to show"
|
||||
msgstr "無更多網域可展示"
|
||||
|
||||
#: admin/static/admin/js/admin.js:150
|
||||
#: admin/static/admin/js/admin.js:149
|
||||
msgid "Do not delete domain directory"
|
||||
msgstr "不可以刪除網域目錄"
|
||||
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
#: admin/static/admin/js/admin.js:151
|
||||
msgid "This operation will remove ALL data associated to this domain."
|
||||
msgstr "這個操作將會刪除所有這個網域有關的資料。"
|
||||
|
||||
#: admin/static/admin/js/admin.js:153
|
||||
#: admin/static/admin/js/admin.js:152
|
||||
msgid ""
|
||||
"This operation will remove all data associated to this domain, excepting "
|
||||
"accounts."
|
||||
msgstr "這個操作將會刪除所有這個網域有關的資料,除了帳號。"
|
||||
|
||||
#: admin/static/admin/js/admin.js:236
|
||||
#: admin/static/admin/js/admin.js:177 admin/static/admin/js/admin.js:433
|
||||
msgid "No more quota to show"
|
||||
msgstr "沒有配額可能顯示"
|
||||
|
||||
#: admin/static/admin/js/admin.js:306
|
||||
msgid "No domain administrator defined"
|
||||
msgstr "網域管理員未指定"
|
||||
|
||||
#: admin/static/admin/js/admin.js:291 admin/static/admin/js/admin.js:359
|
||||
#: admin/static/admin/js/admin.js:362 admin/static/admin/js/admin.js:430
|
||||
msgid "No more identity to show"
|
||||
msgstr "沒有身份可以顯示"
|
||||
|
||||
#: admin/static/admin/js/admin.js:333
|
||||
#: admin/static/admin/js/admin.js:404
|
||||
msgid "Do not delete mailbox directory"
|
||||
msgstr "不可能刪除郵件目錄"
|
||||
|
||||
#: admin/static/admin/js/admin.js:362
|
||||
msgid "No more quota to show"
|
||||
msgstr "沒有配額可能顯示"
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""A management command to apply mailbox operations."""
|
||||
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Modoboa main management command."""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from subcommand.base import SubcommandCommand
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from .subcommands._export import ExportCommand
|
||||
from .subcommands._import import ImportCommand
|
||||
@@ -13,13 +9,13 @@ from .subcommands._mx import CheckMXRecords
|
||||
from .subcommands._repair import Repair
|
||||
|
||||
|
||||
class Command(SubcommandCommand):
|
||||
class Command(BaseCommand):
|
||||
"""Top management command for modoboa.
|
||||
|
||||
$ python manage.py modo
|
||||
"""
|
||||
|
||||
help = "Modoboa top management command." # NOQA:A003
|
||||
help = "Modoboa top management command."
|
||||
|
||||
subcommands = {
|
||||
"export": ExportCommand,
|
||||
@@ -28,3 +24,27 @@ class Command(SubcommandCommand):
|
||||
"manage_dkim_keys": ManageDKIMKeys,
|
||||
"repair": Repair,
|
||||
}
|
||||
|
||||
def add_arguments(self, parser):
|
||||
subparsers = parser.add_subparsers(
|
||||
dest='subcommand', title='subcommands'
|
||||
)
|
||||
# required argument is added in Python 3.7
|
||||
subparsers.required = True
|
||||
|
||||
for command_name, command_class in self.subcommands.items():
|
||||
command = command_class(self.stdout, self.stderr)
|
||||
command.style = self.style
|
||||
|
||||
subparser = subparsers.add_parser(
|
||||
command_name, help=command_class.help
|
||||
)
|
||||
# This is needed to output console friendly errors
|
||||
subparser.called_from_command_line = self._called_from_command_line
|
||||
|
||||
command.add_arguments(subparser)
|
||||
|
||||
subparser.set_defaults(command=command)
|
||||
|
||||
def execute(self, *args, **options):
|
||||
return options.pop('command').execute(*args, **options)
|
||||
|
||||
@@ -1,24 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Django management command to export admin objects."""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import sys
|
||||
import csv
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils import six
|
||||
from django.utils.encoding import smart_text
|
||||
|
||||
from modoboa.core.extensions import exts_pool
|
||||
from modoboa.core.models import User
|
||||
from .... import models
|
||||
|
||||
if six.PY2:
|
||||
from backports import csv
|
||||
else:
|
||||
import csv
|
||||
|
||||
|
||||
class ExportCommand(BaseCommand):
|
||||
"""Command class."""
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Django management command to import admin objects."""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import csv
|
||||
import io
|
||||
import os
|
||||
|
||||
@@ -12,7 +9,6 @@ from chardet.universaldetector import UniversalDetector
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import transaction
|
||||
from django.utils import six
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from modoboa.core import models as core_models
|
||||
@@ -20,11 +16,6 @@ from modoboa.core.extensions import exts_pool
|
||||
from modoboa.lib.exceptions import Conflict
|
||||
from .... import signals
|
||||
|
||||
if six.PY2:
|
||||
from backports import csv
|
||||
else:
|
||||
import csv
|
||||
|
||||
|
||||
class ImportCommand(BaseCommand):
|
||||
"""Command class."""
|
||||
@@ -35,7 +26,7 @@ class ImportCommand(BaseCommand):
|
||||
def add_arguments(self, parser):
|
||||
"""Add extra arguments to command."""
|
||||
parser.add_argument(
|
||||
"--sepchar", type=six.text_type, default=";",
|
||||
"--sepchar", type=str, default=";",
|
||||
help="Separator used in file.")
|
||||
parser.add_argument(
|
||||
"--continue-if-exists", action="store_true",
|
||||
@@ -45,7 +36,7 @@ class ImportCommand(BaseCommand):
|
||||
"--crypt-password", action="store_true",
|
||||
default=False, help="Encrypt provided passwords.")
|
||||
parser.add_argument(
|
||||
"files", type=six.text_type, nargs="+", help="CSV files to import.")
|
||||
"files", type=str, nargs="+", help="CSV files to import.")
|
||||
|
||||
def _import(self, filename, options, encoding="utf-8"):
|
||||
"""Import domains or identities."""
|
||||
@@ -121,10 +112,7 @@ class ImportCommand(BaseCommand):
|
||||
encoding=detector.result["encoding"]
|
||||
)
|
||||
except UnicodeDecodeError as exc:
|
||||
six.raise_from(
|
||||
CommandError(
|
||||
_("Unable to decode CSV file using %(encoding)s "
|
||||
"encoding") % detector.result
|
||||
),
|
||||
exc
|
||||
)
|
||||
raise CommandError(
|
||||
_("Unable to decode CSV file using %(encoding)s "
|
||||
"encoding") % detector.result
|
||||
) from exc
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Management command to create DKIM keys."""
|
||||
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import os
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Management command to check defined domains."""
|
||||
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import ipaddress
|
||||
|
||||
import gevent
|
||||
from gevent import socket
|
||||
|
||||
from django.conf import settings
|
||||
from django.core import mail
|
||||
from django.core.mail import EmailMessage
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils import timezone
|
||||
from django.utils.encoding import smart_text
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from modoboa.admin import constants, models
|
||||
from modoboa.dnstools import models as dns_models
|
||||
from modoboa.parameters import tools as param_tools
|
||||
|
||||
|
||||
@@ -74,7 +71,7 @@ class CheckMXRecords(BaseCommand):
|
||||
for ip, mxs in mx_list.items():
|
||||
try:
|
||||
ip = ipaddress.ip_address(smart_text(ip))
|
||||
except ValueError as e:
|
||||
except ValueError:
|
||||
continue
|
||||
else:
|
||||
delim = "." if ip.version == 4 else ":"
|
||||
@@ -89,8 +86,11 @@ class CheckMXRecords(BaseCommand):
|
||||
return provider, results
|
||||
|
||||
def store_dnsbl_result(self, domain, provider, results, **options):
|
||||
"""Store DNSBL provider results for domain."""
|
||||
alerts = {}
|
||||
"""Store DNSBL provider results for domain.
|
||||
|
||||
Return a list of alerts.
|
||||
"""
|
||||
alerts = []
|
||||
to_create = []
|
||||
for mx, result in list(results.items()):
|
||||
if not result:
|
||||
@@ -111,13 +111,29 @@ class CheckMXRecords(BaseCommand):
|
||||
dnsbl_result.save()
|
||||
if not dnsbl_result.status and result:
|
||||
trigger = True
|
||||
alarm_name = "domain_mx_in_dnsbl_{}".format(provider)
|
||||
if trigger:
|
||||
if domain not in alerts:
|
||||
alerts[domain] = []
|
||||
alerts[domain].append((provider, mx))
|
||||
title = _("MX {} listed by DNSBL provider {}").format(
|
||||
mx.name, provider)
|
||||
domain.alarms.create(
|
||||
internal_name=alarm_name,
|
||||
status=constants.ALARM_OPENED,
|
||||
title=title
|
||||
)
|
||||
alerts.append((provider, mx.name))
|
||||
else:
|
||||
domain.alarms.filter(
|
||||
internal_name=alarm_name,
|
||||
status=constants.ALARM_OPENED
|
||||
).update(
|
||||
status=constants.ALARM_CLOSED,
|
||||
closed=timezone.now()
|
||||
)
|
||||
models.DNSBLResult.objects.bulk_create(to_create)
|
||||
if not alerts:
|
||||
return
|
||||
return alerts
|
||||
|
||||
def send_alert_notifications(self, domain, alerts, subject, tpl, **options):
|
||||
"""Send email notifications about given alerts."""
|
||||
emails = list(options["email"])
|
||||
if not options["skip_admin_emails"]:
|
||||
emails.extend(
|
||||
@@ -126,19 +142,9 @@ class CheckMXRecords(BaseCommand):
|
||||
)
|
||||
if not len(emails):
|
||||
return
|
||||
with mail.get_connection() as connection:
|
||||
for domain, providers in list(alerts.items()):
|
||||
content = render_to_string(
|
||||
"admin/notifications/domain_in_dnsbl.html", {
|
||||
"domain": domain, "alerts": providers
|
||||
})
|
||||
subject = _("[modoboa] DNSBL issue(s) for domain {}").format(
|
||||
domain.name)
|
||||
msg = EmailMessage(
|
||||
subject, content.strip(), self.sender, emails,
|
||||
connection=connection
|
||||
)
|
||||
msg.send()
|
||||
content = render_to_string(tpl, {"domain": domain, "alerts": alerts})
|
||||
msg = EmailMessage(subject, content.strip(), self.sender, emails)
|
||||
msg.send()
|
||||
|
||||
def check_valid_mx(self, domain, mx_list, **options):
|
||||
"""Check that domain's MX record exist.
|
||||
@@ -152,38 +158,57 @@ class CheckMXRecords(BaseCommand):
|
||||
for mx in mx_list]
|
||||
valid_mxs = self.valid_mxs
|
||||
if not mxs:
|
||||
alerts.append(_("Domain {} has no MX record").format(domain))
|
||||
elif valid_mxs:
|
||||
for subnet in valid_mxs:
|
||||
for mx, addr in mxs:
|
||||
if addr in subnet:
|
||||
mx.managed = check = True
|
||||
mx.save()
|
||||
if check is False:
|
||||
mx_names = [
|
||||
"{0.name} ({0.address})".format(mx) for mx in mx_list]
|
||||
alarm, created = domain.alarms.get_or_create(
|
||||
internal_name="domain_has_no_mx",
|
||||
status=constants.ALARM_OPENED,
|
||||
defaults={"title": _("Domain has no MX record")}
|
||||
)
|
||||
if created:
|
||||
alerts.append(
|
||||
_("MX record for domain {0} is invalid: {1}").format(
|
||||
domain, ", ".join(mx_names))
|
||||
)
|
||||
_("Domain {} has no MX record").format(domain.name))
|
||||
else:
|
||||
# Close opened alarm
|
||||
domain.alarms.filter(
|
||||
internal_name="domain_has_no_mx",
|
||||
status=constants.ALARM_OPENED).update(
|
||||
status=constants.ALARM_CLOSED, closed=timezone.now())
|
||||
if valid_mxs:
|
||||
for subnet in valid_mxs:
|
||||
for mx, addr in mxs:
|
||||
if addr in subnet:
|
||||
mx.managed = check = True
|
||||
mx.save()
|
||||
if check is False:
|
||||
mx_names = [
|
||||
"{0.name} ({0.address})".format(mx) for mx in mx_list]
|
||||
alarm, created = domain.alarms.get_or_create(
|
||||
internal_name="domain_invalid_mx",
|
||||
status=constants.ALARM_OPENED,
|
||||
defaults={
|
||||
"title": _("Invalid MX record: {}").format(
|
||||
", ".join(mx_names))
|
||||
}
|
||||
)
|
||||
if created:
|
||||
alerts.append(
|
||||
_("MX record for domain {0} is invalid: {1}")
|
||||
.format(domain, ", ".join(mx_names))
|
||||
)
|
||||
else:
|
||||
# Close opened alarm
|
||||
domain.alarms.filter(
|
||||
internal_name="domain_invalid_mx",
|
||||
status=constants.ALARM_OPENED
|
||||
).update(
|
||||
status=constants.ALARM_CLOSED,
|
||||
closed=timezone.now()
|
||||
)
|
||||
if not alerts:
|
||||
return
|
||||
emails = list(options["email"])
|
||||
if not options["skip_admin_emails"]:
|
||||
emails.extend(
|
||||
domain.admins.exclude(mailbox__isnull=True)
|
||||
.values_list("email", flat=True)
|
||||
)
|
||||
if not len(emails):
|
||||
return
|
||||
content = render_to_string(
|
||||
"admin/notifications/domain_invalid_mx.html", {
|
||||
"domain": domain, "alerts": alerts
|
||||
})
|
||||
subject = _("[modoboa] MX issue(s) for domain {}").format(
|
||||
domain.name)
|
||||
msg = EmailMessage(subject, content.strip(), self.sender, emails)
|
||||
msg.send()
|
||||
tpl = "admin/notifications/domain_invalid_mx.html"
|
||||
self.send_alert_notifications(domain, alerts, subject, tpl, **options)
|
||||
|
||||
def check_domain(self, domain, timeout=3, ttl=7200, **options):
|
||||
"""Check specified domain."""
|
||||
@@ -193,6 +218,25 @@ class CheckMXRecords(BaseCommand):
|
||||
if param_tools.get_global_parameter("enable_mx_checks"):
|
||||
self.check_valid_mx(domain, mx_list, **options)
|
||||
|
||||
if param_tools.get_global_parameter("enable_spf_checks"):
|
||||
dns_models.DNSRecord.objects.get_or_create_for_domain(
|
||||
domain, "spf", ttl)
|
||||
condition = (
|
||||
param_tools.get_global_parameter("enable_dkim_checks") and
|
||||
domain.dkim_public_key
|
||||
)
|
||||
if condition:
|
||||
dns_models.DNSRecord.objects.get_or_create_for_domain(
|
||||
domain, "dkim", ttl)
|
||||
if param_tools.get_global_parameter("enable_dmarc_checks"):
|
||||
dns_models.DNSRecord.objects.get_or_create_for_domain(
|
||||
domain, "dmarc", ttl)
|
||||
if param_tools.get_global_parameter("enable_autoconfig_checks"):
|
||||
dns_models.DNSRecord.objects.get_or_create_for_domain(
|
||||
domain, "autoconfig", ttl)
|
||||
dns_models.DNSRecord.objects.get_or_create_for_domain(
|
||||
domain, "autodiscover", ttl)
|
||||
|
||||
condition = (
|
||||
not param_tools.get_global_parameter("enable_dnsbl_checks") or
|
||||
options["no_dnsbl"] is True)
|
||||
@@ -210,11 +254,19 @@ class CheckMXRecords(BaseCommand):
|
||||
gevent.spawn(self.query_dnsbl, mx_by_ip, provider)
|
||||
for provider in self.providers]
|
||||
gevent.joinall(jobs, timeout)
|
||||
alerts = []
|
||||
for job in jobs:
|
||||
if not job.successful():
|
||||
continue
|
||||
provider, results = job.value
|
||||
self.store_dnsbl_result(domain, provider, results, **options)
|
||||
alerts += self.store_dnsbl_result(
|
||||
domain, provider, results, **options)
|
||||
if not alerts:
|
||||
return
|
||||
subject = _("[modoboa] DNSBL issue(s) for domain {}").format(
|
||||
domain.name)
|
||||
tpl = "admin/notifications/domain_in_dnsbl.html"
|
||||
self.send_alert_notifications(domain, alerts, subject, tpl, **options)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""Command entry point."""
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Management command to check and fix known problems."""
|
||||
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils.encoding import smart_str
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.conf import settings
|
||||
|
||||
@@ -22,7 +19,6 @@ class Migration(migrations.Migration):
|
||||
],
|
||||
options={
|
||||
'ordering': ['address'],
|
||||
'permissions': (('view_aliases', 'View aliases'),),
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
@@ -50,7 +46,6 @@ class Migration(migrations.Migration):
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
'permissions': (('view_domain', 'View domain'), ('view_domains', 'View domains')),
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
@@ -61,9 +56,6 @@ class Migration(migrations.Migration):
|
||||
('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(
|
||||
@@ -74,9 +66,6 @@ class Migration(migrations.Migration):
|
||||
('quota', models.PositiveIntegerField()),
|
||||
('use_domain_quota', models.BooleanField(default=False)),
|
||||
],
|
||||
options={
|
||||
'permissions': (('view_mailboxes', 'View mailboxes'),),
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.9 on 2016-09-14 09:07
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.5 on 2016-10-26 08:03
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.5 on 2016-11-04 18:00
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.5 on 2016-11-04 18:15
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.12 on 2017-02-06 06:13
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.5 on 2017-02-15 08:48
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.5 on 2017-02-15 12:28
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.7 on 2017-12-20 14:17
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.8 on 2018-01-12 15:58
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.8 on 2018-01-24 23:11
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 1.11.16 on 2018-10-17 14:28
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('admin', '0013_auto_20180124_2311'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='domain',
|
||||
name='default_mailbox_quota',
|
||||
field=models.PositiveIntegerField(default=0, verbose_name='Default mailbox quota'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='domain',
|
||||
name='quota',
|
||||
field=models.PositiveIntegerField(default=0),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,44 @@
|
||||
# Generated by Django 2.1.15 on 2020-01-13 09:22
|
||||
|
||||
import sys
|
||||
|
||||
from django.db import IntegrityError, migrations, transaction
|
||||
|
||||
VIEW_PERMISSION_CODENAMES = {
|
||||
'view_aliases': 'view_alias',
|
||||
'view_domaliases': 'view_domainalias',
|
||||
'view_mailboxes': 'view_mailbox',
|
||||
}
|
||||
|
||||
|
||||
def rename_view_permissions(apps, schema_editor):
|
||||
Permission = apps.get_model('auth', 'Permission')
|
||||
db_alias = schema_editor.connection.alias
|
||||
# Iterate over permissions and try to rename the codename
|
||||
for perm in Permission.objects.using(db_alias).filter(
|
||||
codename__in=VIEW_PERMISSION_CODENAMES.keys()
|
||||
):
|
||||
codename = perm.codename
|
||||
try:
|
||||
with transaction.atomic():
|
||||
perm.codename = VIEW_PERMISSION_CODENAMES[perm.codename]
|
||||
perm.save()
|
||||
except IntegrityError:
|
||||
print(
|
||||
(
|
||||
"Unable to rename '{}' to '{}', a permission with this "
|
||||
"codename already exists.".format(codename, perm.codename)
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('admin', '0014_auto_20181017_1628'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(rename_view_permissions),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 2.2.12 on 2020-06-02 10:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('admin', '0015_rename_view_permissions'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='domain',
|
||||
name='message_limit',
|
||||
field=models.PositiveIntegerField(blank=True, help_text='Number of messages this domain can send per day', null=True, verbose_name='Message sending limit'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='mailbox',
|
||||
name='message_limit',
|
||||
field=models.PositiveIntegerField(blank=True, help_text='Number of messages this mailbox can send per day', null=True, verbose_name='Message sending limit'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 2.2.12 on 2020-09-11 07:46
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('admin', '0016_auto_20200602_1201'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Alarm',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('closed', models.DateTimeField(blank=True, null=True)),
|
||||
('status', models.IntegerField(choices=[(1, 'Opened'), (2, 'Closed')], db_index=True, default=1)),
|
||||
('title', models.CharField(max_length=150)),
|
||||
('internal_name', models.CharField(max_length=120)),
|
||||
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='alarms', to='admin.Domain')),
|
||||
('mailbox', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='alarms', to='admin.Mailbox')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['created'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Admin models."""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .alarm import Alarm
|
||||
from .alias import Alias, AliasRecipient
|
||||
from .base import AdminObject
|
||||
from .domain import Domain
|
||||
@@ -12,6 +9,7 @@ from .mailbox import Mailbox, MailboxOperation, Quota, SenderAddress
|
||||
from .mxrecord import DNSBLResult, MXRecord
|
||||
|
||||
__all__ = [
|
||||
"Alarm",
|
||||
"AdminObject",
|
||||
"Alias",
|
||||
"AliasRecipient",
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Alert related models."""
|
||||
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
from reversion import revisions as reversion
|
||||
|
||||
from .. import constants
|
||||
|
||||
|
||||
class AlarmQuerySet(models.QuerySet):
|
||||
"""Custom queryset for Alarm."""
|
||||
|
||||
def opened(self):
|
||||
"""Return opened alarms."""
|
||||
return self.filter(status=constants.ALARM_OPENED)
|
||||
|
||||
|
||||
class Alarm(models.Model):
|
||||
"""A simple alarm to attach to a domain and/or mailbox."""
|
||||
|
||||
domain = models.ForeignKey(
|
||||
"admin.Domain", on_delete=models.CASCADE, related_name="alarms")
|
||||
mailbox = models.ForeignKey(
|
||||
"admin.Mailbox", on_delete=models.SET_NULL,
|
||||
related_name="alarms",
|
||||
null=True, blank=True,
|
||||
)
|
||||
created = models.DateTimeField(default=timezone.now)
|
||||
closed = models.DateTimeField(null=True, blank=True)
|
||||
status = models.IntegerField(
|
||||
default=constants.ALARM_OPENED, choices=constants.ALARM_STATUSES,
|
||||
db_index=True
|
||||
)
|
||||
|
||||
title = models.CharField(max_length=150)
|
||||
internal_name = models.CharField(max_length=120)
|
||||
|
||||
objects = models.Manager.from_queryset(AlarmQuerySet)()
|
||||
|
||||
class Meta:
|
||||
ordering = ["created"]
|
||||
|
||||
def __str__(self):
|
||||
return "[{}] {} - {}".format(
|
||||
self.created, self.domain, self.get_status_display())
|
||||
|
||||
def close(self):
|
||||
"""Close this alarm."""
|
||||
self.status = constants.ALARM_CLOSED
|
||||
self.closed = timezone.now()
|
||||
self.save()
|
||||
|
||||
|
||||
reversion.register(Alarm)
|
||||
@@ -1,9 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Models related to aliases management."""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import hashlib
|
||||
import random
|
||||
|
||||
@@ -52,9 +48,6 @@ class Alias(AdminObject):
|
||||
_objectname = "MailboxAlias"
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
("view_aliases", "View aliases"),
|
||||
)
|
||||
ordering = ["address"]
|
||||
unique_together = (("address", "internal"), )
|
||||
app_label = "admin"
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Base admin models."""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
@@ -47,13 +43,13 @@ class AdminObject(models.Model):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Custom constructor."""
|
||||
super(AdminObject, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self._loaded_values = {}
|
||||
|
||||
@classmethod
|
||||
def from_db(cls, db, field_names, values):
|
||||
"""Store loaded values."""
|
||||
instance = super(AdminObject, cls).from_db(db, field_names, values)
|
||||
instance = super().from_db(db, field_names, values)
|
||||
instance._loaded_values = dict(zip(field_names, values))
|
||||
return instance
|
||||
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Models related to domains management."""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
|
||||
from reversion import revisions as reversion
|
||||
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.encoding import (
|
||||
force_text, python_2_unicode_compatible, smart_text
|
||||
)
|
||||
from django.utils.encoding import force_text, smart_text
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
|
||||
@@ -20,29 +14,29 @@ from modoboa.core import signals as core_signals
|
||||
from modoboa.core.models import User
|
||||
from modoboa.lib.exceptions import BadRequest, Conflict
|
||||
from modoboa.parameters import tools as param_tools
|
||||
|
||||
from .. import constants
|
||||
from .base import AdminObject
|
||||
from . import mixins
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Domain(AdminObject):
|
||||
class Domain(mixins.MessageLimitMixin, AdminObject):
|
||||
"""Mail domain."""
|
||||
|
||||
name = models.CharField(ugettext_lazy("name"), max_length=100, unique=True,
|
||||
help_text=ugettext_lazy("The domain name"))
|
||||
quota = models.PositiveIntegerField(
|
||||
default=0,
|
||||
help_text=ugettext_lazy(
|
||||
"Quota in MB shared between mailboxes. A value of 0 means "
|
||||
"no quota."
|
||||
)
|
||||
)
|
||||
default_mailbox_quota = models.PositiveIntegerField(
|
||||
verbose_name=ugettext_lazy("Default mailbox quota"),
|
||||
default=0,
|
||||
default=0
|
||||
)
|
||||
message_limit = models.PositiveIntegerField(
|
||||
ugettext_lazy("Message sending limit"),
|
||||
null=True, blank=True,
|
||||
help_text=ugettext_lazy(
|
||||
"Default quota in MB applied to mailboxes. A value of 0 means "
|
||||
"no quota."
|
||||
"Number of messages this domain can send per day"
|
||||
)
|
||||
)
|
||||
enabled = models.BooleanField(
|
||||
@@ -75,10 +69,6 @@ class Domain(AdminObject):
|
||||
dkim_private_key_path = models.CharField(max_length=254, blank=True)
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
("view_domain", "View domain"),
|
||||
("view_domains", "View domains"),
|
||||
)
|
||||
ordering = ["name"]
|
||||
app_label = "admin"
|
||||
|
||||
@@ -87,6 +77,7 @@ class Domain(AdminObject):
|
||||
super(Domain, self).__init__(*args, **kwargs)
|
||||
self.old_mail_homes = None
|
||||
self.oldname = self.name
|
||||
self.old_dkim_key_length = self.dkim_key_length
|
||||
|
||||
@property
|
||||
def domainalias_count(self):
|
||||
@@ -189,6 +180,31 @@ class Domain(AdminObject):
|
||||
else:
|
||||
return "success"
|
||||
|
||||
@cached_property
|
||||
def spf_record(self):
|
||||
"""Return SPF record."""
|
||||
return self.dnsrecord_set.filter(type="spf").first()
|
||||
|
||||
@cached_property
|
||||
def dkim_record(self):
|
||||
"""Return DKIM record."""
|
||||
return self.dnsrecord_set.filter(type="dkim").first()
|
||||
|
||||
@cached_property
|
||||
def dmarc_record(self):
|
||||
"""Return DMARC record."""
|
||||
return self.dnsrecord_set.filter(type="dmarc").first()
|
||||
|
||||
@cached_property
|
||||
def autoconfig_record(self):
|
||||
"""Return autoconfig record."""
|
||||
return self.dnsrecord_set.filter(type="autoconfig").first()
|
||||
|
||||
@cached_property
|
||||
def autodiscover_record(self):
|
||||
"""Return autodiscover record."""
|
||||
return self.dnsrecord_set.filter(type="autodiscover").first()
|
||||
|
||||
@cached_property
|
||||
def allocated_quota(self):
|
||||
"""Return current quota allocation."""
|
||||
@@ -224,6 +240,11 @@ class Domain(AdminObject):
|
||||
return 0
|
||||
return int(self.used_quota / float(self.quota) * 100)
|
||||
|
||||
@property
|
||||
def message_counter_key(self):
|
||||
"""Return the key used to store messages count."""
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def bind_format_dkim_public_key(self):
|
||||
"""Return DKIM public key using Bind format."""
|
||||
@@ -240,7 +261,7 @@ class Domain(AdminObject):
|
||||
split_record.append(" \"%s\"" % record)
|
||||
break
|
||||
record = "\n".join(split_record)
|
||||
return "{}._domainkey.{}. 10800 IN TXT (\n{})".format(
|
||||
return "{}._domainkey.{}. IN TXT (\n{})".format(
|
||||
self.dkim_key_selector, self.name, record)
|
||||
|
||||
def add_admin(self, account):
|
||||
@@ -283,6 +304,9 @@ class Domain(AdminObject):
|
||||
self.old_mail_homes = {
|
||||
mb.id: mb.mail_home for mb in self.mailbox_set.all()
|
||||
}
|
||||
if self.old_dkim_key_length != self.dkim_key_length:
|
||||
self.dkim_public_key = ""
|
||||
self.dkim_private_key_path = ""
|
||||
super(Domain, self).save(*args, **kwargs)
|
||||
|
||||
def delete(self, fromuser, keepdir=False):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user