diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..f93002ab0 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.gitignore b/.gitignore index 7b725b7a5..429e9f678 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ DEV .ropeproject modoboa.iml .tox +.eggs/ modoboa_test .coverage build/ diff --git a/.travis.yml b/.travis.yml index f4cc7f849..887e7d961 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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: diff --git a/README.rst b/README.rst index ec5c774a8..bd6bdde52 100644 --- a/README.rst +++ b/README.rst @@ -30,11 +30,11 @@ Main features * Reputation protection: `DNSBL `_ checks, `DMARC `_ `reports `_ and more * `Amavis `_ `frontend `_ * `Webmail `_ +* `Calendar `_ * `Address book `_ * `Per-user Sieve filters `_ * `Autoreply messages for Postfix `_ * `Graphical statistics about email traffic `_ -* `Radicale `_ `management frontend `_ ************* Documentation diff --git a/bin/modoboa-admin.py b/bin/modoboa-admin.py index a637e52c6..78de64d39 100644 --- a/bin/modoboa-admin.py +++ b/bin/modoboa-admin.py @@ -7,4 +7,3 @@ from modoboa.core.commands import handle_command_line if __name__ == "__main__": handle_command_line() - diff --git a/doc/conf.py b/doc/conf.py index 37b46b5e5..4f11df49a 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -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 # " v 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 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) } diff --git a/doc/configuration.rst b/doc/configuration.rst index 628261179..996c52559 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -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 `_ +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 *********************** diff --git a/doc/manual_installation/modoboa.rst b/doc/manual_installation/modoboa.rst index 5387a851b..9f37e8580 100644 --- a/doc/manual_installation/modoboa.rst +++ b/doc/manual_installation/modoboa.rst @@ -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: diff --git a/doc/manual_installation/webserver.rst b/doc/manual_installation/webserver.rst index a95fef147..8f528fb88 100644 --- a/doc/manual_installation/webserver.rst +++ b/doc/manual_installation/webserver.rst @@ -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 `_. diff --git a/doc/upgrade.rst b/doc/upgrade.rst index 426b63be8..8c21065ae 100644 --- a/doc/upgrade.rst +++ b/doc/upgrade.rst @@ -185,6 +185,12 @@ Replace ```` and ```` by your values. If ``transport_maps`` contains ``sql-relaydomains-transport.cf``, remove it. +.. warning:: + + If you make use of postfix's `proxymap server + `_, you must also update + the ``proxy_read_maps`` setting. + Reload postfix. Add the following cron job in order to generate DKIM keys:: diff --git a/modoboa/__init__.py b/modoboa/__init__.py index 5d2abec9b..ad361c484 100644 --- a/modoboa/__init__.py +++ b/modoboa/__init__.py @@ -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 diff --git a/modoboa/admin/__init__.py b/modoboa/admin/__init__.py index 5882e6d25..80f7ae2d3 100644 --- a/modoboa/admin/__init__.py +++ b/modoboa/admin/__init__.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + from __future__ import unicode_literals default_app_config = "modoboa.admin.apps.AdminConfig" diff --git a/modoboa/admin/api.py b/modoboa/admin/api.py index a0e91e439..fed9bea69 100644 --- a/modoboa/admin/api.py +++ b/modoboa/admin/api.py @@ -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") diff --git a/modoboa/admin/app_settings.py b/modoboa/admin/app_settings.py index a72dd7fff..cd1afa3e7 100644 --- a/modoboa/admin/app_settings.py +++ b/modoboa/admin/app_settings.py @@ -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 diff --git a/modoboa/admin/apps.py b/modoboa/admin/apps.py index f799fa177..77ba62271 100644 --- a/modoboa/admin/apps.py +++ b/modoboa/admin/apps.py @@ -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 diff --git a/modoboa/admin/constants.py b/modoboa/admin/constants.py index 372f1ef20..e6f1c4e40 100644 --- a/modoboa/admin/constants.py +++ b/modoboa/admin/constants.py @@ -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", diff --git a/modoboa/admin/factories.py b/modoboa/admin/factories.py index b62ba877a..e0d5780c8 100644 --- a/modoboa/admin/factories.py +++ b/modoboa/admin/factories.py @@ -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) diff --git a/modoboa/admin/forms/__init__.py b/modoboa/admin/forms/__init__.py index b7811a0d9..bb1ded79a 100644 --- a/modoboa/admin/forms/__init__.py +++ b/modoboa/admin/forms/__init__.py @@ -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" ] diff --git a/modoboa/admin/forms/account.py b/modoboa/admin/forms/account.py index 1622a8f62..0fa0ab3c7 100644 --- a/modoboa/admin/forms/account.py +++ b/modoboa/admin/forms/account.py @@ -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): diff --git a/modoboa/admin/forms/alias.py b/modoboa/admin/forms/alias.py index 12b306442..09ee606e4 100644 --- a/modoboa/admin/forms/alias.py +++ b/modoboa/admin/forms/alias.py @@ -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): diff --git a/modoboa/admin/forms/domain.py b/modoboa/admin/forms/domain.py index ddd4bf070..5cc3f4a9d 100644 --- a/modoboa/admin/forms/domain.py +++ b/modoboa/admin/forms/domain.py @@ -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)) diff --git a/modoboa/admin/forms/export.py b/modoboa/admin/forms/export.py index 25e9402a3..1a295c599 100644 --- a/modoboa/admin/forms/export.py +++ b/modoboa/admin/forms/export.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + from __future__ import unicode_literals from django import forms diff --git a/modoboa/admin/forms/forward.py b/modoboa/admin/forms/forward.py index ddccba0f4..b270fb32d 100644 --- a/modoboa/admin/forms/forward.py +++ b/modoboa/admin/forms/forward.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + """Forms related to forwards management.""" from __future__ import unicode_literals diff --git a/modoboa/admin/forms/import_.py b/modoboa/admin/forms/import_.py index 790260919..db50913ff 100644 --- a/modoboa/admin/forms/import_.py +++ b/modoboa/admin/forms/import_.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + """Forms related to import operations.""" from __future__ import unicode_literals diff --git a/modoboa/admin/handlers.py b/modoboa/admin/handlers.py index a91a12667..14b499eb8 100644 --- a/modoboa/admin/handlers.py +++ b/modoboa/admin/handlers.py @@ -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: diff --git a/modoboa/admin/lib.py b/modoboa/admin/lib.py index e85ca33cc..2b1819bed 100644 --- a/modoboa/admin/lib.py +++ b/modoboa/admin/lib.py @@ -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 diff --git a/modoboa/admin/management/__init__.py b/modoboa/admin/management/__init__.py index baffc4882..e69de29bb 100644 --- a/modoboa/admin/management/__init__.py +++ b/modoboa/admin/management/__init__.py @@ -1 +0,0 @@ -from __future__ import unicode_literals diff --git a/modoboa/admin/management/commands/__init__.py b/modoboa/admin/management/commands/__init__.py index baffc4882..e69de29bb 100644 --- a/modoboa/admin/management/commands/__init__.py +++ b/modoboa/admin/management/commands/__init__.py @@ -1 +0,0 @@ -from __future__ import unicode_literals diff --git a/modoboa/admin/management/commands/handle_mailbox_operations.py b/modoboa/admin/management/commands/handle_mailbox_operations.py index 8888cb9eb..15d8f6e11 100644 --- a/modoboa/admin/management/commands/handle_mailbox_operations.py +++ b/modoboa/admin/management/commands/handle_mailbox_operations.py @@ -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 diff --git a/modoboa/admin/management/commands/modo.py b/modoboa/admin/management/commands/modo.py index d395c4c19..b4f3495f5 100644 --- a/modoboa/admin/management/commands/modo.py +++ b/modoboa/admin/management/commands/modo.py @@ -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, diff --git a/modoboa/admin/management/commands/subcommands/__init__.py b/modoboa/admin/management/commands/subcommands/__init__.py index baffc4882..e69de29bb 100644 --- a/modoboa/admin/management/commands/subcommands/__init__.py +++ b/modoboa/admin/management/commands/subcommands/__init__.py @@ -1 +0,0 @@ -from __future__ import unicode_literals diff --git a/modoboa/admin/management/commands/subcommands/_export.py b/modoboa/admin/management/commands/subcommands/_export.py index bad0e0afd..15ff8cf2a 100644 --- a/modoboa/admin/management/commands/subcommands/_export.py +++ b/modoboa/admin/management/commands/subcommands/_export.py @@ -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"]))() diff --git a/modoboa/admin/management/commands/subcommands/_import.py b/modoboa/admin/management/commands/subcommands/_import.py index 1c9c16d89..a202fe22b 100644 --- a/modoboa/admin/management/commands/subcommands/_import.py +++ b/modoboa/admin/management/commands/subcommands/_import.py @@ -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 + ) diff --git a/modoboa/admin/management/commands/subcommands/_manage_dkim_keys.py b/modoboa/admin/management/commands/subcommands/_manage_dkim_keys.py index bae55519c..c7002c2c0 100644 --- a/modoboa/admin/management/commands/subcommands/_manage_dkim_keys.py +++ b/modoboa/admin/management/commands/subcommands/_manage_dkim_keys.py @@ -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) diff --git a/modoboa/admin/management/commands/subcommands/_mx.py b/modoboa/admin/management/commands/subcommands/_mx.py index b7ce24fd1..8f0db645e 100644 --- a/modoboa/admin/management/commands/subcommands/_mx.py +++ b/modoboa/admin/management/commands/subcommands/_mx.py @@ -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): diff --git a/modoboa/admin/management/commands/subcommands/_repair.py b/modoboa/admin/management/commands/subcommands/_repair.py index 1406b683d..26bfb42f4 100644 --- a/modoboa/admin/management/commands/subcommands/_repair.py +++ b/modoboa/admin/management/commands/subcommands/_repair.py @@ -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: diff --git a/modoboa/admin/models/__init__.py b/modoboa/admin/models/__init__.py index ead319150..068fc45e9 100644 --- a/modoboa/admin/models/__init__.py +++ b/modoboa/admin/models/__init__.py @@ -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", diff --git a/modoboa/admin/models/alias.py b/modoboa/admin/models/alias.py index 225ed9e78..3056d6155 100644 --- a/modoboa/admin/models/alias.py +++ b/modoboa/admin/models/alias.py @@ -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( diff --git a/modoboa/admin/models/base.py b/modoboa/admin/models/base.py index 9ab6fc6c8..abe182180 100644 --- a/modoboa/admin/models/base.py +++ b/modoboa/admin/models/base.py @@ -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 diff --git a/modoboa/admin/models/domain.py b/modoboa/admin/models/domain.py index fa0337a3a..60ee94427 100644 --- a/modoboa/admin/models/domain.py +++ b/modoboa/admin/models/domain.py @@ -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): diff --git a/modoboa/admin/models/domain_alias.py b/modoboa/admin/models/domain_alias.py index 51ccfba69..3fec21047 100644 --- a/modoboa/admin/models/domain_alias.py +++ b/modoboa/admin/models/domain_alias.py @@ -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) diff --git a/modoboa/admin/models/mailbox.py b/modoboa/admin/models/mailbox.py index d32005959..610157536 100644 --- a/modoboa/admin/models/mailbox.py +++ b/modoboa/admin/models/mailbox.py @@ -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 diff --git a/modoboa/admin/models/mxrecord.py b/modoboa/admin/models/mxrecord.py index 4a4f71e5b..3d2cf9de8 100644 --- a/modoboa/admin/models/mxrecord.py +++ b/modoboa/admin/models/mxrecord.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + """MX records storage.""" from __future__ import unicode_literals diff --git a/modoboa/admin/postfix_maps.py b/modoboa/admin/postfix_maps.py index c904a7eeb..62b84748c 100644 --- a/modoboa/admin/postfix_maps.py +++ b/modoboa/admin/postfix_maps.py @@ -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 " diff --git a/modoboa/admin/serializers.py b/modoboa/admin/serializers.py index 4d28233da..d5e2ac88c 100644 --- a/modoboa/admin/serializers.py +++ b/modoboa/admin/serializers.py @@ -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 diff --git a/modoboa/admin/signals.py b/modoboa/admin/signals.py index facb879b9..b085d7807 100644 --- a/modoboa/admin/signals.py +++ b/modoboa/admin/signals.py @@ -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"]) diff --git a/modoboa/admin/static/admin/js/admin.js b/modoboa/admin/static/admin/js/admin.js index b198b9ab7..3db83f02f 100644 --- a/modoboa/admin/static/admin/js/admin.js +++ b/modoboa/admin/static/admin/js/admin.js @@ -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() { diff --git a/modoboa/admin/templates/admin/_domain_dkim_key.html b/modoboa/admin/templates/admin/_domain_dkim_key.html index d867ac509..ae71983e3 100644 --- a/modoboa/admin/templates/admin/_domain_dkim_key.html +++ b/modoboa/admin/templates/admin/_domain_dkim_key.html @@ -1,6 +1,6 @@ {% load i18n %} -