Merge branch 'master' into feature/codacy

This commit is contained in:
Andrew Fyfe
2018-04-12 21:19:30 +01:00
221 changed files with 2083 additions and 1631 deletions
+30
View File
@@ -0,0 +1,30 @@
# EditorConfig is awesome: http://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{css,js,py}]
charset = utf-8
indent_style = space
indent_size = 4
[Makefile]
indent_style = tab
[{.travis.yml,codecov.yml}]
indent_style = space
indent_size = 2
# Ignore paths
[/modoboa/bower_components/**]
charset = none
end_of_line = none
insert_final_newline = none
trim_trailing_whitespace = none
indent_style = none
indent_size = none
+1
View File
@@ -11,6 +11,7 @@ DEV
.ropeproject
modoboa.iml
.tox
.eggs/
modoboa_test
.coverage
build/
+2
View File
@@ -37,6 +37,8 @@ install:
- pip install -r requirements.txt
- pip install -r ldap-requirements.txt
- pip install -r test-requirements.txt
- if [ $DB = POSTGRESQL ]; then pip install -r postgresql-requirements.txt ; fi
- if [ $DB = MYSQL ]; then pip install -r mysql-requirements.txt ; fi
- python setup.py develop
before_script:
+1 -1
View File
@@ -30,11 +30,11 @@ Main features
* Reputation protection: `DNSBL <https://en.wikipedia.org/wiki/DNSBL>`_ checks, `DMARC <https://dmarc.org/>`_ `reports <https://github.com/modoboa/modoboa-dmarc>`_ and more
* `Amavis <http://www.amavis.org>`_ `frontend <https://github.com/modoboa/modoboa-amavis>`_
* `Webmail <https://github.com/modoboa/modoboa-webmail>`_
* `Calendar <https://github.com/modoboa/modoboa-radicale>`_
* `Address book <https://github.com/modoboa/modoboa-contacts>`_
* `Per-user Sieve filters <https://github.com/modoboa/modoboa-sievefilters>`_
* `Autoreply messages for Postfix <https://github.com/modoboa/modoboa-postfix-autoreply>`_
* `Graphical statistics about email traffic <https://github.com/modoboa/modoboa-stats>`_
* `Radicale <http://radicale.org/>`_ `management frontend <https://github.com/modoboa/modoboa-radicale>`_
*************
Documentation
-1
View File
@@ -7,4 +7,3 @@ from modoboa.core.commands import handle_command_line
if __name__ == "__main__":
handle_command_line()
+63 -59
View File
@@ -11,82 +11,86 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
from __future__ import unicode_literals
import os
# import sys
# 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
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# sys.path.insert(0, os.path.abspath('.'))
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
'sphinx.ext.intersphinx',
"sphinx.ext.intersphinx",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['.templates']
templates_path = [".templates"]
# The suffix of source filenames.
source_suffix = '.rst'
source_suffix = ".rst"
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
master_doc = "index"
# General information about the project.
project = u'Modoboa'
copyright = u'2018, Antoine Nguyen'
project = "Modoboa"
copyright = "2018, Antoine Nguyen" # NOQA:A001
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '1.10'
version = "1.10"
# The full version, including alpha/beta/rc tags.
release = '1.10.0'
release = "1.10.0"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['.build']
exclude_patterns = [".build"]
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
pygments_style = "sphinx"
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
@@ -94,123 +98,123 @@ pygments_style = 'sphinx'
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
on_rtd = os.environ.get("READTHEDOCS", None) == "True"
if on_rtd:
html_theme = 'default'
html_theme = "default"
else:
html_theme = 'nature'
html_theme = "nature"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_static_path = ["_static"]
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'Modoboadoc'
htmlhelp_basename = "Modoboadoc"
# -- Options for LaTeX output --------------------------------------------------
# The paper size ('letter' or 'a4').
#latex_paper_size = 'letter'
# latex_paper_size = 'letter'
# The font size ('10pt', '11pt' or '12pt').
#latex_font_size = '10pt'
# latex_font_size = '10pt'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'Modoboa.tex', u'Modoboa Documentation',
u'Antoine Nguyen', 'manual'),
("index", "Modoboa.tex", "Modoboa Documentation",
"Antoine Nguyen", "manual"),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# latex_show_urls = False
# Additional stuff for the LaTeX preamble.
#latex_preamble = ''
# latex_preamble = ''
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
@@ -218,10 +222,10 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'modoboa', u'Modoboa Documentation',
[u'Antoine Nguyen'], 1)
("index", "modoboa", "Modoboa Documentation",
["Antoine Nguyen"], 1)
]
intersphinx_mapping = {
'amavis': ('http://modoboa-amavis.readthedocs.org/en/latest/', None)
"amavis": ("http://modoboa-amavis.readthedocs.org/en/latest/", None)
}
+19
View File
@@ -254,6 +254,25 @@ This parameter is optional but you must ensure it is set to ``False``
The default configuration file provided by the ``modoboa-admin.py``
command is properly configured.
**********************
Logging authentication
**********************
To trace login attempts to the web interface, Modoboa uses python
`SysLogHandler <https://docs.python.org/3/library/logging.handlers.html#logging.handlers.SysLogHandler>`_
so you can see them in your syslog authentication log file
(`/var/log/auth.log` in most cases).
Depending on your configuration, you may have to edit the :file:`settings.py` file
and add `'address': '/dev/log'` to the logging section::
'syslog-auth': {
'class': 'logging.handlers.SysLogHandler',
'facility': SysLogHandler.LOG_AUTH,
'address': '/dev/log',
'formatter': 'syslog'
},
***********************
External authentication
***********************
+19 -9
View File
@@ -32,15 +32,25 @@ 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 |CentOS |
+==============================+==============================+
|build-essential, python-dev, |gcc, gcc-c++, python-devel, |
|libxml2-dev, libxslt-dev, |libxml2-devel, libxslt-devel, |
|libjpeg-dev, librrd-dev, |libjpeg-turbo-devel, |
|rrdtool, libffi-dev |rrdtool-devel, rrdtool, |
| |libffi-devel |
+------------------------------+------------------------------+
+------------------------------+
|Debian / Ubuntu |
+==============================+
|build-essential python-dev |
|libxml2-dev libxslt-dev |
|libjpeg-dev librrd-dev |
|rrdtool libffi-dev |
| |
+------------------------------+
+------------------------------+
|CentOS |
+==============================+
|gcc gcc-c++ python-devel |
|libxml2-devel libxslt-devel |
|libjpeg-turbo-devel |
|rrdtool-devel rrdtool |
|libffi-devel |
+------------------------------+
Then, install Modoboa:
+2 -9
View File
@@ -168,15 +168,8 @@ uwsgi:
.. warning::
Some distributions, including Debian and Ubuntu, ship an outdated
version of uWSGI that does not conform to the WSGI
specification. Versions prior to 1.2.6 do not call close on the
response object after handling a request. In those cases the
request_finished signal isn’t sent. This can result in idle
connections to database and memcache servers.
Use uwsgi 1.2.6 or newer. If you do not, you *will* run into
problems. Modoboa will fail in obscure ways.
Use uwsgi 1.2.6 or newer. If you do not, you *will* run into
problems. Modoboa will fail in obscure ways.
To use this setup, first `download and install uwsgi
<http://uwsgi-docs.readthedocs.org/en/latest/WSGIquickstart.html>`_.
+6
View File
@@ -185,6 +185,12 @@ Replace ``<driver>`` and ``<path>`` by your values.
If ``transport_maps`` contains ``sql-relaydomains-transport.cf``, remove it.
.. warning::
If you make use of postfix's `proxymap server
<http://www.postfix.org/proxymap.8.html>`_, you must also update
the ``proxy_read_maps`` setting.
Reload postfix.
Add the following cron job in order to generate DKIM keys::
+1 -2
View File
@@ -4,8 +4,7 @@
from __future__ import unicode_literals
from pkg_resources import get_distribution, DistributionNotFound
from pkg_resources import DistributionNotFound, get_distribution
try:
__version__ = get_distribution(__name__).version
+2
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
default_app_config = "modoboa.admin.apps.AdminConfig"
+6 -7
View File
@@ -1,21 +1,20 @@
# -*- coding: utf-8 -*-
"""Admin API."""
from __future__ import unicode_literals
from django import http
from django.contrib.contenttypes.models import ContentType
from rest_framework import filters, status, viewsets
from rest_framework.decorators import detail_route, list_route
from rest_framework.exceptions import ParseError
from rest_framework.permissions import IsAuthenticated, DjangoModelPermissions
from rest_framework.permissions import DjangoModelPermissions, IsAuthenticated
from rest_framework.response import Response
from modoboa.core import models as core_models
from . import models
from . import serializers
from . import models, serializers
class DomainViewSet(viewsets.ModelViewSet):
@@ -81,7 +80,7 @@ class AccountViewSet(viewsets.ModelViewSet):
user = self.request.user
ids = user.objectaccess_set \
.filter(content_type=ContentType.objects.get_for_model(user)) \
.values_list('object_id', flat=True)
.values_list("object_id", flat=True)
queryset = core_models.User.objects.filter(pk__in=ids)
domain = self.request.query_params.get("domain")
if domain:
@@ -139,7 +138,7 @@ class AliasViewSet(viewsets.ModelViewSet):
ids = (
user.objectaccess_set.filter(
content_type=ContentType.objects.get_for_model(models.Alias))
.values_list('object_id', flat=True)
.values_list("object_id", flat=True)
)
queryset = models.Alias.objects.filter(pk__in=ids)
domain = self.request.query_params.get("domain")
+4 -3
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
"""Modoboa admin settings."""
from __future__ import unicode_literals
@@ -6,13 +8,12 @@ import os
from django import forms
from django.conf import settings
from django.utils.translation import ugettext as _, ugettext_lazy
from django.utils.encoding import force_text
from django.utils.translation import ugettext as _, ugettext_lazy
from modoboa.lib.form_utils import YesNoField, SeparatorField
from modoboa.lib.form_utils import SeparatorField, YesNoField
from modoboa.lib.sysutils import exec_cmd
from modoboa.parameters import forms as param_forms
from . import constants
+3 -1
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
"""AppConfig for admin."""
from __future__ import unicode_literals
@@ -17,4 +19,4 @@ class AdminConfig(AppConfig):
def ready(self):
load_admin_settings()
from . import handlers
from . import handlers # NOQA:F401
+2 -1
View File
@@ -1,10 +1,11 @@
# -*- 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",
+19 -17
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
"""
Fixtures factories.
"""
@@ -18,7 +20,7 @@ class DomainFactory(PermissionFactory):
model = models.Domain
django_get_or_create = ("name", )
type = "domain"
type = "domain" # NOQA:A003
quota = 0
default_mailbox_quota = 10
enabled = True
@@ -79,44 +81,44 @@ def populate_database():
"""
dom = DomainFactory(name="test.com", quota=50)
admin = UserFactory(
username="admin@test.com", groups=('DomainAdmins', ),
password='{PLAIN}toto'
username="admin@test.com", groups=("DomainAdmins", ),
password="{PLAIN}toto"
)
MailboxFactory(address='admin', domain=dom, user=admin)
MailboxFactory(address="admin", domain=dom, user=admin)
account = UserFactory.create(
username="user@test.com", groups=('SimpleUsers',),
username="user@test.com", groups=("SimpleUsers",),
)
MailboxFactory.create(address='user', domain=dom, user=account)
MailboxFactory.create(address="user", domain=dom, user=account)
al = AliasFactory.create(
address='forward@test.com', domain=dom
address="forward@test.com", domain=dom
)
AliasRecipientFactory.create(
address='user@external.com', alias=al)
address="user@external.com", alias=al)
al = AliasFactory.create(
address='alias@test.com', domain=dom
address="alias@test.com", domain=dom
)
mb = account.mailbox
AliasRecipientFactory.create(
address=mb.full_address, alias=al, r_mailbox=mb)
al = AliasFactory.create(
address='postmaster@test.com', domain=dom
address="postmaster@test.com", domain=dom
)
for address in ['toto@titi.com', 'test@truc.fr']:
for address in ["toto@titi.com", "test@truc.fr"]:
AliasRecipientFactory.create(address=address, alias=al)
dom.add_admin(admin)
dom2 = DomainFactory.create(name='test2.com', default_mailbox_quota=0)
dom2 = DomainFactory.create(name="test2.com", default_mailbox_quota=0)
admin = UserFactory.create(
username='admin@test2.com', groups=('DomainAdmins',),
password='{PLAIN}toto'
username="admin@test2.com", groups=("DomainAdmins",),
password="{PLAIN}toto"
)
MailboxFactory.create(address='admin', domain=dom2, user=admin)
MailboxFactory.create(address="admin", domain=dom2, user=admin)
u = UserFactory.create(
username='user@test2.com', groups=('SimpleUsers',)
username="user@test2.com", groups=("SimpleUsers",)
)
MailboxFactory.create(address='user', domain=dom2, user=u)
MailboxFactory.create(address="user", domain=dom2, user=u)
dom2.add_admin(admin)
+13 -11
View File
@@ -1,21 +1,23 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from .domain import (
DomainFormGeneral, DomainFormOptions, DomainForm, DomainWizard
)
from .account import (
AccountFormGeneral, AccountFormMail, AccountPermissionsForm,
AccountForm, AccountWizard
AccountForm, AccountFormGeneral, AccountFormMail, AccountPermissionsForm,
AccountWizard
)
from .alias import AliasForm
from .domain import (
DomainForm, DomainFormGeneral, DomainFormOptions, DomainWizard
)
from .export import ExportDataForm, ExportDomainsForm, ExportIdentitiesForm
from .forward import ForwardForm
from .import_ import ImportDataForm, ImportIdentitiesForm
from .export import ExportDataForm, ExportDomainsForm, ExportIdentitiesForm
__all__ = [
'DomainFormGeneral', 'DomainFormOptions', 'DomainForm',
'AccountFormGeneral', 'AccountFormMail', 'AccountPermissionsForm',
'AccountForm', 'AliasForm', 'ImportDataForm', 'ImportIdentitiesForm',
'ExportDataForm', 'ExportDomainsForm', 'ExportIdentitiesForm',
'ForwardForm', 'DomainWizard', 'AccountWizard'
"DomainFormGeneral", "DomainFormOptions", "DomainForm",
"AccountFormGeneral", "AccountFormMail", "AccountPermissionsForm",
"AccountForm", "AliasForm", "ImportDataForm", "ImportIdentitiesForm",
"ExportDataForm", "ExportDomainsForm", "ExportIdentitiesForm",
"ForwardForm", "DomainWizard", "AccountWizard"
]
+11 -14
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
"""Forms related to accounts management."""
from __future__ import unicode_literals
@@ -7,17 +9,15 @@ from functools import reduce
from django import forms
from django.conf import settings
from django.contrib.auth import password_validation
from django.http import QueryDict
from django.urls import reverse
from django.utils.translation import ugettext as _, ugettext_lazy
from django.contrib.auth import password_validation
from modoboa.core import signals as core_signals
from modoboa.core.models import User
from modoboa.lib import exceptions as lib_exceptions, fields as lib_fields
from modoboa.lib.email_utils import split_mailbox
from modoboa.lib import exceptions as lib_exceptions
from modoboa.lib import fields as lib_fields
from modoboa.lib.form_utils import (
DynamicForm, TabForms, WizardForm, WizardStep
)
@@ -25,10 +25,7 @@ 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.parameters import tools as param_tools
from .. import lib
from .. import models
from .. import signals
from .. import lib, models, signals
class AccountFormGeneral(forms.ModelForm):
@@ -527,9 +524,9 @@ class AccountForm(TabForms):
def extra_context(self, context):
account = self.instances["general"]
context.update({
'title': account.username,
'formid': 'accountform',
'action': reverse("admin:account_change",
"title": account.username,
"formid": "accountform",
"action": reverse("admin:account_change",
args=[account.id]),
})
@@ -604,9 +601,9 @@ class AccountWizard(WizardForm):
def extra_context(self, context):
context.update({
'title': _("New account"),
'action': reverse("admin:account_add"),
'formid': 'newaccount_form'
"title": _("New account"),
"action": reverse("admin:account_add"),
"formid": "newaccount_form"
})
def done(self):
+5 -7
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
"""Forms related to aliases management."""
from __future__ import unicode_literals
@@ -9,14 +11,10 @@ from django.http import QueryDict
from django.utils.translation import ugettext as _, ugettext_lazy
from modoboa.core import signals as core_signals
from modoboa.lib import exceptions as lib_exceptions
from modoboa.lib import exceptions as lib_exceptions, fields as lib_fields
from modoboa.lib.email_utils import split_mailbox
from modoboa.lib import fields as lib_fields
from modoboa.lib.form_utils import (
DynamicForm
)
from ..models import Domain, Alias
from modoboa.lib.form_utils import DynamicForm
from ..models import Alias, Domain
class AliasForm(forms.ModelForm, DynamicForm):
+9 -13
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
"""Forms related to domains management."""
from __future__ import unicode_literals
@@ -5,34 +7,28 @@ from __future__ import unicode_literals
from functools import reduce
from django import forms
from django.urls import reverse
from django.http import QueryDict
from django.utils.translation import ugettext as _, ugettext_lazy
from django.urls import reverse
from django.utils.encoding import force_text
from django.utils.translation import ugettext as _, ugettext_lazy
from modoboa.core import signals as core_signals
from modoboa.core.models import User
from modoboa.lib.exceptions import Conflict
from modoboa.lib.fields import DomainNameField
from modoboa.lib.form_utils import (
YesNoField, WizardForm, WizardStep, DynamicForm, TabForms
DynamicForm, TabForms, WizardForm, WizardStep, YesNoField
)
from modoboa.lib.web_utils import render_to_json_response
from modoboa.parameters import tools as param_tools
from .. import constants
from .. import lib
from .. import signals
from ..models import (
Domain, DomainAlias, Mailbox, Alias
)
from .. import constants, lib, signals
from ..models import Alias, Domain, DomainAlias, Mailbox
class DomainFormGeneral(forms.ModelForm, DynamicForm):
"""A form to create/edit a domain."""
type = forms.ChoiceField(
type = forms.ChoiceField( # NOQA:A003
label=ugettext_lazy("Type"),
)
aliases = DomainNameField(
@@ -82,7 +78,7 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm):
"""Check unicity and more."""
name = self.cleaned_data["name"]
label = lib.check_if_domain_exists(
name, [(DomainAlias, _('domain alias'))])
name, [(DomainAlias, _("domain alias"))])
if label is not None:
raise forms.ValidationError(
_("A %s with this name already exists") % force_text(label))
+2
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django import forms
+2
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
"""Forms related to forwards management."""
from __future__ import unicode_literals
+2
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
"""Forms related to import operations."""
from __future__ import unicode_literals
+8 -14
View File
@@ -1,27 +1,21 @@
# -*- coding: utf-8 -*-
"""Django signal handlers for admin."""
from __future__ import unicode_literals
from django.urls import reverse
from django.contrib.contenttypes.models import ContentType
from django.db.models import signals
from django.dispatch import receiver
from django.urls import reverse
from django.utils.translation import ugettext as _
from django.contrib.contenttypes.models import ContentType
from modoboa.core import models as core_models
from modoboa.core import signals as core_signals
from modoboa.core import models as core_models, signals as core_signals
from modoboa.lib import exceptions, permissions, signals as lib_signals
from modoboa.lib.cryptutils import encrypt
from modoboa.lib.email_utils import split_mailbox
from modoboa.lib import exceptions
from modoboa.lib import permissions
from modoboa.lib import signals as lib_signals
from modoboa.parameters import tools as param_tools
from . import signals as admin_signals
from . import lib
from . import models
from . import postfix_maps
from . import lib, models, postfix_maps, signals as admin_signals
@receiver(signals.post_save, sender=models.Domain)
@@ -161,7 +155,7 @@ def account_auto_created(sender, user, **kwargs):
if not param_tools.get_global_parameter("auto_create_domain_and_mailbox"):
return
localpart, domname = split_mailbox(user.username)
if user.role != 'SimpleUsers' and domname is None:
if user.role != "SimpleUsers" and domname is None:
return
sadmins = core_models.User.objects.filter(is_superuser=True)
try:
+10 -12
View File
@@ -4,30 +4,28 @@
from __future__ import unicode_literals
from functools import wraps
import ipaddress
from itertools import chain
import logging
import random
import socket
import string
from functools import wraps
from itertools import chain
from dns.name import IDNA_2008_UTS_46
import dns.resolver
from dns.name import IDNA_2008_UTS_46
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.utils.encoding import smart_text
from django.utils.translation import ugettext as _
from modoboa.core.models import User
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 .models import Domain, DomainAlias, Alias
from . import signals
from .models import Alias, Domain, DomainAlias
def needs_mailbox():
@@ -59,7 +57,7 @@ def get_identities(user, searchquery=None, idtfilter=None, grpfilter=None):
if idtfilter is None or not idtfilter or idtfilter == "account":
ids = user.objectaccess_set \
.filter(content_type=ContentType.objects.get_for_model(user)) \
.values_list('object_id', flat=True)
.values_list("object_id", flat=True)
q = Q(pk__in=ids)
if searchquery is not None:
q &= Q(username__icontains=searchquery) \
@@ -76,7 +74,7 @@ def get_identities(user, searchquery=None, idtfilter=None, grpfilter=None):
or (idtfilter in ["alias", "forward", "dlist"]):
alct = ContentType.objects.get_for_model(Alias)
ids = user.objectaccess_set.filter(content_type=alct) \
.values_list('object_id', flat=True)
.values_list("object_id", flat=True)
q = Q(pk__in=ids, internal=False)
if searchquery is not None:
q &= (
@@ -187,8 +185,8 @@ def get_domain_mx_list(domain):
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)
_("DNS resolution timeout, unable to query %s at the moment") %
domain, exc_info=e)
else:
for dns_answer in dns_answers:
try:
@@ -225,7 +223,7 @@ def domain_has_authorized_mx(name):
valid_mxs = [ipaddress.ip_network(smart_text(v.strip()))
for v in valid_mxs.split() if v.strip()]
domain_mxs = get_domain_mx_list(name)
for mx_addr, mx_ip_addr in domain_mxs: # noqa:B007
for _mx_addr, mx_ip_addr in domain_mxs:
for subnet in valid_mxs:
if mx_ip_addr in subnet:
return True
-1
View File
@@ -1 +0,0 @@
from __future__ import unicode_literals
@@ -1 +0,0 @@
from __future__ import unicode_literals
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
"""A management command to apply mailbox operations."""
from __future__ import unicode_literals, print_function
from __future__ import print_function, unicode_literals
import logging
import os
@@ -8,10 +10,9 @@ import shutil
from django.core.management.base import BaseCommand
from modoboa.parameters import tools as param_tools
from modoboa.lib.sysutils import exec_cmd
from modoboa.lib.exceptions import InternalError
from modoboa.lib.sysutils import exec_cmd
from modoboa.parameters import tools as param_tools
from ...app_settings import load_admin_settings
from ...models import MailboxOperation
@@ -25,7 +26,7 @@ class OperationError(Exception):
class Command(BaseCommand):
"""Command definition"""
help = "Handles rename and delete operations on mailboxes"
help = "Handles rename and delete operations on mailboxes" # NOQA:A003
def __init__(self, *args, **kwargs):
super(Command, self).__init__(*args, **kwargs)
@@ -80,7 +81,7 @@ class Command(BaseCommand):
)
if not code:
return False
with open(path, 'w') as fp:
with open(path, "w") as fp:
print(os.getpid(), file=fp)
return True
+4 -2
View File
@@ -1,13 +1,15 @@
# -*- coding: utf-8 -*-
"""Modoboa main management command."""
from __future__ import unicode_literals
from subcommand.base import SubcommandCommand
from .subcommands._mx import CheckMXRecords
from .subcommands._export import ExportCommand
from .subcommands._import import ImportCommand
from .subcommands._manage_dkim_keys import ManageDKIMKeys
from .subcommands._mx import CheckMXRecords
from .subcommands._repair import Repair
@@ -17,7 +19,7 @@ class Command(SubcommandCommand):
$ python manage.py modo
"""
help = "Modoboa top management command."
help = "Modoboa top management command." # NOQA:A003
subcommands = {
"export": ExportCommand,
@@ -1 +0,0 @@
from __future__ import unicode_literals
@@ -1,21 +1,28 @@
# -*- coding: utf-8 -*-
"""Django management command to export admin objects."""
from __future__ import unicode_literals
from backports import csv
import sys
from django.core.management.base import BaseCommand
from django.utils import six
from modoboa.core.models import User
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."""
help = "Export domains or identities using CSV format" # noqa:A003
help = "Export domains or identities using CSV format" # NOQA:A003
def add_arguments(self, parser):
"""Add arguments to command."""
@@ -46,12 +53,12 @@ class ExportCommand(BaseCommand):
dumped_aliases += [alias.pk]
qset = (
models.Alias.objects.exclude(pk__in=dumped_aliases)
.prefetch_related('aliasrecipient_set')
.prefetch_related("aliasrecipient_set")
)
for alias in qset:
alias.to_csv(self.csvwriter)
def handle(self, *args, **options):
exts_pool.load_all()
self.csvwriter = csv.writer(sys.stdout, delimiter=options['sepchar'])
self.csvwriter = csv.writer(sys.stdout, delimiter=options["sepchar"])
getattr(self, "export_{}".format(options["objtype"]))()
@@ -1,58 +1,69 @@
# -*- coding: utf-8 -*-
"""Django management command to import admin objects."""
from __future__ import unicode_literals
from backports import csv
import io
import os
import progressbar
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
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."""
args = "csvfile"
help = "Import identities from a csv file" # noqa:A003
help = "Import identities from a csv file" # NOQA:A003
def add_arguments(self, parser):
"""Add extra arguments to command."""
parser.add_argument(
"--sepchar", type=str, default=";",
"--sepchar", type=six.text_type, default=";",
help="Separator used in file.")
parser.add_argument(
"--continue-if-exists", action="store_true",
dest='continue_if_exists', default=False,
dest="continue_if_exists", default=False,
help="Continue even if an entry already exists.")
parser.add_argument(
"--crypt-password", action="store_true",
default=False, help="Encrypt provided passwords.")
parser.add_argument(
"files", type=str, nargs="+", help="CSV files to import.")
"files", type=six.text_type, nargs="+", help="CSV files to import.")
def _import(self, filename, options):
def _import(self, filename, options, encoding="utf-8"):
"""Import domains or identities."""
superadmin = (
core_models.User.objects.filter(is_superuser=True).first()
)
if not os.path.exists(filename):
if not os.path.isfile(filename):
raise CommandError("File not found")
num_lines = sum(1 for line in open(filename) if line)
num_lines = sum(
1 for line in io.open(filename, encoding=encoding) if line
)
pbar = progressbar.ProgressBar(
widgets=[
progressbar.Percentage(), progressbar.Bar(), progressbar.ETA()
], maxval=num_lines
).start()
with io.open(filename, encoding="utf8") as f:
with io.open(filename, encoding=encoding, newline="") as f:
reader = csv.reader(f, delimiter=options["sepchar"])
i = 0
for row in reader:
@@ -81,4 +92,39 @@ class ImportCommand(BaseCommand):
"""Command entry point."""
exts_pool.load_all()
for filename in options["files"]:
self._import(filename, options)
try:
with transaction.atomic():
self._import(filename, options)
except CommandError as exc:
raise exc
except UnicodeDecodeError:
self.stdout.write(self.style.NOTICE(
_("CSV file is not encoded in UTF-8, attempting to guess "
"encoding")
))
detector = UniversalDetector()
with io.open(filename, "rb") as fp:
for line in fp:
detector.feed(line)
if detector.done:
break
detector.close()
self.stdout.write(self.style.NOTICE(
_("Reading CSV file using %(encoding)s encoding") %
detector.result
))
try:
with transaction.atomic():
self._import(
filename, options,
encoding=detector.result["encoding"]
)
except UnicodeDecodeError as exc:
six.raise_from(
CommandError(
_("Unable to decode CSV file using %(encoding)s "
"encoding") % detector.result
),
exc
)
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
"""Management command to create DKIM keys."""
from __future__ import print_function, unicode_literals
@@ -11,6 +13,7 @@ from modoboa.lib import sysutils
from modoboa.parameters import tools as param_tools
from .... import models
from .... import signals
class ManageDKIMKeys(BaseCommand):
@@ -50,3 +53,5 @@ class ManageDKIMKeys(BaseCommand):
enable_dkim=True, dkim_private_key_path="")
for domain in qset:
self.create_new_dkim_key(domain)
if qset.exists():
signals.new_dkim_keys.send(sender=self.__class__, domains=qset)
@@ -18,15 +18,14 @@ 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
from modoboa.admin import models
from modoboa.admin import constants, models
from modoboa.parameters import tools as param_tools
class CheckMXRecords(BaseCommand):
"""Command class."""
help = "Check defined domains." # noqa:A003
help = "Check defined domains." # NOQA:A003
@cached_property
def providers(self):
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
"""Management command to check and fix known problems."""
from __future__ import print_function, unicode_literals
@@ -6,11 +8,9 @@ from django.conf import settings
from django.core.management.base import BaseCommand
from django.utils.encoding import smart_str
from modoboa.lib.permissions import grant_access_to_object
from modoboa.lib.permissions import get_object_owner
from modoboa.core.models import User
from modoboa.admin import models
from modoboa.core.models import User
from modoboa.lib.permissions import get_object_owner, grant_access_to_object
known_problems = []
@@ -97,7 +97,7 @@ def sometimes_mailbox_have_no_alias(**options):
class Repair(BaseCommand):
"""Command class."""
help = "Check and fix known problems."
help = "Check and fix known problems." # NOQA:A003
def add_arguments(self, parser):
"""Add extra arguments to command."""
@@ -113,7 +113,10 @@ class Repair(BaseCommand):
# Load known problems from extensions.
for ext in settings.MODOBOA_APPS:
try:
__import__(ext, locals(), globals(), [smart_str("known_problems")])
__import__(
ext, locals(), globals(),
[smart_str("known_problems")]
)
except ImportError:
pass
for func in known_problems:
+4 -2
View File
@@ -1,13 +1,15 @@
# -*- coding: utf-8 -*-
"""Admin models."""
from __future__ import unicode_literals
from .alias import Alias, AliasRecipient
from .base import AdminObject
from .domain import Domain
from .domain_alias import DomainAlias
from .mailbox import Mailbox, Quota, MailboxOperation, SenderAddress
from .mailbox import Mailbox, MailboxOperation, Quota, SenderAddress
from .mxrecord import DNSBLResult, MXRecord
from .alias import Alias, AliasRecipient
__all__ = [
"AdminObject",
+10 -9
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
"""Models related to aliases management."""
from __future__ import unicode_literals
@@ -5,25 +7,24 @@ from __future__ import unicode_literals
import hashlib
import random
from django.urls import reverse
from reversion import revisions as reversion
from django.db import models
from django.urls import reverse
from django.utils.encoding import (
python_2_unicode_compatible, smart_text, force_str, force_bytes
force_bytes, force_str, python_2_unicode_compatible, smart_text
)
from django.utils.translation import ugettext as _, ugettext_lazy
from reversion import revisions as reversion
from modoboa.core import signals as core_signals
from modoboa.lib.email_utils import split_mailbox
from modoboa.lib.exceptions import (
PermDeniedException, BadRequest, Conflict, NotFound
BadRequest, Conflict, NotFound, PermDeniedException
)
from .. import signals
from .base import AdminObject
from .domain import Domain
from .mailbox import Mailbox
from .. import signals
@python_2_unicode_compatible
@@ -48,7 +49,7 @@ class Alias(AdminObject):
ugettext_lazy("Description"), blank=True)
expire_at = models.DateTimeField(
ugettext_lazy("Expire at"), blank=True, null=True)
_objectname = 'MailboxAlias'
_objectname = "MailboxAlias"
class Meta:
permissions = (
@@ -139,7 +140,7 @@ class Alias(AdminObject):
domain=domain, address=local_part).first()
if rcpt is None:
rcpt = Alias.objects.filter(
address='%s@%s' % (local_part, domname)
address="%s@%s" % (local_part, domname)
).first()
if rcpt is None:
raise NotFound(
+3 -2
View File
@@ -1,12 +1,13 @@
# -*- 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
from django.contrib.contenttypes.fields import GenericRelation
from modoboa.core import models as core_models
from modoboa.lib.permissions import (
grant_access_to_object, ungrant_access_to_object
+27 -13
View File
@@ -1,33 +1,34 @@
# -*- 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 (
python_2_unicode_compatible, smart_text, force_text
force_text, python_2_unicode_compatible, smart_text
)
from django.utils.functional import cached_property
from django.utils.translation import ugettext as _, ugettext_lazy
from reversion import revisions as reversion
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 .base import AdminObject
from .. import constants
from .base import AdminObject
@python_2_unicode_compatible
class Domain(AdminObject):
"""Mail domain."""
name = models.CharField(ugettext_lazy('name'), max_length=100, unique=True,
name = models.CharField(ugettext_lazy("name"), max_length=100, unique=True,
help_text=ugettext_lazy("The domain name"))
quota = models.PositiveIntegerField(
default=0,
@@ -45,11 +46,11 @@ class Domain(AdminObject):
)
)
enabled = models.BooleanField(
ugettext_lazy('enabled'),
ugettext_lazy("enabled"),
help_text=ugettext_lazy("Check to activate this domain"),
default=True
)
type = models.CharField(default="domain", max_length=20)
type = models.CharField(default="domain", max_length=20) # NOQA:A003
enable_dns_checks = models.BooleanField(
ugettext_lazy("Enable DNS checks"), default=True,
help_text=ugettext_lazy("Check to enable DNS checks for this domain")
@@ -205,8 +206,21 @@ class Domain(AdminObject):
@property
def bind_format_dkim_public_key(self):
"""Return DKIM public key using Bind format."""
return "{}._domainkey 10800 IN TXT \"v=DKIM1; k=rsa; p={}\"".format(
self.dkim_key_selector, self.dkim_public_key)
record = "v=DKIM1;k=rsa;p=%s" % self.dkim_public_key
# TXT records longer than 255 characters need split into chunks
# split record into 74 character chunks (+2 for indent, +2 for quotes(")
# == 78 characters) to make more readable in text editors.
split_record = []
while record:
if len(record) > 74:
split_record.append(" \"%s\"" % record[:74])
record = record[74:]
else:
split_record.append(" \"%s\"" % record)
break
record = "\n".join(split_record)
return "{}._domainkey.{}. 10800 IN TXT (\n{})".format(
self.dkim_key_selector, self.name, record)
def add_admin(self, account):
"""Add a new administrator to this domain.
@@ -245,9 +259,9 @@ class Domain(AdminObject):
def save(self, *args, **kwargs):
"""Store current data if domain is renamed."""
if self.oldname != self.name:
self.old_mail_homes = (
dict((mb.id, mb.mail_home) for mb in self.mailbox_set.all())
)
self.old_mail_homes = {
mb.id: mb.mail_home for mb in self.mailbox_set.all()
}
super(Domain, self).save(*args, **kwargs)
def delete(self, fromuser, keepdir=False):
+11 -10
View File
@@ -1,19 +1,18 @@
# -*- coding: utf-8 -*-
"""Models related to domain aliases management."""
from __future__ import unicode_literals
from reversion import revisions as reversion
from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
from django.utils.encoding import python_2_unicode_compatible, smart_text
from django.utils.translation import ugettext as _, ugettext_lazy
from django.contrib.contenttypes.fields import GenericRelation
from reversion import revisions as reversion
from modoboa.core import models as core_models
from modoboa.core import signals as core_signals
from modoboa.core import models as core_models, signals as core_signals
from modoboa.lib.exceptions import BadRequest, Conflict
from .base import AdminObject
from .domain import Domain
@@ -39,12 +38,12 @@ class DomainAlias(AdminObject):
name = models.CharField(ugettext_lazy("name"), max_length=100, unique=True,
help_text=ugettext_lazy("The alias name"))
target = models.ForeignKey(
Domain, verbose_name=ugettext_lazy('target'),
Domain, verbose_name=ugettext_lazy("target"),
help_text=ugettext_lazy("The domain this alias points to"),
on_delete=models.CASCADE
)
enabled = models.BooleanField(
ugettext_lazy('enabled'),
ugettext_lazy("enabled"),
help_text=ugettext_lazy("Check to activate this alias"),
default=True
)
@@ -65,7 +64,8 @@ class DomainAlias(AdminObject):
def from_csv(self, user, row):
"""Create a domain alias from a CSV row
Expected format: ["domainalias", domain alias name, targeted domain, enabled]
Expected format: ["domainalias", domain alias name, targeted domain,
enabled]
:param user: a ``User`` object
:param row: a list containing the alias definition
@@ -94,4 +94,5 @@ class DomainAlias(AdminObject):
csvwriter.writerow(["domainalias", self.name,
self.target.name, self.enabled])
reversion.register(DomainAlias)
+15 -14
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
"""Models related to mailboxes management."""
from __future__ import unicode_literals
@@ -5,6 +7,8 @@ from __future__ import unicode_literals
import os
import pwd
from reversion import revisions as reversion
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models import Q
@@ -12,14 +16,11 @@ from django.db.models.manager import Manager
from django.utils.encoding import python_2_unicode_compatible, smart_text
from django.utils.translation import ugettext as _, ugettext_lazy
from reversion import revisions as reversion
from modoboa.core.models import User
from modoboa.lib import exceptions as lib_exceptions
from modoboa.lib.email_utils import split_mailbox
from modoboa.lib.sysutils import exec_cmd
from modoboa.parameters import tools as param_tools
from .base import AdminObject
from .domain import Domain
@@ -42,7 +43,7 @@ class Quota(models.Model):
"""Keeps track of Mailbox current quota."""
username = models.EmailField(primary_key=True, max_length=254)
bytes = models.BigIntegerField(default=0)
bytes = models.BigIntegerField(default=0) # NOQA:A003
messages = models.IntegerField(default=0)
objects = QuotaManager()
@@ -66,9 +67,9 @@ class MailboxManager(Manager):
"""
qf = None
if squery is not None:
if '@' in squery:
parts = squery.split('@')
addrfilter = '@'.join(parts[:-1])
if "@" in squery:
parts = squery.split("@")
addrfilter = "@".join(parts[:-1])
domfilter = parts[-1]
qf = (
Q(address__contains=addrfilter) &
@@ -81,7 +82,7 @@ class MailboxManager(Manager):
)
ids = admin.objectaccess_set \
.filter(content_type=ContentType.objects.get_for_model(Mailbox)) \
.values_list('object_id', flat=True)
.values_list("object_id", flat=True)
if qf is not None:
qf = Q(pk__in=ids) & qf
else:
@@ -94,7 +95,7 @@ class Mailbox(AdminObject):
"""User mailbox."""
address = models.CharField(
ugettext_lazy('address'), max_length=252,
ugettext_lazy("address"), max_length=252,
help_text=ugettext_lazy(
"Mailbox address (without the @domain.tld part)")
)
@@ -375,8 +376,8 @@ class MailboxOperation(models.Model):
mailbox = models.ForeignKey(Mailbox, blank=True, null=True,
on_delete=models.CASCADE)
type = models.CharField(
max_length=20, choices=(('rename', 'rename'), ('delete', 'delete'))
type = models.CharField( # NOQA:A003
max_length=20, choices=(("rename", "rename"), ("delete", "delete"))
)
argument = models.TextField()
@@ -384,6 +385,6 @@ class MailboxOperation(models.Model):
app_label = "admin"
def __str__(self):
if self.type == 'rename':
return 'Rename %s -> %s' % (self.argument, self.mailbox.mail_home)
return 'Delete %s' % self.argument
if self.type == "rename":
return "Rename %s -> %s" % (self.argument, self.mailbox.mail_home)
return "Delete %s" % self.argument
+2
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
"""MX records storage."""
from __future__ import unicode_literals
+6 -4
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
"""Map file definitions for postfix."""
from __future__ import unicode_literals
@@ -7,7 +9,7 @@ class DomainsMap(object):
"""Map to list all domains."""
filename = 'sql-domains.cf'
filename = "sql-domains.cf"
mysql = (
"SELECT name FROM admin_domain "
"WHERE name='%s' AND type='domain' AND enabled=1"
@@ -26,7 +28,7 @@ class DomainsAliasesMap(object):
"""Map to list all domain aliases."""
filename = 'sql-domain-aliases.cf'
filename = "sql-domain-aliases.cf"
mysql = (
"SELECT dom.name FROM admin_domain dom "
"INNER JOIN admin_domainalias domal ON dom.id=domal.target_id "
@@ -48,7 +50,7 @@ class AliasesMap(object):
"""A map to list all mailbox aliases."""
filename = 'sql-aliases.cf'
filename = "sql-aliases.cf"
mysql = (
"SELECT alr.address FROM modoboa_admin_aliasrecipient AS alr "
"INNER JOIN admin_alias AS al ON alr.alias_id=al.id "
@@ -73,7 +75,7 @@ class MaintainMap(object):
"""Map files to list non available mailboxes."""
filename = 'sql-maintain.cf'
filename = "sql-maintain.cf"
mysql = (
"SELECT '450 Requested mail action not taken: mailbox unavailable' "
"FROM admin_mailbox mb INNER JOIN admin_domain dom "
+17 -13
View File
@@ -1,27 +1,28 @@
# -*- coding: utf-8 -*-
"""Admin serializers."""
from __future__ import unicode_literals
from django.conf import settings
from django.contrib.auth import password_validation
from django.core.exceptions import ValidationError
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext as _, ugettext_lazy
from django.utils.encoding import force_text
from django.contrib.auth import password_validation
from django.utils.translation import ugettext as _, ugettext_lazy
from rest_framework import serializers
from modoboa.admin import models as admin_models
from modoboa.core import constants as core_constants
from modoboa.core import models as core_models
from modoboa.core import signals as core_signals
from modoboa.lib import exceptions as lib_exceptions
from modoboa.lib import permissions, email_utils, fields as lib_fields
from modoboa.core import (
constants as core_constants, models as core_models, signals as core_signals
)
from modoboa.lib import (
email_utils, exceptions as lib_exceptions, fields as lib_fields,
permissions
)
from modoboa.parameters import tools as param_tools
from . import lib
from . import models
from . import lib, models
class DomainSerializer(serializers.ModelSerializer):
@@ -32,7 +33,10 @@ class DomainSerializer(serializers.ModelSerializer):
model = models.Domain
fields = (
"pk", "name", "quota", "default_mailbox_quota", "enabled", "type",
"enable_dkim", "dkim_key_selector", "dkim_key_length",
"dkim_public_key"
)
read_only_fields = ("pk", "dkim_public_key", )
def validate_name(self, value):
"""Check name constraints."""
@@ -62,8 +66,8 @@ class DomainSerializer(serializers.ModelSerializer):
domain = models.Domain(**validated_data)
creator = self.context["request"].user
core_signals.can_create_object.send(
sender=self.__class__, context=creator,
klass=models.Domain, instance=domain)
sender=self.__class__, context=creator,
klass=models.Domain, instance=domain)
domain.save(creator=creator)
return domain
+3
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
"""Modoboa admin signals."""
from __future__ import unicode_literals
@@ -30,3 +32,4 @@ get_domain_form_instances = django.dispatch.Signal(
# get_domain_tags = django.dispatch.Signal(providing_args=["domain"])
import_object = django.dispatch.Signal(providing_args=["objtype"])
use_external_recipients = django.dispatch.Signal(providing_args=["recipients"])
new_dkim_keys = django.dispatch.Signal(providing_args=["domains"])
+19
View File
@@ -161,6 +161,24 @@ Domains.prototype = {
$(document).trigger('domain_listing_refresh');
},
/**
* Add a type to let the server know what kind of object we expect.
*
* @this Listing
*/
get_load_page_args: function() {
var args = Admin.prototype.get_load_page_args.call(this);
if (this.navobj.getbaseurl() === "list") {
args.objtype = "domain";
this.options.eor_message = gettext("No more domain to show");
} else {
args.objtype = "quota";
this.options.eor_message = gettext("No more quota to show");
}
return args;
},
/**
* Navigation callback: default.
*
@@ -305,6 +323,7 @@ Domains.prototype = {
},
success_callback: $.proxy(this.reload_listing, this)
});
$(document).trigger('domwizard_init');
},
domainform_cb: function() {
@@ -1,6 +1,6 @@
{% load i18n %}
<div class="modal-dialog {% block modal_css_classes %}{% endblock %}">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">{% trans "Close" %}</span></button>
@@ -13,7 +13,7 @@
</div>
<div class="alert alert-info" style="overflow-wrap: break-word;">
<strong>{% trans "Bind/named format" %}</strong><br>
{{ domain.bind_format_dkim_public_key }}
<span style="white-space: pre">{{ domain.bind_format_dkim_public_key }}</span>
</div>
</div>
<div class="modal-footer">
+1 -1
View File
@@ -15,7 +15,7 @@
<script type="text/javascript">
$(document).ready(function() {
admin = new Domains({
load_page_url: "{% url 'admin:_domain_list' %}"
load_page_url: "{% url 'admin:domain_page' %}"
});
});
</script>
-1
View File
@@ -1 +0,0 @@
from __future__ import unicode_literals
+10 -9
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
"""Admin extension tags."""
from __future__ import unicode_literals
@@ -5,15 +7,14 @@ from __future__ import unicode_literals
from functools import reduce
from django import template
from django.urls import reverse
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _, ugettext_lazy
from modoboa.core import signals as core_signals
from modoboa.lib.templatetags.lib_tags import render_link
from modoboa.lib.web_utils import render_actions
from .. import signals
register = template.Library()
@@ -75,7 +76,7 @@ def domains_menu(selection, user, ajax_mode=True):
"modalcb": "admin.exportform_cb"}
]
return render_to_string('common/menulist.html', {
return render_to_string("common/menulist.html", {
"entries": entries,
"selection": selection,
"user": user
@@ -123,7 +124,7 @@ def identities_menu(user, selection=None, ajax_mode=True):
"modalcb": "admin.exportform_cb"}
]
return render_to_string('common/menulist.html', {
return render_to_string("common/menulist.html", {
"entries": entries,
"user": user
})
@@ -157,7 +158,7 @@ def domain_actions(user, domain):
responses = signals.extra_domain_actions.send(
sender=None, user=user, domain=domain)
for receiver, response in responses:
for _receiver, response in responses:
if response:
actions += response
@@ -229,15 +230,15 @@ def domain_aliases(domain):
:rtype: str
"""
if not domain.aliases.count():
return '---'
res = ''
return "---"
res = ""
for alias in domain.aliases.all():
res += '%s<br/>' % alias.name
res += "%s<br/>" % alias.name
return mark_safe(res)
@register.simple_tag
def identity_modify_link(identity, active_tab='default'):
def identity_modify_link(identity, active_tab="default"):
"""Return the appropriate modification link.
According to the identity type, a specific modification link (URL)
-1
View File
@@ -1 +0,0 @@
from __future__ import unicode_literals
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash
#!/bin/sh
echo "2.12"
+44 -47
View File
@@ -1,34 +1,31 @@
# coding: utf-8
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from unittest import skipIf
from django.urls import reverse
from django.test import override_settings
from django.urls import reverse
from modoboa.core import factories as core_factories
from modoboa.core.tests import test_ldap
from modoboa.core.models import User
from modoboa.lib.tests import ModoTestCase
from modoboa.lib.tests import NO_LDAP
from modoboa.core.tests import test_ldap
from modoboa.lib.tests import NO_LDAP, ModoTestCase
from modoboa.limits import utils as limits_utils
from .. import factories
from .. import models
from .. import factories, models
class AuthenticationTestCase(ModoTestCase):
"""Check authentication."""
@classmethod
def setUpTestData(cls):
def setUpTestData(cls): # NOQA:N802
"""Create test data."""
super(AuthenticationTestCase, cls).setUpTestData()
cls.mb = factories.MailboxFactory(
domain__name="test.com", address="user",
user__username="user@test.com",
user__groups=('SimpleUsers',)
user__groups=("SimpleUsers",)
)
def test_authentication_unicode(self):
@@ -46,18 +43,18 @@ class AuthenticationTestCase(ModoTestCase):
class AccountTestCase(ModoTestCase):
@classmethod
def setUpTestData(cls):
def setUpTestData(cls): # NOQA:N802
"""Create test data."""
super(AccountTestCase, cls).setUpTestData()
factories.populate_database()
def test_crud(self):
values = dict(
username="tester@test.com", first_name="Tester", last_name="Toto",
password1="Toto1234", password2="Toto1234", role="SimpleUsers",
quota_act=True,
is_active=True, email="tester@test.com", stepid='step2'
)
values = {
"username": "tester@test.com", "first_name": "Tester",
"last_name": "Toto", "password1": "Toto1234",
"password2": "Toto1234", "role": "SimpleUsers", "quota_act": True,
"is_active": True, "email": "tester@test.com", "stepid": "step2"
}
self.ajax_post(reverse("admin:account_add"), values)
account = User.objects.get(username="tester@test.com")
@@ -344,12 +341,12 @@ class AccountTestCase(ModoTestCase):
def test_utf8_username(self):
"""Create an account with non-ASCII characters."""
values = dict(
username="téster@test.com", first_name="Tester", last_name="Toto",
password1="Toto1234", password2="Toto1234", role="SimpleUsers",
quota_act=True,
is_active=True, email="téster@test.com", stepid="step2"
)
values = {
"username": "téster@test.com", "first_name": "Tester",
"last_name": "Toto", "password1": "Toto1234",
"password2": "Toto1234", "role": "SimpleUsers", "quota_act": True,
"is_active": True, "email": "téster@test.com", "stepid": "step2"
}
self.ajax_post(reverse("admin:account_add"), values)
def _set_quota(self, email, value, expected_status=200):
@@ -459,8 +456,8 @@ class AccountTestCase(ModoTestCase):
@skipIf(NO_LDAP, "No ldap module installed")
@override_settings(AUTHENTICATION_BACKENDS=(
'modoboa.lib.authbackends.LDAPBackend',
'django.contrib.auth.backends.ModelBackend',
"modoboa.lib.authbackends.LDAPBackend",
"django.contrib.auth.backends.ModelBackend",
))
class LDAPAccountTestCase(test_ldap.LDAPTestCaseMixin, ModoTestCase):
"""Check LDAP related code."""
@@ -481,11 +478,11 @@ class LDAPAccountTestCase(test_ldap.LDAPTestCaseMixin, ModoTestCase):
class PermissionsTestCase(ModoTestCase):
@classmethod
def setUpTestData(cls):
def setUpTestData(cls): # NOQA:N802
"""Create test data."""
super(PermissionsTestCase, cls).setUpTestData()
parameters = {}
for name, tpl in limits_utils.get_user_limit_templates():
for name, _definition in limits_utils.get_user_limit_templates():
parameters["deflt_user_{0}_limit".format(name)] = 2
cls.localconfig.parameters.set_values(parameters, app="limits")
cls.localconfig.save()
@@ -597,7 +594,7 @@ class PermissionsTestCase(ModoTestCase):
response = self.client.get(
reverse("admin:account_change", args=[self.user.id]),
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
HTTP_X_REQUESTED_WITH="XMLHttpRequest")
self.assertNotEqual(response["Content-Type"], "application/json")
def test_domainadmin_deletes_superadmin(self):
@@ -607,12 +604,12 @@ class PermissionsTestCase(ModoTestCase):
for the associated domain, this domain admin must not be able
to access the super admin.
"""
values = dict(
username="superadmin2@test.com", first_name="Super",
last_name="Admin", password1="Toto1234", password2="Toto1234",
role="SuperAdmins", is_active=True,
email="superadmin2@test.com", stepid='step2'
)
values = {
"username": "superadmin2@test.com", "first_name": "Super",
"last_name": "Admin", "password1": "Toto1234",
"password2": "Toto1234", "role": "SuperAdmins", "is_active": True,
"email": "superadmin2@test.com", "stepid": "step2"
}
self.ajax_post(
reverse("admin:account_add"),
values
@@ -627,22 +624,22 @@ class PermissionsTestCase(ModoTestCase):
def test_domainadmin_dlist_local_domain_not_owned(self):
"""Check if a domain admin can use a local mailbox he can't
access as a recipient in a distribution list"""
values = dict(
address="all@test.com",
recipients="user@test.com",
recipients_1="user@test2.com",
enabled=True
)
values = {
"address": "all@test.com",
"recipients": "user@test.com",
"recipients_1": "user@test2.com",
"enabled": True
}
self.ajax_post(reverse("admin:alias_add"), values)
def test_domainadmin_master_user(self):
"""Check domain administrator is not allowed to access this feature."""
values = dict(
username="user10@test.com", first_name="Test",
last_name="Test", password1="Toto1234", password2="Toto1234",
role="SimpleUsers", is_active=True, master_user=True,
email="user10@test.com", stepid='step2'
)
values = {
"username": "user10@test.com", "first_name": "Test",
"last_name": "Test", "password1": "Toto1234",
"password2": "Toto1234", "role": "SimpleUsers", "is_active": True,
"master_user": True, "email": "user10@test.com", "stepid": "step2"
}
self.ajax_post(
reverse("admin:account_add"),
values, status=400
@@ -656,7 +653,7 @@ class PermissionsTestCase(ModoTestCase):
dom = models.Domain.objects.get(name="test.com")
mb = factories.MailboxFactory(
domain=dom, address="admin2",
user__username="admin2@test.com", user__groups=('DomainAdmins', ),
user__username="admin2@test.com", user__groups=("DomainAdmins", ),
user__password="{PLAIN}toto")
dom.add_admin(mb.user)
self.client.logout()
+47 -44
View File
@@ -1,34 +1,33 @@
# coding: utf-8
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
"""Admin test cases."""
from django.urls import reverse
from modoboa.core.models import User
from modoboa.lib.tests import ModoTestCase
from .. import factories
from ..models import Alias, AliasRecipient, Domain
"""Admin test cases."""
class AliasTestCase(ModoTestCase):
@classmethod
def setUpTestData(cls):
def setUpTestData(cls): # NOQA:N802
"""Create test data."""
super(AliasTestCase, cls).setUpTestData()
factories.populate_database()
def test_alias(self):
user = User.objects.get(username="user@test.com")
values = dict(
username="user@test.com", role=user.role,
is_active=user.is_active, email="user@test.com",
aliases="toto@test.com", aliases_1="titi@test.com",
language="en"
)
values = {
"username": "user@test.com", "role": user.role,
"is_active": user.is_active, "email": "user@test.com",
"aliases": "toto@test.com", "aliases_1": "titi@test.com",
"language": "en"
}
self.ajax_post(
reverse("admin:account_change", args=[user.id]),
values
@@ -66,11 +65,11 @@ class AliasTestCase(ModoTestCase):
def test_upper_case_alias(self):
"""Try to create an upper case alias."""
user = User.objects.get(username="user@test.com")
values = dict(
username="user@test.com", role=user.role,
is_active=user.is_active, email="user@test.com",
aliases="Toto@test.com", language="en"
)
values = {
"username": "user@test.com", "role": user.role,
"is_active": user.is_active, "email": "user@test.com",
"aliases": "Toto@test.com", "language": "en"
}
self.ajax_post(
reverse("admin:account_change", args=[user.id]),
values
@@ -96,11 +95,11 @@ class AliasTestCase(ModoTestCase):
def test_append_alias_with_tag(self):
"""Try to create a alias with tag in recipient address"""
user = User.objects.get(username="user@test.com")
values = dict(
username="user@test.com", role=user.role,
is_active=user.is_active, email="user@test.com",
language="en"
)
values = {
"username": "user@test.com", "role": user.role,
"is_active": user.is_active, "email": "user@test.com",
"language": "en"
}
self.ajax_post(
reverse("admin:account_change", args=[user.id]),
values
@@ -120,11 +119,13 @@ class AliasTestCase(ModoTestCase):
def test_utf8_alias(self):
"""Test alias with non-ASCII characters."""
values = dict(address="testé@test.com",
recipients="user@test.com",
recipients_1="admin@test.com",
recipients_2="ext@titi.com",
enabled=True)
values = {
"address": "testé@test.com",
"recipients": "user@test.com",
"recipients_1": "admin@test.com",
"recipients_2": "ext@titi.com",
"enabled": True
}
self.ajax_post(
reverse("admin:alias_add"), values
)
@@ -168,7 +169,7 @@ class AliasTestCase(ModoTestCase):
Alias.DoesNotExist, Alias.objects.get, address="all@test.com")
def test_forward(self):
values = dict(address="forward2@test.com", recipients="rcpt@dest.com")
values = {"address": "forward2@test.com", "recipients": "rcpt@dest.com"}
self.ajax_post(
reverse("admin:alias_add"), values
)
@@ -191,7 +192,7 @@ class AliasTestCase(ModoTestCase):
Alias.DoesNotExist, Alias.objects.get, address="forward2@test.com")
def test_forward_and_local_copies(self):
values = dict(address="user@test.com", recipients="rcpt@dest.com")
values = {"address": "user@test.com", "recipients": "rcpt@dest.com"}
self.ajax_post(
reverse("admin:alias_add"), values
)
@@ -242,21 +243,23 @@ class AliasTestCase(ModoTestCase):
changed.
"""
values = dict(address="list@test.com",
recipients="user@test.com",
recipients_1="admin@test.com",
enabled=True)
values = {
"address": "list@test.com",
"recipients": "user@test.com",
"recipients_1": "admin@test.com",
"enabled": True
}
self.ajax_post(
reverse("admin:alias_add"), values
)
user = User.objects.get(username="user@test.com")
values = dict(
username=user.username, first_name="Tester", last_name="Toto",
password1="Toto1234", password2="Toto1234", role="SimpleUsers",
quota_act=True, is_active=True, email=user.email,
language="en"
)
values = {
"username": user.username, "first_name": "Tester",
"last_name": "Toto", "password1": "Toto1234",
"password2": "Toto1234", "role": "SimpleUsers", "quota_act": True,
"is_active": True, "email": user.email, "language": "en"
}
self.ajax_post(reverse("admin:account_change", args=[user.id]), values)
self.assertEqual(
@@ -277,12 +280,12 @@ class AliasTestCase(ModoTestCase):
self.client.force_login(admin)
user = User.objects.get(username="user@test.com")
values = dict(
username="user@test.com", role=user.role,
is_active=user.is_active, email="user@test.com",
aliases="toto@test.com", aliases_1="titi@test2.com",
language="en"
)
values = {
"username": "user@test.com", "role": user.role,
"is_active": user.is_active, "email": "user@test.com",
"aliases": "toto@test.com", "aliases_1": "titi@test2.com",
"language": "en"
}
response = self.ajax_post(
reverse("admin:account_change", args=[user.id]),
values, 400
+24 -21
View File
@@ -8,7 +8,6 @@ import copy
import json
import dns.resolver
from mock import patch
from django.test import override_settings
from django.urls import reverse
@@ -16,20 +15,24 @@ from django.urls import reverse
from rest_framework.authtoken.models import Token
from modoboa.admin import models as admin_models
from modoboa.core import factories as core_factories
from modoboa.core import models as core_models
from modoboa.core import factories as core_factories, models as core_models
from modoboa.lib.tests import ModoAPITestCase
from . import utils
from .. import factories
from .. import models
from .. import factories, models
try:
# mock is part of the Python (>= 3.3) standard library
from unittest import mock
except ImportError:
# fall back to the mock backport
import mock
class DomainAPITestCase(ModoAPITestCase):
"""Check API."""
@classmethod
def setUpTestData(cls): # noqa: N802
def setUpTestData(cls): # NOQA:N802
"""Create test data."""
super(DomainAPITestCase, cls).setUpTestData()
factories.populate_database()
@@ -69,13 +72,13 @@ class DomainAPITestCase(ModoAPITestCase):
self.assertIn("default_mailbox_quota", response.data)
self.client.credentials(
HTTP_AUTHORIZATION='Token ' + self.da_token.key)
HTTP_AUTHORIZATION="Token " + self.da_token.key)
response = self.client.post(
url, {"name": "test4.com", "default_mailbox_quota": 10})
self.assertEqual(response.status_code, 403)
@patch.object(dns.resolver.Resolver, "query")
@patch("socket.getaddrinfo")
@mock.patch.object(dns.resolver.Resolver, "query")
@mock.patch("socket.getaddrinfo")
def test_create_domain_with_mx_check(self, mock_getaddrinfo, mock_query):
"""Check domain creation when MX check is activated."""
self.set_global_parameter("enable_admin_limits", False, app="limits")
@@ -84,7 +87,7 @@ class DomainAPITestCase(ModoAPITestCase):
reseller = core_factories.UserFactory(
username="reseller", groups=("Resellers", ))
token = Token.objects.create(user=reseller)
self.client.credentials(HTTP_AUTHORIZATION='Token ' + token.key)
self.client.credentials(HTTP_AUTHORIZATION="Token " + token.key)
url = reverse("api:domain-list")
mock_query.side_effect = utils.mock_dns_query_result
@@ -140,7 +143,7 @@ class DomainAliasAPITestCase(ModoAPITestCase):
"""Check DomainAlias API."""
@classmethod
def setUpTestData(cls): # noqa: N802
def setUpTestData(cls): # NOQA:N802
"""Create test data."""
super(DomainAliasAPITestCase, cls).setUpTestData()
factories.populate_database()
@@ -169,7 +172,7 @@ class DomainAliasAPITestCase(ModoAPITestCase):
self.assertEqual(len(response.data), 1)
self.client.credentials(
HTTP_AUTHORIZATION='Token ' + self.da_token.key)
HTTP_AUTHORIZATION="Token " + self.da_token.key)
response = self.client.get(reverse("api:domain_alias-list"))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), 0)
@@ -191,7 +194,7 @@ class DomainAliasAPITestCase(ModoAPITestCase):
self.assertEqual(dalias.target, target)
self.client.credentials(
HTTP_AUTHORIZATION='Token ' + self.da_token.key)
HTTP_AUTHORIZATION="Token " + self.da_token.key)
response = self.client.post(
url, {"name": "dalias4.com", "target": target.pk}, format="json")
self.assertEqual(response.status_code, 403)
@@ -234,7 +237,7 @@ class AccountAPITestCase(ModoAPITestCase):
}
@classmethod
def setUpTestData(cls): # noqa: N802
def setUpTestData(cls): # NOQA:N802
"""Create test data."""
super(AccountAPITestCase, cls).setUpTestData()
factories.populate_database()
@@ -389,7 +392,7 @@ class AccountAPITestCase(ModoAPITestCase):
def test_create_account_as_domadmin(self):
"""As DomainAdmin, try to create a new account."""
self.client.credentials(
HTTP_AUTHORIZATION='Token ' + self.da_token.key)
HTTP_AUTHORIZATION="Token " + self.da_token.key)
data = copy.deepcopy(self.ACCOUNT_DATA)
data["mailbox"]["quota"] = 1000
url = reverse("api:account-list")
@@ -566,7 +569,7 @@ class AliasAPITestCase(ModoAPITestCase):
}
@classmethod
def setUpTestData(cls): # noqa: N802
def setUpTestData(cls): # NOQA:N802
"""Create test data."""
super(AliasAPITestCase, cls).setUpTestData()
cls.localconfig.parameters.set_value(
@@ -604,7 +607,7 @@ class AliasAPITestCase(ModoAPITestCase):
response = self.client.post(url, self.ALIAS_DATA, format="json")
self.assertEqual(response.status_code, 201)
alias = json.loads(response.content.decode('utf-8'))
alias = json.loads(response.content.decode("utf-8"))
alias = models.Alias.objects.filter(pk=alias["pk"]).first()
domadmin = core_models.User.objects.get(username="admin@test.com")
self.assertTrue(domadmin.can_access(alias))
@@ -626,7 +629,7 @@ class AliasAPITestCase(ModoAPITestCase):
def test_create_alias_as_domadmin(self):
"""As DomainAdmin, try to create a new alias."""
self.client.credentials(
HTTP_AUTHORIZATION='Token ' + self.da_token.key)
HTTP_AUTHORIZATION="Token " + self.da_token.key)
url = reverse("api:alias-list")
response = self.client.post(url, self.ALIAS_DATA, format="json")
self.assertEqual(response.status_code, 201)
@@ -689,7 +692,7 @@ class SenderAddressAPITestCase(ModoAPITestCase):
"""Check SenderAddress API."""
@classmethod
def setUpTestData(cls): # noqa: N802
def setUpTestData(cls): # NOQA:N802
"""Create test data."""
super(SenderAddressAPITestCase, cls).setUpTestData()
cls.localconfig.parameters.set_value(
@@ -723,7 +726,7 @@ class SenderAddressAPITestCase(ModoAPITestCase):
self.assertEqual(response.status_code, 201)
self.client.credentials(
HTTP_AUTHORIZATION='Token ' + self.da_token.key)
HTTP_AUTHORIZATION="Token " + self.da_token.key)
data = {"address": "user@test2.com", "mailbox": mbox.pk}
response = self.client.post(url, data)
self.assertEqual(response.status_code, 400)
@@ -0,0 +1,2 @@
domain;domain1.com;1000;100;True
domain;dómain2.com;2000;200;True
1 domain domain1.com 1000 100 True
2 domain dómain2.com 2000 200 True
@@ -0,0 +1,3 @@
domain;domain1.com;1000;100;True
domain;dómain2.com;2000;200;True
domain;dómain2.com;2000;200;True
1 domain domain1.com 1000 100 True
2 domain dómain2.com 2000 200 True
3 domain dómain2.com 2000 200 True
@@ -0,0 +1,2 @@
domain;domain1.com;1000;100;True
domain;dómain2.com;2000;200;True
1 domain domain1.com 1000 100 True
2 domain dómain2.com 2000 200 True
+23 -10
View File
@@ -9,7 +9,6 @@ import shutil
import tempfile
import dns.resolver
from mock import patch
from testfixtures import compare
from django.core.management import call_command
@@ -20,17 +19,23 @@ from modoboa.core import factories as core_factories
from modoboa.core.models import User
from modoboa.core.tests.test_views import SETTINGS_SAMPLE
from modoboa.lib.tests import ModoTestCase
from . import utils
from .. import factories
from ..models import Domain, Alias
from ..models import Alias, Domain
try:
# mock is part of the Python (>= 3.3) standard library
from unittest import mock
except ImportError:
# fall back to the mock backport
import mock
class DomainTestCase(ModoTestCase):
"""Test case for Domain."""
@classmethod
def setUpTestData(cls): # noqa: N802
def setUpTestData(cls): # NOQA:N802
"""Create test data."""
super(DomainTestCase, cls).setUpTestData()
factories.populate_database()
@@ -176,7 +181,7 @@ class DomainTestCase(ModoTestCase):
values = {
"name": "pouet.com", "quota": 10, "default_mailbox_quota": 100,
"create_dom_admin": True, "dom_admin_username": "toto",
"create_aliases": True, "type": "domain", "stepid": 'step3'
"create_aliases": True, "type": "domain", "stepid": "step3"
}
response = self.ajax_post(
reverse("admin:domain_add"),
@@ -206,8 +211,8 @@ class DomainTestCase(ModoTestCase):
self.assertContains(response, "value=\"500\"")
self.assertContains(response, "value=\"50\"")
@patch.object(dns.resolver.Resolver, "query")
@patch("socket.getaddrinfo")
@mock.patch.object(dns.resolver.Resolver, "query")
@mock.patch("socket.getaddrinfo")
def test_create_and_check_mx(self, mock_getaddrinfo, mock_query):
"""Check for authorized MX record."""
reseller = core_factories.UserFactory(
@@ -347,22 +352,30 @@ class DomainTestCase(ModoTestCase):
url = reverse("core:dashboard")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertIn("Global statistics", response.content.decode('utf-8'))
self.assertIn("Global statistics", response.content.decode("utf-8"))
self.client.force_login(
User.objects.get(username="admin@test.com"))
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
content = response.content.decode('utf-8')
content = response.content.decode("utf-8")
self.assertNotIn("Global statistics", content)
self.assertIn("Per-domain statistics", content)
def test_page_loader(self):
"""Test page loader view."""
url = reverse("admin:domain_page")
response = self.ajax_get(url)
self.assertIn("handle_mailboxes", response)
response = self.ajax_get("{}?objtype=quota".format(url))
self.assertIn("progress-bar", response["rows"])
class DKIMTestCase(ModoTestCase):
"""Test case for DKIM."""
@classmethod
def setUpTestData(cls): # noqa: N802
def setUpTestData(cls): # NOQA:N802
"""Create test data."""
super(DKIMTestCase, cls).setUpTestData()
factories.populate_database()
+4 -5
View File
@@ -1,23 +1,22 @@
# coding: utf-8
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.urls import reverse
from modoboa.lib.tests import ModoTestCase
from .. import factories
from ..models import Domain, DomainAlias, Alias
from ..models import Alias, Domain, DomainAlias
class DomainAliasTestCase(ModoTestCase):
@classmethod
def setUpTestData(cls):
def setUpTestData(cls): # NOQA:N802
"""Create test data."""
super(DomainAliasTestCase, cls).setUpTestData()
factories.populate_database()
cls.dom = Domain.objects.get(name='test.com')
cls.dom = Domain.objects.get(name="test.com")
def test_model(self):
dom = Domain.objects.get(name="test.com")
+8 -10
View File
@@ -1,4 +1,4 @@
# coding: utf-8
# -*- coding: utf-8 -*-
"""Export related test cases."""
@@ -8,16 +8,14 @@ from django.urls import reverse
from django.utils.encoding import force_text
from modoboa.lib.tests import ModoTestCase
from .. import factories
from .. import models
from .. import factories, models
class ExportTestCase(ModoTestCase):
"""Test case for export operations."""
@classmethod
def setUpTestData(cls):
def setUpTestData(cls): # NOQA:N802
"""Create test data."""
super(ExportTestCase, cls).setUpTestData()
factories.populate_database()
@@ -40,7 +38,7 @@ class ExportTestCase(ModoTestCase):
{"filename": "test.csv"}
)
def assertListEqual(self, list1, list2):
def assertListEqual(self, list1, list2): # NOQA:N802
list1 = force_text(list1).split("\r\n")
list2 = force_text(list2).split("\r\n")
self.assertEqual(len(list1), len(list2))
@@ -67,7 +65,7 @@ class ExportTestCase(ModoTestCase):
def test_export_identities(self):
response = self.__export_identities()
self.assertListEqual(
"account;admin@test.com;{PLAIN}toto;;;True;DomainAdmins;admin@test.com;10;test.com\r\naccount;admin@test2.com;{PLAIN}toto;;;True;DomainAdmins;admin@test2.com;10;test2.com\r\naccount;user@test.com;{PLAIN}toto;;;True;SimpleUsers;user@test.com;10\r\naccount;user@test2.com;{PLAIN}toto;;;True;SimpleUsers;user@test2.com;10\r\nalias;alias@test.com;True;user@test.com\r\nalias;forward@test.com;True;user@external.com\r\nalias;postmaster@test.com;True;test@truc.fr;toto@titi.com\r\n",
"account;admin@test.com;{PLAIN}toto;;;True;DomainAdmins;admin@test.com;10;test.com\r\naccount;admin@test2.com;{PLAIN}toto;;;True;DomainAdmins;admin@test2.com;10;test2.com\r\naccount;user@test.com;{PLAIN}toto;;;True;SimpleUsers;user@test.com;10\r\naccount;user@test2.com;{PLAIN}toto;;;True;SimpleUsers;user@test2.com;10\r\nalias;alias@test.com;True;user@test.com\r\nalias;forward@test.com;True;user@external.com\r\nalias;postmaster@test.com;True;test@truc.fr;toto@titi.com\r\n", # NOQA:E501
response.content.strip()
)
@@ -81,7 +79,7 @@ class ExportTestCase(ModoTestCase):
idtfilter="account", grpfilter="SimpleUsers"
)
self.assertListEqual(
"account;user@test.com;{PLAIN}toto;;;True;SimpleUsers;user@test.com;10\r\naccount;user@test2.com;{PLAIN}toto;;;True;SimpleUsers;user@test2.com;10\r\naccount;toto@test.com;{PLAIN}toto;Léon;;True;SimpleUsers;toto@test.com;10",
"account;user@test.com;{PLAIN}toto;;;True;SimpleUsers;user@test.com;10\r\naccount;user@test2.com;{PLAIN}toto;;;True;SimpleUsers;user@test2.com;10\r\naccount;toto@test.com;{PLAIN}toto;Léon;;True;SimpleUsers;toto@test.com;10", # NOQA:E501
response.content.strip()
)
@@ -105,7 +103,7 @@ class ExportTestCase(ModoTestCase):
idtfilter="account", grpfilter="DomainAdmins"
)
self.assertListEqual(
"account;admin@test.com;{PLAIN}toto;;;True;DomainAdmins;admin@test.com;10;test.com\r\naccount;admin@test2.com;{PLAIN}toto;;;True;DomainAdmins;admin@test2.com;10;test2.com",
"account;admin@test.com;{PLAIN}toto;;;True;DomainAdmins;admin@test.com;10;test.com\r\naccount;admin@test2.com;{PLAIN}toto;;;True;DomainAdmins;admin@test2.com;10;test2.com", # NOQA:E501
response.content.strip()
)
@@ -113,5 +111,5 @@ class ExportTestCase(ModoTestCase):
response = self.__export_identities(idtfilter="alias")
self.assertEqual(
response.content.decode().strip(),
"alias;alias@test.com;True;user@test.com\r\nalias;forward@test.com;True;user@external.com\r\nalias;postmaster@test.com;True;test@truc.fr;toto@titi.com"
"alias;alias@test.com;True;user@test.com\r\nalias;forward@test.com;True;user@external.com\r\nalias;postmaster@test.com;True;test@truc.fr;toto@titi.com" # NOQA:E501
)
+95 -14
View File
@@ -2,25 +2,34 @@
from __future__ import unicode_literals
import os
import dns.resolver
from mock import patch
from django.core.files.base import ContentFile
from django.core.management import call_command
from django.core.management.base import CommandError
from django.urls import reverse
from modoboa.core import factories as core_factories
from modoboa.core.models import User
from modoboa.lib.tests import ModoTestCase
from . import utils
from .. import factories
from ..models import Domain, Alias, DomainAlias
from ..models import Alias, Domain, DomainAlias
try:
# mock is part of the Python (>= 3.3) standard library
from unittest import mock
except ImportError:
# fall back to the mock backport
import mock
class ImportTestCase(ModoTestCase):
@classmethod
def setUpTestData(cls): # noqa:N802
def setUpTestData(cls): # NOQA:N802
"""Create test data."""
super(ImportTestCase, cls).setUpTestData()
cls.localconfig.parameters.set_value(
@@ -79,8 +88,8 @@ domainalias; domalias1.com; domain1.com; True
response,
"Default mailbox quota cannot be greater than domain quota")
@patch.object(dns.resolver.Resolver, "query")
@patch("socket.getaddrinfo")
@mock.patch.object(dns.resolver.Resolver, "query")
@mock.patch("socket.getaddrinfo")
def test_domain_import_with_mx_check(self, mock_getaddrinfo, mock_query):
"""Check domain import when MX check is enabled."""
reseller = core_factories.UserFactory(
@@ -135,7 +144,7 @@ alias; alias1@test.com; True; user1@test.com
forward; alias2@test.com; True; user1+ext@test.com
forward; fwd1@test.com; True; user@extdomain.com
dlist; dlist@test.com; True; user1@test.com; user@extdomain.com
""", name="identities.csv")
""", name="identities.csv") # NOQA:E501
self.client.post(
reverse("admin:identity_import"),
{"sourcefile": f, "crypt_password": True}
@@ -201,7 +210,7 @@ dlist; dlist@test.com; True; user1@test.com; user@extdomain.com
"""Try to import an account for nonlocal domain."""
f = ContentFile(b"""
account; user1@nonlocal.com; toto; User; One; True; SimpleUsers; user1@nonlocal.com; 0
""", name="identities.csv")
""", name="identities.csv") # NOQA:E501
self.client.post(
reverse("admin:identity_import"),
{"sourcefile": f, "crypt_password": True}
@@ -212,12 +221,12 @@ account; user1@nonlocal.com; toto; User; One; True; SimpleUsers; user1@nonlocal.
def test_import_invalid_quota(self):
f = ContentFile(b"""
account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com; ; test.com
""", name="identities.csv")
""", name="identities.csv") # NOQA:E501
resp = self.client.post(
reverse("admin:identity_import"),
{"sourcefile": f, "crypt_password": True}
)
self.assertIn('wrong quota value', resp.content.decode())
self.assertIn("wrong quota value", resp.content.decode())
def test_import_domain_by_domainadmin(self):
"""Check if a domain admin is not allowed to import a domain."""
@@ -230,7 +239,9 @@ domain; domain2.com; 1000; 200; False
reverse("admin:identity_import"),
{"sourcefile": f, "crypt_password": True}
)
self.assertIn("You are not allowed to import domains", resp.content.decode())
self.assertIn(
"You are not allowed to import domains",
resp.content.decode())
f = ContentFile(b"""
domainalias; domalias1.com; test.com; True
""", name="identities.csv")
@@ -239,7 +250,8 @@ domainalias; domalias1.com; test.com; True
{"sourcefile": f, "crypt_password": True}
)
self.assertIn(
"You are not allowed to import domain aliases", resp.content.decode())
"You are not allowed to import domain aliases",
resp.content.decode())
def test_import_quota_too_big(self):
self.client.logout()
@@ -271,7 +283,7 @@ account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com
f = ContentFile("""
account; admin@test.com; toto; Admin; ; True; DomainAdmins; admin@test.com; 0; test.com
account; truc@test.com; toto; René; Truc; True; DomainAdmins; truc@test.com; 0; test.com
""", name="identities.csv")
""", name="identities.csv") # NOQA:E501
self.client.post(
reverse("admin:identity_import"),
{"sourcefile": f, "crypt_password": True,
@@ -292,7 +304,7 @@ account; truc@test.com; toto; René; Truc; True; DomainAdmins; truc@test.com; 0;
)
f = ContentFile(b"""
account; sa@test.com; toto; Super; Admin; True; SuperAdmins; superadmin@test.com; 50
""", name="identities.csv")
""", name="identities.csv") # NOQA:E501
self.client.post(
reverse("admin:identity_import"),
{"sourcefile": f, "crypt_password": True,
@@ -345,3 +357,72 @@ dómain; dómªin2.com; 1000; 100; True
self.assertEqual(dom.default_mailbox_quota, 100)
self.assertTrue(dom.enabled)
self.assertTrue(admin.is_owner(dom))
def test_import_command_missing_file(self):
with mock.patch("os.path.isfile") as mock_isfile:
mock_isfile.return_value = False
with self.assertRaises(CommandError) as cm:
call_command("modo", "import", "test.csv")
ex_message = cm.exception.messages
self.assertEqual(ex_message, "File not found")
def test_import_command(self):
test_file = os.path.join(
os.path.dirname(__file__),
"test_data/import_domains.csv"
)
call_command("modo", "import", test_file)
admin = User.objects.get(username="admin")
dom = Domain.objects.get(name="domain1.com")
self.assertEqual(dom.quota, 1000)
self.assertEqual(dom.default_mailbox_quota, 100)
self.assertTrue(dom.enabled)
self.assertTrue(admin.is_owner(dom))
dom = Domain.objects.get(name="dómain2.com")
self.assertEqual(dom.quota, 2000)
self.assertEqual(dom.default_mailbox_quota, 200)
self.assertTrue(dom.enabled)
self.assertTrue(admin.is_owner(dom))
def test_import_command_non_utf8(self):
test_file = os.path.join(
os.path.dirname(__file__),
"test_data/import_domains_iso8859.csv"
)
call_command("modo", "import", test_file)
dom = Domain.objects.get(name="dómain2.com")
self.assertEqual(dom.quota, 2000)
def test_import_command_duplicates(self):
test_file = os.path.join(
os.path.dirname(__file__),
"test_data/import_domains_duplicates.csv"
)
with self.assertRaises(CommandError) as cm:
call_command("modo", "import", test_file)
ex_message = cm.exception.messages
self.assertTrue(ex_message.startswith("Object already exists: "))
def test_import_command_duplicates_continue(self):
test_file = os.path.join(
os.path.dirname(__file__),
"test_data/import_domains_duplicates.csv"
)
call_command("modo", "import", "--continue-if-exists", test_file)
admin = User.objects.get(username="admin")
dom = Domain.objects.get(name="domain1.com")
self.assertEqual(dom.quota, 1000)
self.assertEqual(dom.default_mailbox_quota, 100)
self.assertTrue(dom.enabled)
self.assertTrue(admin.is_owner(dom))
dom = Domain.objects.get(name="dómain2.com")
self.assertEqual(dom.quota, 2000)
self.assertEqual(dom.default_mailbox_quota, 200)
self.assertTrue(dom.enabled)
self.assertTrue(admin.is_owner(dom))
+11 -6
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
"""Management command tests."""
from __future__ import unicode_literals
@@ -6,16 +8,19 @@ import os
import shutil
import tempfile
import mock
from django.core.management import call_command
from django.urls import reverse
from django.test import override_settings
from django.urls import reverse
from modoboa.lib.tests import ModoTestCase
from .. import factories, models
from .. import factories
from .. import models
try:
# mock is part of the Python (>= 3.3) standard library
from unittest import mock
except ImportError:
# fall back to the mock backport
import mock
@override_settings(
@@ -24,7 +29,7 @@ class MailboxOperationTestCase(ModoTestCase):
"""Test management command."""
@classmethod
def setUpTestData(cls):
def setUpTestData(cls): # NOQA:N802
"""Create test data."""
super(MailboxOperationTestCase, cls).setUpTestData()
factories.populate_database()
+2
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
"""Test map files generation."""
from __future__ import unicode_literals
+34 -31
View File
@@ -5,29 +5,32 @@
from __future__ import unicode_literals
import dns.resolver
from mock import patch
from testfixtures import LogCapture
from django.core import mail
from django.core import management
from django.core import mail, management
from django.test import override_settings
from django.urls import reverse
from django.utils.translation import ugettext as _
from modoboa.core import factories as core_factories
from modoboa.lib.tests import ModoTestCase
from . import utils
from .. import factories
from .. import factories, models
from ..lib import get_domain_mx_list
from .. import models
try:
# mock is part of the Python (>= 3.3) standard library
from unittest import mock
except ImportError:
# fall back to the mock backport
import mock
class MXTestCase(ModoTestCase):
"""TestCase for DNSBL related features."""
@classmethod
def setUpTestData(cls): # noqa:N802
def setUpTestData(cls): # NOQA:N802
"""Create some data."""
super(MXTestCase, cls).setUpTestData()
cls.domain = factories.DomainFactory(name="modoboa.org")
@@ -52,11 +55,11 @@ class MXTestCase(ModoTestCase):
cls.localconfig.save()
models.MXRecord.objects.all().delete()
@patch("gevent.socket.gethostbyname")
@patch("socket.getaddrinfo")
@patch.object(dns.resolver.Resolver, "query")
@mock.patch("gevent.socket.gethostbyname")
@mock.patch("socket.getaddrinfo")
@mock.patch.object(dns.resolver.Resolver, "query")
def test_management_command(
self, mock_query, mock_getaddrinfo, mock_g_gethostbyname):
self, mock_query, mock_getaddrinfo, mock_g_gethostbyname):
"""Check that command works fine."""
mock_query.side_effect = utils.mock_dns_query_result
mock_getaddrinfo.side_effect = utils.mock_ip_query_result
@@ -80,11 +83,11 @@ class MXTestCase(ModoTestCase):
qs = models.MXRecord.objects.filter(domain=self.domain)
self.assertEqual(id_, qs[0].id)
@patch("gevent.socket.gethostbyname")
@patch("socket.getaddrinfo")
@patch.object(dns.resolver.Resolver, "query")
@mock.patch("gevent.socket.gethostbyname")
@mock.patch("socket.getaddrinfo")
@mock.patch.object(dns.resolver.Resolver, "query")
def test_single_domain_update(
self, mock_query, mock_getaddrinfo, mock_g_gethostbyname):
self, mock_query, mock_getaddrinfo, mock_g_gethostbyname):
"""Update only one domain."""
mock_query.side_effect = utils.mock_dns_query_result
mock_getaddrinfo.side_effect = utils.mock_ip_query_result
@@ -105,8 +108,8 @@ class MXTestCase(ModoTestCase):
management.call_command(
"modo", "check_mx", "--domain", "toto.com")
@patch("socket.getaddrinfo")
@patch.object(dns.resolver.Resolver, "query")
@mock.patch("socket.getaddrinfo")
@mock.patch.object(dns.resolver.Resolver, "query")
def test_get_domain_mx_list_logging(self, mock_query, mock_getaddrinfo):
mock_query.side_effect = utils.mock_dns_query_result
mock_getaddrinfo.side_effect = utils.mock_ip_query_result
@@ -133,7 +136,7 @@ class MXTestCase(ModoTestCase):
("modoboa.admin", "WARNING",
_("Invalid IP address format for %(domain)s; %(addr)s")
% {"domain": "bad-response.example.com", "addr": "BAD"}),
)
)
@override_settings(DNSBL_PROVIDERS=["zen.spamhaus.org"])
@@ -141,7 +144,7 @@ class DNSBLTestCase(ModoTestCase):
"""TestCase for DNSBL related features."""
@classmethod
def setUpTestData(cls): # noqa:N802
def setUpTestData(cls): # NOQA:N802
"""Create some data."""
super(DNSBLTestCase, cls).setUpTestData()
cls.domain = factories.DomainFactory(name="modoboa.org")
@@ -152,11 +155,11 @@ class DNSBLTestCase(ModoTestCase):
name="modoboa.com", enable_dns_checks=False)
models.DNSBLResult.objects.all().delete()
@patch("gevent.socket.gethostbyname")
@patch("socket.getaddrinfo")
@patch.object(dns.resolver.Resolver, "query")
@mock.patch("gevent.socket.gethostbyname")
@mock.patch("socket.getaddrinfo")
@mock.patch.object(dns.resolver.Resolver, "query")
def test_management_command(
self, mock_query, mock_getaddrinfo, mock_g_gethostbyname):
self, mock_query, mock_getaddrinfo, mock_g_gethostbyname):
"""Check that command works fine."""
mock_query.side_effect = utils.mock_dns_query_result
mock_getaddrinfo.side_effect = utils.mock_ip_query_result
@@ -173,11 +176,11 @@ class DNSBLTestCase(ModoTestCase):
self.assertFalse(self.domain.uses_a_reserved_tld)
self.assertTrue(self.domain2.uses_a_reserved_tld)
@patch("gevent.socket.gethostbyname")
@patch("socket.getaddrinfo")
@patch.object(dns.resolver.Resolver, "query")
@mock.patch("gevent.socket.gethostbyname")
@mock.patch("socket.getaddrinfo")
@mock.patch.object(dns.resolver.Resolver, "query")
def test_notifications(
self, mock_query, mock_getaddrinfo, mock_g_gethostbyname):
self, mock_query, mock_getaddrinfo, mock_g_gethostbyname):
"""Check notifications."""
mock_query.side_effect = utils.mock_dns_query_result
mock_getaddrinfo.side_effect = utils.mock_ip_query_result
@@ -189,11 +192,11 @@ class DNSBLTestCase(ModoTestCase):
print(message.subject)
self.assertEqual(len(mail.outbox), 2)
@patch("gevent.socket.gethostbyname")
@patch("socket.getaddrinfo")
@patch.object(dns.resolver.Resolver, "query")
@mock.patch("gevent.socket.gethostbyname")
@mock.patch("socket.getaddrinfo")
@mock.patch.object(dns.resolver.Resolver, "query")
def test_management_command_no_dnsbl(
self, mock_query, mock_getaddrinfo, mock_g_gethostbyname):
self, mock_query, mock_getaddrinfo, mock_g_gethostbyname):
"""Check that command works fine without dnsbl."""
mock_query.side_effect = utils.mock_dns_query_result
mock_getaddrinfo.side_effect = utils.mock_ip_query_result
+19 -18
View File
@@ -1,28 +1,29 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.urls import reverse
from modoboa.core.models import User
from modoboa.lib.tests import ModoTestCase
from .. import factories
class PasswordSchemesTestCase(ModoTestCase):
@classmethod
def setUpTestData(cls):
def setUpTestData(cls): # NOQA:N802
"""Create test data."""
super(PasswordSchemesTestCase, cls).setUpTestData()
factories.populate_database()
def _create_account(self):
values = dict(
username="tester@test.com", first_name="Tester", last_name="Toto",
password1="Toto1234", password2="Toto1234", role="SimpleUsers",
quota_act=True,
is_active=True, email="tester@test.com", stepid='step2'
)
values = {
"username": "tester@test.com", "first_name": "Tester",
"last_name": "Toto", "password1": "Toto1234",
"password2": "Toto1234", "role": "SimpleUsers", "quota_act": True,
"is_active": True, "email": "tester@test.com", "stepid": "step2"
}
self.ajax_post(
reverse("admin:account_add"),
values
@@ -31,30 +32,30 @@ class PasswordSchemesTestCase(ModoTestCase):
def _test_scheme(self, name, startpattern):
self.set_global_parameter("password_scheme", name, app="core")
self._create_account()
account = User.objects.get(username='tester@test.com')
account = User.objects.get(username="tester@test.com")
self.assertTrue(account.password.startswith(startpattern))
self.assertTrue(account.check_password('Toto1234'))
self.assertTrue(account.check_password("Toto1234"))
def test_bcrypt_scheme(self):
self._test_scheme('blfcrypt', '{BLF-CRYPT}')
self._test_scheme("blfcrypt", "{BLF-CRYPT}")
def test_sha512crypt_scheme(self):
self._test_scheme('sha512crypt', '{SHA512-CRYPT}')
self._test_scheme("sha512crypt", "{SHA512-CRYPT}")
def test_sha256crypt_scheme(self):
self._test_scheme('sha256crypt', '{SHA256-CRYPT}')
self._test_scheme("sha256crypt", "{SHA256-CRYPT}")
def test_md5crypt_scheme(self):
self._test_scheme('md5crypt', '{MD5-CRYPT}')
self._test_scheme("md5crypt", "{MD5-CRYPT}")
def test_sha256_scheme(self):
self._test_scheme('sha256', '{SHA256}')
self._test_scheme("sha256", "{SHA256}")
def test_md5_scheme(self):
self._test_scheme('md5', '{MD5}')
self._test_scheme("md5", "{MD5}")
def test_crypt(self):
self._test_scheme('crypt', '{CRYPT}')
self._test_scheme("crypt", "{CRYPT}")
def test_plain(self):
self._test_scheme('plain', '{PLAIN}')
self._test_scheme("plain", "{PLAIN}")
+6 -7
View File
@@ -1,22 +1,21 @@
# -*- coding: utf-8 -*-
"""Repair command tests"""
from __future__ import unicode_literals
from django.core import management
from modoboa.lib.permissions import get_object_owner
from modoboa.lib.permissions import ObjectAccess
from modoboa.lib.permissions import ObjectAccess, get_object_owner
from modoboa.lib.tests import ModoTestCase
from .. import factories
from .. import models
from .. import factories, models
class RepairTestCase(ModoTestCase):
"""TestCase for repair command."""
@classmethod
def setUpTestData(cls):
def setUpTestData(cls): # NOQA:N802
"""Create some data."""
super(RepairTestCase, cls).setUpTestData()
factories.populate_database()
@@ -49,7 +48,7 @@ class RepairTestCase(ModoTestCase):
def test_management_command_with_nul_domain(self):
"""Just assume nothing raise when an alias has no domain."""
models.Alias.objects.create(address='@modoboa.xxx')
models.Alias.objects.create(address="@modoboa.xxx")
ret = management.call_command("modo", "repair", "--quiet")
assert ret is None
+2 -1
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
"""User related tests."""
from __future__ import unicode_literals
@@ -6,7 +8,6 @@ from django.urls import reverse
from modoboa.core.models import User
from modoboa.lib.tests import ModoTestCase
from ..factories import populate_database
from ..models import Alias
+3 -2
View File
@@ -8,12 +8,13 @@ import socket
from dns.name import Name
from dns.rdtypes.ANY.MX import MX
from dns.resolver import NoAnswer, NoNameservers, NXDOMAIN, Timeout
from dns.resolver import NXDOMAIN, NoAnswer, NoNameservers, Timeout
_MX_RECORD_1 = MX("IN", "MX", 10, Name("mx.example.com".split(".")))
_MX_RECORD_2 = MX("IN", "MX", 10, Name("mx2.example.com".split(".")))
_BAD_MX_RECORD = MX("IN", "MX", 10, Name("bad-response.example.com".split(".")))
_DNE_MX_RECORD = MX("IN", "MX", 10, Name("does-not-exist.example.com".split(".")))
_DNE_MX_RECORD = MX("IN", "MX", 10, Name(
"does-not-exist.example.com".split(".")))
_MX_RECORDS = [_MX_RECORD_1]
_IPV4_RECORD_1 = (socket.AF_INET, socket.SOCK_STREAM, 6, "", ("192.0.2.1", 25))
+9 -8
View File
@@ -1,4 +1,5 @@
# pylint: disable=invalid-name, protected-access
# -*- coding: utf-8 -*-
"""Admin urls."""
from __future__ import unicode_literals
@@ -6,12 +7,10 @@ from __future__ import unicode_literals
from django.conf.urls import url
from . import views
from .views import import_ as import_views
from .views import alias as alias_views
from .views import domain as domain_views
from .views import export as export_views
from .views import identity as identity_views
from .views import (
alias as alias_views, domain as domain_views, export as export_views,
identity as identity_views, import_ as import_views
)
urlpatterns = [
url(r'^$', domain_views.index, name="index"),
@@ -27,11 +26,13 @@ urlpatterns = [
name="domain_quota_list"),
url(r'^domains/flatlist/$', domain_views.domains_list,
name="domain_flat_list"),
url(r'^domains/new/', domain_views.newdomain, name='domain_add'),
url(r'^domains/new/', domain_views.newdomain, name="domain_add"),
url(r'^domains/(?P<dom_id>\d+)/edit/$', domain_views.editdomain,
name="domain_change"),
url(r'^domains/(?P<dom_id>\d+)/delete/$', domain_views.deldomain,
name="domain_delete"),
url(r'^domains/page/$', domain_views.get_next_page,
name="domain_page"),
]
urlpatterns += [
+2 -1
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
"""Admin API urls."""
from __future__ import unicode_literals
@@ -6,7 +8,6 @@ from rest_framework import routers
from . import api
router = routers.SimpleRouter()
router.register(r"domains", api.DomainViewSet, base_name="domain")
router.register(
+3 -2
View File
@@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
"""Shortcuts."""
from __future__ import unicode_literals
from .alias import AliasDetailView
from .identity import AccountDetailView
from .dns import DNSBLDomainDetailView, MXDomainDetailView
from .domain import DomainDetailView
from .identity import AccountDetailView
__all__ = [
"AccountDetailView",
+15 -17
View File
@@ -1,24 +1,22 @@
# -*- coding: utf-8 -*-
"""Alias related views."""
from __future__ import unicode_literals
from django.urls import reverse
from reversion import revisions as reversion
from django.contrib.auth import mixins as auth_mixins
from django.contrib.auth.decorators import login_required, permission_required
from django.db import IntegrityError
from django.shortcuts import render
from django.urls import reverse
from django.utils.translation import ugettext as _, ungettext
from django.views import generic
from django.contrib.auth import mixins as auth_mixins
from django.contrib.auth.decorators import (
login_required, permission_required
)
from reversion import revisions as reversion
from modoboa.core import signals as core_signals
from modoboa.lib.exceptions import PermDeniedException, Conflict
from modoboa.lib.exceptions import Conflict, PermDeniedException
from modoboa.lib.web_utils import render_to_json_response
from ..forms import AliasForm
from ..models import Alias
@@ -37,7 +35,7 @@ def _validate_alias(request, form, successmsg, callback=None):
callback(request.user, alias)
return render_to_json_response(successmsg)
return render_to_json_response({'form_errors': form.errors}, status=400)
return render_to_json_response({"form_errors": form.errors}, status=400)
def _new_alias(request, title, action, successmsg,
@@ -87,12 +85,12 @@ def editalias(request, alid, tplname="admin/aliasform.html"):
return _validate_alias(request, form, successmsg)
ctx = {
'action': reverse("admin:alias_change", args=[alias.id]),
'formid': 'aliasform',
'title': alias.address,
'action_label': _('Update'),
'action_classes': 'submit',
'form': AliasForm(request.user, instance=alias)
"action": reverse("admin:alias_change", args=[alias.id]),
"formid": "aliasform",
"title": alias.address,
"action_label": _("Update"),
"action_classes": "submit",
"form": AliasForm(request.user, instance=alias)
}
return render(request, tplname, ctx)
+4 -4
View File
@@ -1,12 +1,12 @@
# -*- coding: utf-8 -*-
"""DNSBL related views."""
from __future__ import unicode_literals
from django.views import generic
from django.utils.translation import ugettext as _
from django.contrib.auth import mixins as auth_mixins
from django.utils.translation import ugettext as _
from django.views import generic
from .. import models
+36 -25
View File
@@ -1,34 +1,34 @@
# -*- coding: utf-8 -*-
"""Domain related views."""
from __future__ import unicode_literals
from functools import reduce
from django.urls import reverse
from django.db.models import Sum
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.template.loader import render_to_string
from django.utils.translation import ugettext as _, ungettext
from django.views import generic
from django.views.decorators.csrf import ensure_csrf_cookie
from reversion import revisions as reversion
from django.contrib.auth import mixins as auth_mixins
from django.contrib.auth.decorators import (
login_required, permission_required, user_passes_test
)
from reversion import revisions as reversion
from django.db.models import Sum
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.translation import ugettext as _, ungettext
from django.views import generic
from django.views.decorators.csrf import ensure_csrf_cookie
from modoboa.core import signals as core_signals
from modoboa.lib.web_utils import render_to_json_response
from modoboa.lib.exceptions import PermDeniedException
from modoboa.lib.listing import get_sort_order, get_listing_page
from modoboa.lib.listing import get_listing_page, get_sort_order
from modoboa.lib.web_utils import render_to_json_response
from .. import signals
from ..forms import DomainForm, DomainWizard
from ..lib import get_domains
from ..models import Domain, Mailbox
from .. import signals
@login_required
@@ -47,20 +47,20 @@ def _domains(request):
if extra_filters:
extra_filters = reduce(
lambda a, b: a + b, [result[1] for result in extra_filters])
filters = dict(
(flt, request.GET.get(flt, None))
filters = {
flt: request.GET.get(flt, None)
for flt in ["domfilter", "searchquery"] + extra_filters
)
request.session['domains_filters'] = filters
}
request.session["domains_filters"] = filters
domainlist = get_domains(request.user, **filters)
if sort_order == 'name':
if sort_order == "name":
domainlist = sorted(
domainlist,
key=lambda d: getattr(d, sort_order), reverse=sort_dir == '-'
key=lambda d: getattr(d, sort_order), reverse=sort_dir == "-"
)
else:
domainlist = sorted(domainlist, key=lambda d: d.tags[0]["name"],
reverse=sort_dir == '-')
reverse=sort_dir == "-")
context = {
"handle_mailboxes": request.localconfig.parameters.get_value(
"handle_mailboxes", raise_exception=False),
@@ -108,6 +108,19 @@ def domains(request, tplname="admin/domains.html"):
})
@login_required
@user_passes_test(
lambda u: u.has_perm("admin.view_domains") or
u.has_perm("admin.view_mailboxes") or
u.has_perm("admin.add_domain")
)
def get_next_page(request):
"""Return the next page of the domain or quota list."""
if request.GET.get("objtype", "domain") == "domain":
return _domains(request)
return list_quotas(request)
@login_required
@permission_required("core.add_user")
def domains_list(request):
@@ -118,8 +131,6 @@ def domains_list(request):
@login_required
@permission_required("admin.add_domain")
def list_quotas(request):
from modoboa.lib.db_utils import db_type
sort_order, sort_dir = get_sort_order(request.GET, "name")
domains = Domain.objects.get_for_admin(request.user)
domains = domains.exclude(quota=0)
@@ -164,7 +175,7 @@ def editdomain(request, dom_id):
if not request.user.can_access(domain):
raise PermDeniedException
instances = dict(general=domain)
instances = {"general": domain}
results = signals.get_domain_form_instances.send(
sender="editdomain", user=request.user, domain=domain)
for result in results:
@@ -217,7 +228,7 @@ class DomainDetailView(
"enable_mx_checks": parameters.get_value("enable_mx_checks"),
"enable_dnsbl_checks": parameters.get_value("enable_dnsbl_checks"),
})
for receiver, widgets in result:
for _receiver, widgets in result:
for widget in widgets:
context["templates"][widget["column"]].append(
widget["template"])
+12 -7
View File
@@ -1,23 +1,28 @@
# -*- coding: utf-8 -*-
"""Export related views."""
from __future__ import unicode_literals
from backports import csv
from rfc6266 import build_header
from django.contrib.auth.decorators import (
login_required, permission_required, user_passes_test
)
from django.urls import reverse
from django.http import HttpResponse
from django.shortcuts import render
from django.utils.translation import ugettext as _
from django.urls import reverse
from django.utils import six
from django.utils.translation import ugettext as _
from ..forms import ExportIdentitiesForm, ExportDomainsForm
from ..forms import ExportDomainsForm, ExportIdentitiesForm
from ..lib import get_domains, get_identities
if six.PY2:
from backports import csv
else:
import csv
def _export(content, filename):
"""Export a csv file's content
@@ -52,7 +57,7 @@ def export_identities(request):
fp = six.StringIO()
csvwriter = csv.writer(fp, delimiter=form.cleaned_data["sepchar"])
identities = get_identities(
request.user, **request.session['identities_filters'])
request.user, **request.session["identities_filters"])
for ident in identities:
ident.to_csv(csvwriter)
content = fp.getvalue()
@@ -80,7 +85,7 @@ def export_domains(request):
fp = six.StringIO()
csvwriter = csv.writer(fp, delimiter=form.cleaned_data["sepchar"])
for dom in get_domains(request.user,
**request.session['domains_filters']):
**request.session["domains_filters"]):
dom.to_csv(csvwriter)
content = fp.getvalue()
fp.close()
+25 -26
View File
@@ -1,33 +1,29 @@
# -*- coding: utf-8 -*-
"""Identity related views."""
from __future__ import unicode_literals
from reversion import revisions as reversion
from django.contrib.auth import mixins as auth_mixins
from django.contrib.auth.decorators import (
login_required, permission_required, user_passes_test
)
from django.shortcuts import render
from django.template.loader import render_to_string
from django.utils.translation import ugettext as _, ungettext
from django.views import generic
from django.views.decorators.csrf import ensure_csrf_cookie
from django.contrib.auth import mixins as auth_mixins
from django.contrib.auth.decorators import (
login_required, permission_required, user_passes_test
)
from reversion import revisions as reversion
from modoboa.core.models import User
from modoboa.lib.exceptions import (
PermDeniedException, BadRequest
)
from modoboa.lib.listing import (
get_sort_order, get_listing_page
)
from modoboa.lib.exceptions import BadRequest, PermDeniedException
from modoboa.lib.listing import get_listing_page, get_sort_order
from modoboa.lib.web_utils import render_to_json_response
from .. import signals
from ..forms import AccountForm, AccountWizard
from ..lib import get_identities
from ..models import Mailbox, Domain
from .. import signals
from ..models import Domain, Mailbox
@login_required
@@ -36,18 +32,20 @@ from .. import signals
u.has_perm("admin.add_alias")
)
def _identities(request):
filters = dict((fname, request.GET.get(fname, None))
for fname in ['searchquery', 'idtfilter', 'grpfilter'])
request.session['identities_filters'] = filters
filters = {
fname: request.GET.get(fname, None)
for fname in ["searchquery", "idtfilter", "grpfilter"]
}
request.session["identities_filters"] = filters
idents_list = get_identities(request.user, **filters)
sort_order, sort_dir = get_sort_order(request.GET, "identity",
["identity", "name_or_rcpt", "tags"])
if sort_order in ["identity", "name_or_rcpt"]:
objects = sorted(idents_list, key=lambda o: getattr(o, sort_order),
reverse=sort_dir == '-')
reverse=sort_dir == "-")
else:
objects = sorted(idents_list, key=lambda o: o.tags[0],
reverse=sort_dir == '-')
reverse=sort_dir == "-")
context = {
"handle_mailboxes": request.localconfig.parameters.get_value(
"handle_mailboxes", raise_exception=False)
@@ -104,7 +102,7 @@ def list_quotas(request):
"* 1048576)) * 100"
)
mboxes = mboxes.extra(
select={'quota_usage': select},
select={"quota_usage": select},
where=["admin_quota.username=%s" % where],
tables=["admin_quota", "admin_domain"],
order_by=["%s%s" % (sort_dir, sort_order)]
@@ -157,7 +155,7 @@ def identities(request, tplname="admin/identities.html"):
@permission_required("core.add_user")
def accounts_list(request):
accs = User.objects.filter(is_superuser=False) \
.exclude(groups__name='SimpleUsers')
.exclude(groups__name="SimpleUsers")
res = [a.username for a in accs.all()]
return render_to_json_response(res)
@@ -179,8 +177,9 @@ def editaccount(request, pk):
raise PermDeniedException
mb = account.mailbox if hasattr(account, "mailbox") else None
instances = dict(
general=account, profile=account, mail=mb, perms=account)
instances = {
"general": account, "profile": account, "mail": mb, "perms": account
}
results = signals.get_account_form_instances.send(
sender="editaccount", user=request.user, account=account)
for result in results:
@@ -238,7 +237,7 @@ class AccountDetailView(
result = signals.extra_account_dashboard_widgets.send(
self.__class__, user=self.request.user, account=self.object)
context["templates"] = {"left": [], "right": []}
for receiver, widgets in result:
for _receiver, widgets in result:
for widget in widgets:
context["templates"][widget["column"]].append(
widget["template"])
+16 -11
View File
@@ -1,26 +1,31 @@
# -*- coding: utf-8 -*-
"""Import related views."""
from __future__ import unicode_literals
from backports import csv
import io
from reversion import revisions as reversion
from django.contrib.auth.decorators import (
login_required, permission_required, user_passes_test
)
from django.urls import reverse
from django.db import transaction
from django.shortcuts import render
from django.urls import reverse
from django.utils import six
from django.utils.encoding import smart_text
from django.utils.translation import ugettext as _
from reversion import revisions as reversion
from modoboa.lib.exceptions import ModoboaException, Conflict
from ..forms import ImportIdentitiesForm, ImportDataForm
from modoboa.lib.exceptions import Conflict, ModoboaException
from .. import signals
from ..forms import ImportDataForm, ImportIdentitiesForm
if six.PY2:
from backports import csv
else:
import csv
@reversion.create_revision()
@@ -39,8 +44,8 @@ def importdata(request, formclass=ImportDataForm):
if form.is_valid():
try:
infile = io.TextIOWrapper(
request.FILES['sourcefile'].file, encoding="utf8")
reader = csv.reader(infile, delimiter=form.cleaned_data['sepchar'])
request.FILES["sourcefile"].file, encoding="utf8")
reader = csv.reader(infile, delimiter=form.cleaned_data["sepchar"])
except csv.Error as inst:
error = smart_text(inst)
else:
@@ -63,7 +68,7 @@ def importdata(request, formclass=ImportDataForm):
continue
raise Conflict(
_("Object already exists: %s"
% form.cleaned_data['sepchar'].join(row[:2]))
% form.cleaned_data["sepchar"].join(row[:2]))
)
cpt += 1
msg = _("%d objects imported successfully" % cpt)
+6 -6
View File
@@ -1,16 +1,16 @@
# -*- coding: utf-8 -*-
"""SimpleUsers views."""
from __future__ import unicode_literals
from reversion import revisions as reversion
from django.contrib.auth.decorators import login_required
from django.template.loader import render_to_string
from django.utils.translation import ugettext as _
from django.contrib.auth.decorators import login_required
from reversion import revisions as reversion
from modoboa.lib.web_utils import render_to_json_response
from ..forms import ForwardForm
from ..lib import needs_mailbox
from ..models import Alias
@@ -40,7 +40,7 @@ def forward(request, tplname="admin/forward.html"):
return render_to_json_response(_("Forward updated"))
return render_to_json_response(
{'form_errors': form.errors}, status=400
{"form_errors": form.errors}, status=400
)
form = ForwardForm()
+2
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
"""App config for core."""
from __future__ import unicode_literals
+12 -16
View File
@@ -1,21 +1,17 @@
# coding: utf-8
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django import forms
from django.conf import settings
from django.contrib.auth import password_validation
from django.utils.translation import ugettext as _, ugettext_lazy
from django.contrib.auth import password_validation
from modoboa.lib import fields as lib_fields
from modoboa.lib.cryptutils import random_key
from modoboa.lib.form_utils import (
YesNoField, SeparatorField, HorizontalRadioSelect
HorizontalRadioSelect, SeparatorField, YesNoField
)
from modoboa.parameters import forms as param_forms
from modoboa.parameters import tools as param_tools
from modoboa.parameters import forms as param_forms, tools as param_tools
from . import constants
@@ -40,8 +36,8 @@ class GeneralParametersForm(param_forms.AdminParametersForm):
authentication_type = forms.ChoiceField(
label=ugettext_lazy("Authentication type"),
choices=[('local', ugettext_lazy("Local")),
('ldap', "LDAP")],
choices=[("local", ugettext_lazy("Local")),
("ldap", "LDAP")],
initial="local",
help_text=ugettext_lazy("The backend used for authentication"),
widget=HorizontalRadioSelect()
@@ -115,16 +111,16 @@ class GeneralParametersForm(param_forms.AdminParametersForm):
ldap_auth_method = forms.ChoiceField(
label=ugettext_lazy("Authentication method"),
choices=[('searchbind', ugettext_lazy("Search and bind")),
('directbind', ugettext_lazy("Direct bind"))],
initial='searchbind',
choices=[("searchbind", ugettext_lazy("Search and bind")),
("directbind", ugettext_lazy("Direct bind"))],
initial="searchbind",
help_text=ugettext_lazy("Choose the authentication method to use"),
widget=forms.Select(attrs={"class": "form-control"})
)
ldap_bind_dn = forms.CharField(
label=ugettext_lazy("Bind DN"),
initial='',
initial="",
help_text=ugettext_lazy(
"The distinguished name to use when binding to the LDAP server. "
"Leave empty for an anonymous bind"
@@ -135,7 +131,7 @@ class GeneralParametersForm(param_forms.AdminParametersForm):
ldap_bind_password = forms.CharField(
label=ugettext_lazy("Bind password"),
initial='',
initial="",
help_text=ugettext_lazy(
"The password to use when binding to the LDAP server "
"(with 'Bind DN')"
@@ -344,7 +340,7 @@ class GeneralParametersForm(param_forms.AdminParametersForm):
def clean_ldap_user_dn_template(self):
tpl = self.cleaned_data["ldap_user_dn_template"]
try:
test = tpl % {"user": "toto"}
tpl % {"user": "toto"}
except (KeyError, ValueError):
raise forms.ValidationError(_("Invalid syntax"))
return tpl
+3 -1
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
"""Core config for admin."""
from __future__ import unicode_literals
@@ -30,7 +32,7 @@ class CoreConfig(AppConfig):
load_core_settings()
# Import these to force registration of checks and signals
from . import checks # noqa:F401
from . import checks # NOQA:F401
from . import handlers
signals.post_migrate.connect(handlers.create_local_config, sender=self)
+3 -1
View File
@@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
# Import these to force registration of checks
from . import settings_checks # noqa:F401
from . import settings_checks # NOQA:F401
-1
View File
@@ -6,7 +6,6 @@ from django.conf import settings
from django.core.checks import Warning, register
from django.utils.translation import ugettext as _
W001 = Warning(
_("You have USE_TZ set to False, this may result in issues during "
"transitions between summer/winter time (ie the same local time occuring "
+7 -7
View File
@@ -1,6 +1,6 @@
# coding: utf-8
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, print_function
from __future__ import print_function, unicode_literals
import argparse
import os
@@ -17,7 +17,7 @@ class Command(object):
A valid administrative command must inherit from this class.
"""
help = "No help available."
help = "No help available." # NOQA:A003
def __init__(self, commands, verbose=False):
self._commands = commands
@@ -90,10 +90,10 @@ def handle_command_line():
epilog="""Available commands:
%s
""" % "\n".join(["\t%s" % c for c in sorted(commands)]))
parser.add_argument('--verbose', action='store_true',
help='Activate verbose output')
parser.add_argument('command', type=str,
help='A valid command name')
parser.add_argument("--verbose", action="store_true",
help="Activate verbose output")
parser.add_argument("command", type=str,
help="A valid command name")
(args, remaining) = parser.parse_known_args()
if args.command not in commands:
+65 -69
View File
@@ -1,8 +1,8 @@
# coding: utf-8
# -*- coding: utf-8 -*-
"""A shortcut to deploy a fresh modoboa instance."""
from __future__ import unicode_literals, print_function
from __future__ import print_function, unicode_literals
import codecs
import getpass
@@ -10,26 +10,19 @@ import os
import shutil
import subprocess
import sys
from os.path import isfile
try:
import pip
except ImportError:
sys.stderr.write("Error: pip is required to install extensions.\n")
sys.exit(2)
import dj_database_url
import django
from django.core import management
from django.template import Context, Template
from django.utils.encoding import smart_str
import dj_database_url
from django.utils.six.moves import input
from modoboa.core.commands import Command
from modoboa.lib.api_client import ModoAPIClient
if sys.version_info < (3, 0, 0):
input = raw_input
from modoboa.lib.sysutils import exec_cmd
DBCONN_TPL = """
'{{ conn_name }}': {
@@ -51,47 +44,47 @@ class DeployCommand(Command):
"""The ``deploy`` command."""
help = (
help = ( # NOQA:A003
"Create a fresh django project (calling startproject)"
" and apply Modoboa specific settings."
)
def __init__(self, *args, **kwargs):
super(DeployCommand, self).__init__(*args, **kwargs)
self._parser.add_argument('name', type=str,
help='The name of your Modoboa instance')
self._parser.add_argument("name", type=str,
help="The name of your Modoboa instance")
self._parser.add_argument(
'--collectstatic', action='store_true', default=False,
help='Run django collectstatic command'
"--collectstatic", action="store_true", default=False,
help="Run django collectstatic command"
)
self._parser.add_argument(
'--dburl', type=str, nargs="+", default=None,
help='A database-url with a name')
"--dburl", type=str, nargs="+", default=None,
help="A database-url with a name")
self._parser.add_argument(
'--domain', type=str, default=None,
help='The domain under which you want to deploy modoboa')
"--domain", type=str, default=None,
help="The domain under which you want to deploy modoboa")
self._parser.add_argument(
'--lang', type=str, default="en-us",
"--lang", type=str, default="en-us",
help="Set the default language"
)
self._parser.add_argument(
'--timezone', type=str, default="UTC",
"--timezone", type=str, default="UTC",
help="Set the local timezone"
)
self._parser.add_argument(
'--devel', action='store_true', default=False,
help='Create a development instance'
"--devel", action="store_true", default=False,
help="Create a development instance"
)
self._parser.add_argument(
'--extensions', type=str, nargs='*',
"--extensions", type=str, nargs="*",
help="The list of extension to deploy"
)
self._parser.add_argument(
'--dont-install-extensions', action='store_true', default=False,
help='Do not install extensions using pip'
"--dont-install-extensions", action="store_true", default=False,
help="Do not install extensions using pip"
)
self._parser.add_argument(
'--admin-username', default='admin',
"--admin-username", default="admin",
help="Username of the initial super administrator"
)
@@ -101,7 +94,7 @@ class DeployCommand(Command):
:param name: the command name
:param cwd: the directory where the command must be executed
"""
cmd = [sys.executable, 'manage.py', name]
cmd = [sys.executable, "manage.py", name]
cmd.extend(args)
if not self._verbose:
p = subprocess.Popen(
@@ -120,7 +113,7 @@ class DeployCommand(Command):
)
print("%s failed, check your configuration" % cmd, file=sys.stderr)
def ask_db_info(self, name='default'):
def ask_db_info(self, name="default"):
"""Prompt the user for database information
Gather all information required to create a new database
@@ -130,33 +123,33 @@ class DeployCommand(Command):
"""
print("Configuring database connection: %s" % name)
info = {
'conn_name': name,
'ENGINE': input(
'Database type (mysql, postgres or sqlite3): ')
"conn_name": name,
"ENGINE": input(
"Database type (mysql, postgres or sqlite3): ")
}
if info['ENGINE'] not in ['mysql', 'postgres', 'sqlite3']:
raise RuntimeError('Unsupported database engine')
if info["ENGINE"] not in ["mysql", "postgres", "sqlite3"]:
raise RuntimeError("Unsupported database engine")
if info['ENGINE'] == 'sqlite3':
info['ENGINE'] = 'django.db.backends.sqlite3'
info['NAME'] = '%s.db' % name
if info["ENGINE"] == "sqlite3":
info["ENGINE"] = "django.db.backends.sqlite3"
info["NAME"] = "%s.db" % name
return info
if info['ENGINE'] == 'postgres':
info['ENGINE'] = 'django.db.backends.postgresql_psycopg2'
if info["ENGINE"] == "postgres":
info["ENGINE"] = "django.db.backends.postgresql_psycopg2"
default_port = 5432
else:
info['ENGINE'] = 'django.db.backends.mysql'
info["ENGINE"] = "django.db.backends.mysql"
default_port = 3306
info['HOST'] = input("Database host (default: 'localhost'): ")
info['PORT'] = input(
info["HOST"] = input("Database host (default: 'localhost'): ")
info["PORT"] = input(
"Database port (default: '%s'): " % default_port)
# leave port setting empty, if default value is supplied and
# leave it to django
if info['PORT'] == default_port:
info['PORT'] = ''
info['NAME'] = input('Database name: ')
info['USER'] = input('Username: ')
info['PASSWORD'] = getpass.getpass('Password: ')
if info["PORT"] == default_port:
info["PORT"] = ""
info["NAME"] = input("Database name: ")
info["USER"] = input("Username: ")
info["PASSWORD"] = getpass.getpass("Password: ")
return info
def _get_extension_list(self):
@@ -188,9 +181,9 @@ class DeployCommand(Command):
def handle(self, parsed_args):
django.setup()
management.call_command(
'startproject', parsed_args.name, verbosity=False
"startproject", parsed_args.name, verbosity=False
)
path = "%(name)s/%(name)s" % {'name': parsed_args.name}
path = "%(name)s/%(name)s" % {"name": parsed_args.name}
sys.path.append(parsed_args.name)
conn_tpl = Template(DBCONN_TPL)
@@ -206,7 +199,7 @@ class DeployCommand(Command):
info = self.ask_db_info(conn_name)
# If we set this earlier, our fallback method will never
# be triggered
info['conn_name'] = conn_name
info["conn_name"] = conn_name
connections[conn_name] = conn_tpl.render(Context(info))
else:
connections["default"] = conn_tpl.render(
@@ -216,7 +209,7 @@ class DeployCommand(Command):
allowed_host = parsed_args.domain
else:
allowed_host = input(
'What will be the hostname used to access Modoboa? ')
"What will be the hostname used to access Modoboa? ")
if not allowed_host:
allowed_host = "localhost"
extra_settings = []
@@ -227,9 +220,12 @@ class DeployCommand(Command):
extensions = [(extension, extension.replace("-", "_"))
for extension in extensions]
if not parsed_args.dont_install_extensions:
pip_args = (
["install"] + [extension[0] for extension in extensions])
pip.main(pip_args)
cmd = (
sys.executable +
" -m pip install " +
" ".join([extension[0] for extension in extensions])
)
exec_cmd(cmd, capture_output=False)
extra_settings = self.find_extra_settings(extensions)
extensions = [extension[1] for extension in extensions]
@@ -238,20 +234,20 @@ class DeployCommand(Command):
)
mod = __import__(
parsed_args.name, globals(), locals(), [smart_str('settings')]
parsed_args.name, globals(), locals(), [smart_str("settings")]
)
tpl = self._render_template(
"%s/settings.py.tpl" % self._templates_dir, {
'db_connections': connections,
'secret_key': mod.settings.SECRET_KEY,
'name': parsed_args.name,
'allowed_host': allowed_host,
'lang': parsed_args.lang,
'timezone': parsed_args.timezone,
'bower_components_dir': bower_components_dir,
'devmode': parsed_args.devel,
'extensions': extensions,
'extra_settings': extra_settings
"db_connections": connections,
"secret_key": mod.settings.SECRET_KEY,
"name": parsed_args.name,
"allowed_host": allowed_host,
"lang": parsed_args.lang,
"timezone": parsed_args.timezone,
"bower_components_dir": bower_components_dir,
"devmode": parsed_args.devel,
"extensions": extensions,
"extra_settings": extra_settings
}
)
with codecs.open("%s/settings.py" % path, "w", "utf-8") as fp:
@@ -264,7 +260,7 @@ class DeployCommand(Command):
if isfile("%s/settings.pyc" % path):
os.unlink("%s/settings.pyc" % path)
self._exec_django_command(
"migrate", parsed_args.name, '--noinput'
"migrate", parsed_args.name, "--noinput"
)
self._exec_django_command(
"load_initial_data", parsed_args.name,
@@ -272,7 +268,7 @@ class DeployCommand(Command):
)
if parsed_args.collectstatic:
self._exec_django_command(
"collectstatic", parsed_args.name, '--noinput'
"collectstatic", parsed_args.name, "--noinput"
)
self._exec_django_command(
"set_default_site", parsed_args.name, allowed_host
+5 -5
View File
@@ -1,6 +1,6 @@
# coding: utf-8
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, print_function
from __future__ import print_function, unicode_literals
import sys
@@ -9,12 +9,12 @@ from . import Command
class HelpCommand(Command):
help = "Display the help message associated to a specific command"
help = "Display the help message associated to a specific command" # NOQA:A003
def __init__(self, *args, **kwargs):
super(HelpCommand, self).__init__(*args, **kwargs)
self._parser.add_argument('name', type=str,
help='A command name')
self._parser.add_argument("name", type=str,
help="A command name")
def handle(self, parsed_args):
if parsed_args.name not in self._commands:
+1 -1
View File
@@ -1,4 +1,4 @@
# coding: utf-8
# -*- coding: utf-8 -*-
"""Core application constants."""
+2
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
"""Custom context processors."""
from __future__ import unicode_literals
+2
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
# Development settings
from __future__ import unicode_literals
+4 -2
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
"""Extension management."""
from __future__ import unicode_literals
@@ -60,7 +62,7 @@ class ExtensionsPool(object):
:param ext: a class inheriting from ``Extension``
:param show: list the extension or not
"""
self.extensions[ext.name] = dict(cls=ext, show=show)
self.extensions[ext.name] = {"cls": ext, "show": show}
def get_extension(self, name):
"""Retrieve the current instance of an extension."""
@@ -79,7 +81,7 @@ class ExtensionsPool(object):
def load_extension(self, name):
"""Load a registered extension."""
__import__(name, locals(), globals(), [smart_str('modo_extension')])
__import__(name, locals(), globals(), [smart_str("modo_extension")])
extinstance = self.get_extension(name)
if extinstance is None:
return None
+2
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
"""Factories for core application."""
from __future__ import unicode_literals
+4 -6
View File
@@ -1,18 +1,16 @@
# coding: utf-8
# -*- coding: utf-8 -*-
"""Core forms."""
from __future__ import unicode_literals
from django import forms
from django.contrib.auth import (
forms as auth_forms, get_user_model, password_validation
)
from django.db.models import Q
from django.utils.translation import ugettext as _, ugettext_lazy
from django.contrib.auth import forms as auth_forms
from django.contrib.auth import get_user_model
from django.contrib.auth import password_validation
from modoboa.core.models import User
from modoboa.parameters import tools as param_tools
+8 -11
View File
@@ -1,27 +1,24 @@
# -*- coding: utf-8 -*-
"""Modoboa core signal handlers."""
from __future__ import unicode_literals
import logging
from reversion import revisions as reversion
from reversion.models import Version
from django.contrib.sites import models as sites_models
from django.db.models import signals
from django.dispatch import receiver
from django.urls import reverse
from django.utils.encoding import smart_text
from django.utils.translation import ugettext as _
from django.contrib.sites import models as sites_models
from reversion import revisions as reversion
from reversion.models import Version
from modoboa.lib import exceptions
from modoboa.lib import permissions
from modoboa.lib import exceptions, permissions
from modoboa.lib.signals import get_request
from . import models
from . import signals as core_signals
from . import utils
from . import models, signals as core_signals, utils
@receiver(reversion.post_revision_commit)
+2
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import logging
-1
View File
@@ -1 +0,0 @@
from __future__ import unicode_literals
@@ -1 +0,0 @@
from __future__ import unicode_literals
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
"""Inactive accounts cleanup tool."""
from __future__ import print_function, unicode_literals
@@ -9,14 +11,13 @@ from django.core.management.base import BaseCommand
from django.utils import timezone
from modoboa.parameters import tools as param_tools
from ... import models
class Command(BaseCommand):
"""Management command to clean inactive accounts."""
help = "Inactive accounts cleanup"
help = "Inactive accounts cleanup" # NOQA:A003
def add_arguments(self, parser):
"""Add extra arguments to command line."""
@@ -1,4 +1,4 @@
# coding: utf-8
# -*- coding: utf-8 -*-
from __future__ import print_function, unicode_literals
@@ -15,7 +15,7 @@ from modoboa.parameters import tools as param_tools
class Command(BaseCommand):
"""Command class."""
help = "Log table cleanup"
help = "Log table cleanup" # NOQA:A003
def add_arguments(self, parser):
"""Add extra arguments to command line."""
@@ -33,9 +33,9 @@ class Command(BaseCommand):
def handle(self, *args, **options):
if options["debug"]:
l = logging.getLogger("django.db.backends")
l.setLevel(logging.DEBUG)
l.addHandler(logging.StreamHandler())
log = logging.getLogger("django.db.backends")
log.setLevel(logging.DEBUG)
log.addHandler(logging.StreamHandler())
self.verbose = options["verbose"]
log_maximum_age = param_tools.get_global_parameter("log_maximum_age")

Some files were not shown because too many files have changed in this diff Show More