From d7b0a149a843af8bff10a8c912f890b73a56a7db Mon Sep 17 00:00:00 2001 From: Christophe Chauvet Date: Thu, 8 Jan 2026 20:13:23 +0100 Subject: [PATCH 1/8] docs: migrate documentation from sphinx to vitepress --- .github/workflows/documentation.yml | 34 + .github/workflows/modoboa.yml | 2 + .gitignore | 1 + doc-ng/configuration.md | 466 ++++++ doc-ng/contributing/getting_start.md | 141 ++ doc-ng/contributing/plugin_api.md | 193 +++ doc-ng/contributing/translation.md | 20 + doc-ng/contributors.md | 7 + doc-ng/index.md | 41 + doc-ng/installation/amavis.md | 272 ++++ doc-ng/installation/dovecot.md | 656 ++++++++ doc-ng/installation/installer.md | 54 + doc-ng/installation/manual.md | 13 + doc-ng/installation/modoboa.md | 317 ++++ doc-ng/installation/opendkim.md | 79 + doc-ng/installation/postfix.md | 103 ++ doc-ng/installation/radicale.md | 142 ++ doc-ng/installation/requirements.md | 26 + doc-ng/installation/upgrade.md | 2113 ++++++++++++++++++++++++ doc-ng/installation/webserver.md | 347 ++++ doc-ng/moving.md | 169 ++ doc-ng/package.json | 11 + doc-ng/pnpm-lock.yaml | 1837 +++++++++++++++++++++ doc-ng/public/admin_dashboard.png | Bin 0 -> 94291 bytes doc-ng/public/api_access_form.png | Bin 0 -> 37115 bytes doc-ng/public/modoboa_logo.png | Bin 0 -> 5989 bytes doc-ng/public/user_webmail.png | Bin 0 -> 45446 bytes doc-ng/rebuild_venv.md | 70 + doc-ng/rest_api.md | 35 + doc-ng/upgrade.md | 2241 ++++++++++++++++++++++++++ 30 files changed, 9390 insertions(+) create mode 100644 .github/workflows/documentation.yml create mode 100644 doc-ng/configuration.md create mode 100644 doc-ng/contributing/getting_start.md create mode 100644 doc-ng/contributing/plugin_api.md create mode 100644 doc-ng/contributing/translation.md create mode 100644 doc-ng/contributors.md create mode 100644 doc-ng/index.md create mode 100644 doc-ng/installation/amavis.md create mode 100644 doc-ng/installation/dovecot.md create mode 100644 doc-ng/installation/installer.md create mode 100644 doc-ng/installation/manual.md create mode 100644 doc-ng/installation/modoboa.md create mode 100644 doc-ng/installation/opendkim.md create mode 100644 doc-ng/installation/postfix.md create mode 100644 doc-ng/installation/radicale.md create mode 100644 doc-ng/installation/requirements.md create mode 100644 doc-ng/installation/upgrade.md create mode 100644 doc-ng/installation/webserver.md create mode 100644 doc-ng/moving.md create mode 100644 doc-ng/package.json create mode 100644 doc-ng/pnpm-lock.yaml create mode 100644 doc-ng/public/admin_dashboard.png create mode 100644 doc-ng/public/api_access_form.png create mode 100644 doc-ng/public/modoboa_logo.png create mode 100644 doc-ng/public/user_webmail.png create mode 100644 doc-ng/rebuild_venv.md create mode 100644 doc-ng/rest_api.md create mode 100644 doc-ng/upgrade.md diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 000000000..3b35c5559 --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,34 @@ +on: + push: + branches: [ master ] + paths-ignore: + - 'modoboa/**' + - 'frontend/**' + - 'doc/**' + pull_request: + branches: [ master ] + paths-ignore: + - 'modoboa/**' + - 'frontend/**' + - 'doc/**' + +jobs: + cache-and-install: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + version: 10 + cache: true + + - name: Install dependencies + run: pnpm install + + - name: Build documentation + run: pnpm run build + \ No newline at end of file diff --git a/.github/workflows/modoboa.yml b/.github/workflows/modoboa.yml index 1016f6aed..0229ee8fc 100644 --- a/.github/workflows/modoboa.yml +++ b/.github/workflows/modoboa.yml @@ -10,12 +10,14 @@ on: - 'modoboa/**.po' - 'frontend/**' - 'doc/**' + - 'doc-ng/**' pull_request: branches: [ master ] paths-ignore: - 'modoboa/**.po' - 'frontend/**' - 'doc/**' + - 'doc-ng/**' release: branches: [ master ] types: [ published ] diff --git a/.gitignore b/.gitignore index 4488700e0..0708bfd31 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ node_modules /frontend/webpack-stats.json /frontend/src/translations.json /frontend/.__mf__temp/ +.vitepress/ # TOX stuff test_project/modoboa.db diff --git a/doc-ng/configuration.md b/doc-ng/configuration.md new file mode 100644 index 000000000..0e4530983 --- /dev/null +++ b/doc-ng/configuration.md @@ -0,0 +1,466 @@ +# Configuration + +## Online parameters + +Modoboa provides online panels to modify internal parameters. There are +two available levels: + +- **Application level**: global parameters, define how the application + behaves. Available at `Modoboa > Parameters` +- **User level**: per user customization. Available at `User > Settings > + Preferences` +Regardless level, parameters are displayed using tabs, each tab +corresponding to one application. + +## General parameters {#admin-params} + +The *admin* application exposes several parameters, they are presented +below: + + +| Name | Tab | Description | Default Value | +| :-: | :-: | :-- | :-: | +| Authentication type | General | The backend used for authentication | Local | +| Default password scheme | General passwords | Scheme used to crypt mailbox | sha512crypt | +| Rounds | General | Number of rounds (only used by [sha256crypt] and [sha512crypt]). Must be between 1000 and 999999999, inclusive. | 70000 | +| Secret key | General | A key used to encrypt users password in sessions | random value | +| Sender address | General | Email address used to send notifications. | | +| Enable communication | General | Enable communication with Modoboa public API | yes | +| Check new versions | General | Automatically checks if a newer version is available | yes | +| Send statistics | General | Send statistics to Modoboa public API (counters and used extensions) | yes | +| Top notifications check interval | General | Interval between two top notification checks (in seconds) | 30 | +| Maximum log record age | General | The maximum age in days of a log record | 365 | +| Items per page | General | Number of displayed items per page | 30 | +| Default top redirection | General | The default redirection used when no application is specified | userprefs | +| Enable MX checks | Admin | Check that every domain has a valid MX record | yes | +| Valid MXs | Admin | A list of IP or network address every MX should match. A warning will be sent if a record does not respect this it. | | +| Enable DNSBL checks | Admin | Check every domain against major DNSBL providers | yes | +| DKIM keys storage directory | Admin | Path to a directory where DKIM generated keys will be stored | | +| Default DKIM key length | Admin | The default size (in bits) for new keys | 2048 | +| Handle mailboxes on filesystem | Admin | Rename or remove mailboxes on the filesystem when they get renamed or removed within Modoboa | no | +| Mailboxes owner | Admin | The UNIX account who owns mailboxes on the filesystem | vmail | +| Default domain quota | Admin | Default quota (in MB) applied to freshly created domains with no value specified. A value of 0 means no quota. | 0 | +| Automatic account removal | Admin | When a mailbox is removed, also remove the associated account | no | +| Automatic domain/mailbox creation | Admin | Create a domain and a mailbox when an account is automatically created | yes | + +::: warning +If Dovecot is not running on the same host than Modoboa, you will have +to define which password schemes are supported. + +To do so, open the `settings.py` file and add a `DOVECOT_SUPPORTED_SCHEMES` +variable with the output of the command: +```shell +$ doveadm pw -l +``` +::: + +::: info + +If you are not familiar with virtual domain hosting, you should take a look at +[postfix's documentation](http://www.postfix.org/VIRTUAL_README.html). + +This [How to](https://help.ubuntu.com/community/PostfixVirtualMailBoxClamSmtpHowto) +also contains useful information. +::: + +::: tip +A random secret key will be generated each time the *Parameters* page is +refreshed and until you save parameters at least once. +::: + +::: info +Specific LDAP parameters are also available, see `LDAP +authentication `. +::: + +### Media files + +Modoboa uses a specific directory to upload files (ie. when the webmail +is in use) or to create ones (ex: graphical statistics). + +This directory is named `media` and is located inside modoboa's +installation directory (called `modoboa_site` in this documentation). + +To work properly, the system user which runs modoboa (`www-data`, +`apache`, whatever) must have write access to this directory. + +### Customization + +## Custom logo + +You have the possibility to use a custom logo instead of the default one +on the login page and inside generated PDF documents. + +To do so, open the `settings.py` file and add a `MODOBOA_CUSTOM_LOGO` variable. + +This variable must contain the relative URL of your logo under `MEDIA_URL`. + +For example: + +``` python +MODOBOA_CUSTOM_LOGO = os.path.join(MEDIA_URL, "custom_logo.png") +``` + +Then copy your logo file into the directory indicated by `MEDIA_ROOT`. + +You can also customize the logo(s) used in the frontend application +(left menu and creation forms). + +To do so, copy the corresponding files inside the frontend directory +directly (defaults to `/srv/modoboa/instance/frontend`). + +Then, edit the `config.json` file located in this very same directory and add the following variables: + +```json +{ + "MENU_LOGO_PATH": "/cusrtom_logo.png", // [!code focus] + "CREATION_FORM_LOGO_PATH": "/custom_logo2.png" // [!code focus] +} +``` + +### Host configuration + +::: tip +This section is only relevant when Modoboa handles mailboxes renaming +and removal from the filesystem, +which requires that Dovecot is installed and running on this host. + +If it is installed at a non-standard directory, +paths to its binaries can be set in the `settings.py` file +with the `DOVECOT_LOOKUP_PATH` and `DOVEADM_LOOKUP_PATH` variables. +::: + +To manipulate mailboxes on the filesystem, you must allow the user who +runs Modoboa to execute commands as the user who owns mailboxes. + +To do so, edit the `/etc/sudoers` file and add the following inside: + +```txt + ALL=() NOPASSWD: ALL +``` + +Replace values between `<>` by the ones you use. + +### Time zone and language {#timezone_lang} + +Modoboa is available in many languages. + +To specify the default language to use, edit the `settings.py` file +and modify the `LANGUAGE_CODE` variable: + +```python +LANGUAGE_CODE = 'fr' # or 'en' for english, etc. +``` + +::: tip +Each user has the possibility to define the language he prefers. +::: + +In the same configuration file, specify the timezone to use +by modifying the `TIME_ZONE` variable. For example: + +```python +TIME_ZONE = 'Europe/Paris' +``` + +### Sessions management + +Modoboa uses [Django's session framework](https://docs.djangoproject.com/en/dev/topics/http/sessions/?from=olddocs) +to store per-user information. + +Few parameters need to be set in the `settings.py` configuration file to make Modoboa behave as expected: + +```python +SESSION_EXPIRE_AT_BROWSER_CLOSE = False # Default value +``` + +This parameter is optional but you must ensure it is set to `False` (the default value). + +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 `settings.py` file and add +['address': \'/dev/log\'] to the logging section: + +```python +'syslog-auth': { + 'class': 'logging.handlers.SysLogHandler', + 'facility': SysLogHandler.LOG_AUTH, + 'address': '/dev/log', + 'formatter': 'syslog' +}, +``` + +### External authentication + +## LDAP {#ldap_auth} + +Modoboa supports external LDAP authentication using the following extra components: + +* [Python LDAP client](http://www.python-ldap.org/) +* [Django LDAP authentication backend](http://pypi.python.org/pypi/django-auth-ldap) + +If you want to use this feature, you must first install those components: + +```shell +$ pip install python-ldap django-auth-ldap +``` + +Then, all you have to do is to modify the `settings.py` file. + +Add a new authentication backend to the [AUTHENTICATION_BACKENDS] variable, like this: + +```python +AUTHENTICATION_BACKENDS = ( + 'modoboa.lib.authbackends.LDAPBackend', + # 'modoboa.lib.authbackends.LDAPSecondaryBackend', # Useful for a fallback mechanism + 'django.contrib.auth.backends.ModelBackend', +) +``` + +Finally, go to **Modoboa > Parameters > General** and set *Authentication type* to LDAP. + +From there, new parameters will appear to let you configure the way +Modoboa should connect to your LDAP server. + +They are described just below: + +| Name | Description | Default Value | +| :-: | :-- | :-: | +| Server address | The IP address of the DNS name of the LDAP server |localhost | +| Server port | The TCP port number used by the LDAP server | 389 | +| Use a secure connection | Use an SSL/TLS connection to access the LDAP server | no | +| Authentication method | Choose the authentication method to use | Direct bind | +| User DN template (direct bind mode)| The template used to construct a user\'s DN. It should contain one placeholder (ie.`%(user)s`) | | +| Bind BN | The distinguished name to use when binding to the LDAP server. Leave empty for an anonymous bind | | +| Bind password | The password to use when binding to the LDAP server (with \'Bind DN\') | | +| Search base | The distinguished name of the search base | |  +| Search filter | An optional filter string (e.g. \'(objectClass=person)\'). In order to be valid, it must be enclosed in parentheses. | (mail=%(user)s) | +| Password attribute | The attribute used to store user passwords | userPassword | +| Active Directory | Tell if the LDAP server is an no Active Directory one | | +| Administrator groups | Members of those LDAP Posix groups will be created ad domain administrators. Use ';' characters to separate groups. | |  +| Group type | The type of group used by your PosixGroup LDAP directory. | | +| Groups search base | The distinguished name of the search base used to find groups | | +| Domain/mailbox creation | Automatically create a domain and a mailbox when a new user is created just after the first successful authentication. You will generally want to disable this feature when the relay domains extension is in use | yes | + +::: tip +If you need additional parameters, you will find a detailed +documentation [here](http://packages.python.org/django-auth-ldap/). +::: + +Once the authentication is properly configured, the users defined in +your `LDAP` directory will be able to connect to Modoboa, the associated +domain and mailboxes will be automatically created if needed. + +The first time a user connects to Modoboa, a local account is created if +the `LDAP username` is a valid email address. By default, this account +belongs to the `SimpleUsers` group and it has a mailbox. + +To automatically create domain administrators, you can use the +**Administrator groups** setting. If a LDAP user belongs to one the +listed groups, its local account will belong to the `DomainAdmins` +group. + +In this case, the username is not necessarily an email address. + +Users will also be able to update their `LDAP password` directly from +Modoboa. + +::: warning +Modoboa doesn't provide any synchronization mechanism once a user is +registered into the database. Any modification done from the directory +to a user account will not be reported to Modoboa (an email address +change for example). Currently, the only solution is to manually delete +the Modoboa record, it will be recreated on the next user login. +::: + +## SMTP {#smtp_auth} + +It is possible to use an existing SMTP server as an authentication +source. + +To enable this feature, edit the `settings.py` file and change the following setting: + +``` python +AUTHENTICATION_BACKENDS = ( + 'modoboa.lib.authbackends.SMTPBackend', + 'django.contrib.auth.backends.ModelBackend', +) +``` + +SMTP server location can be customized using the following settings: + +``` python +AUTH_SMTP_SERVER_ADDRESS = 'localhost' +AUTH_SMTP_SERVER_PORT = 25 +AUTH_SMTP_SECURED_MODE = None # 'ssl' or 'starttls' are accepted +``` + +### LDAP synchronization {#ldap-sync} + +Modoboa can synchronize accounts with an LDAP directory (tested with +OpenLDAP) but this feature is not enabled by default. + +To activate it, add `modoboa.ldapsync` to `MODOBOA_APPS` in the `settings.py` file: + +```python +MODOBOA_APPS = ( + 'modoboa', + 'modoboa.core', + 'modoboa.lib', + 'modoboa.admin', + 'modoboa.transport', + 'modoboa.relaydomains', + 'modoboa.limits', + 'modoboa.parameters', + 'modoboa.dnstools', + 'modoboa.ldapsync', +) +``` + +and enable it from the admin panel. + +::: warning +Make sure to install additional `requirements ` +otherwise it won't work. +::: + +The following parameters are available and must be filled: + +| Name | Description | Default Value | +| :-: | :-- | :-: | +| Enable LDAP synchronization | Control LDAP synchronization state | no | +| Bind DN | The DN of a user with write permission to create/update accounts | | +| Bind password | The associated password | |  +| Account DN template | The template used to build account DNs (must contain a %(user)s placeholder ) | + +## Database maintenance + +### Cleaning the logs table + +Modoboa logs administrator specific actions into the database. A +clean-up script is provided to automatically remove oldest records. The +maximum log record age can be configured through the online panel. + +To use it, you can setup a cron job to run every night: + +```txt +0 0 * * * /manage.py cleanlogs +# +# Or like this if you use a virtual environment: +# 0 0 * * * /manage.py cleanlogs +``` + +### Cleaning the session table + +Django does not provide automatic purging. Therefore, it's your job to +purge expired sessions on a regular basis. + +Django provides a sample clean-up script: + +* `django-admin.py clearsessions`. That script deletes any session in the + session table whose `expire_date` is in the past. + +For example, you could setup a cron job to run this script every night: + +```txt +0 0 * * * /manage.py clearsessions +# +# Or like this if you use a virtual environment: +# 0 0 * * * /manage.py clearsessions +``` + +### Cleaning inactive accounts {#inactive_accounts} + +Thanks to `lastlogin`{.interpreted-text role="ref"}, it is now possible +to monitor inactive accounts. An account is considered inactive if no +login has been recorded for the last 30 days (this value can be changed +through the admin panel). + +A management command is available to disable or delete inactive +accounts. + +For example, you could setup a cron job to run it every night: + +```txt +0 0 * * * /manage.py clean_inactive_accounts +# +# Or like this if you use a virtual environment: +# 0 0 * * * /manage.py clean_inactive_accounts +``` + +The default behaviour is to disable accounts. You can delete them using the `--delete` option. + +## DMARC reports + +::: warning +A set of tools to use DMARC through Modoboa. + +This feature is still in BETA stage, for now it only parses XML +aggregated reports and generate visual reports on a per-domain basis. +::: + +### Installation + +::: info +modoboa-installer can automatically set it up for you. +::: + +Make sure to install the following additional system package according +to your distribution: + +* Debian / Ubuntu: libmagic1 +* CentOS: file-devel + +### Integration with Postfix + +A management command is provided to automatically parse DMARC aggregated +reports (rua) and feed the database. + +The execution of this command can be automated with the definition of a +postfix service and a custom transport table. + +First, declare a new service in `/etc/postfix/master.cf`: + +```txt +dmarc-rua-parser unix - n n - - pipe + flags= user=vmail:vmail argv= /manage.py import_aggregated_report --pipe +``` + +Define a new transport table inside `/etc/postfix/main.cf`: + +```txt +transport_maps = + hash:/etc/postfix/dmarc_transport + # other transport maps... +``` + +Create a file called `/etc/postfix/dmarc_transport` with the following content: + +```txt + dmarc-rua-parser: +``` + +::: warning +You must not declare this email address as an identity (user account or +alias), else DMARC reports will be directed to your mailbox and won\'t +be parsed. +::: + +Hash the file using the following command: + +```shell +$ postmap /etc/postfix/dmarc_transport +``` + +Finally, reload postfix: + +```shell +$ service postfix reload +``` \ No newline at end of file diff --git a/doc-ng/contributing/getting_start.md b/doc-ng/contributing/getting_start.md new file mode 100644 index 000000000..bf8da39a1 --- /dev/null +++ b/doc-ng/contributing/getting_start.md @@ -0,0 +1,141 @@ +# Getting started + +You would like to work on Modoboa but you don\'t know where to start? +You're at the right place! Browse this page to learn useful tips. + +## With Docker + +A docker image is available for developers. + +To use it, you must install docker and docker-compose + +* [docker](https://docs.docker.com/install/) and +* [docker-compose](https://docs.docker.com/compose/install/) first. + +If not already done, clone the repo and open it: + +```shell +$ git clone https://github.com/modoboa/modoboa.git +$ cd modoboa +``` + +Then, just run the following command: + +```shell +$ docker-compose up +``` + +Then if not done already, run this command to create an OIDC application in order to be able to log in from the frontend: + +``` bash +$ docker exec modoboa-api '/bin/sh -c python3 /code/test_project/manage.py createapplication --name frontend --client-id "LVQbfIIX3khWR3nDvix1u9yEGHZUxcx53bhJ7FlD" --user 1 --algorithm RS256 --redirect-uris 'https://localhost:3000/login/logged' public authorization-code' +$ docker exec modoboa-api '/bin/sh -c python3 /code/test_project/manage.py createapplication --name Dovecot --skip-authorization --client-id=dovecot --client-secret=Toto12345 confidential client-credentials' +``` + +It will start the docker environment and make a Modoboa instance available at `https://localhost:8000` +and the new admin interface at `https://localhost:3000`. + +If you don't want to use docker or need a more complex development setup, go to the next section. + +## Without Docker + +### Prepare a virtual environment + +A [virtual environment](https://docs.python.org/fr/3/library/venv.html) +is a good way to setup a development environment on your machine. + +To do so, run the following commands: + +```shell +$ python3 -m venv +$ source /bin/activate +$ git clone https://github.com/modoboa/modoboa.git +$ cd modoboa +$ pip install -e .[dev] +``` + +This will create a symbolic link to your local copy so any modification +you make will be automatically available in your environment, +no need to copy them. + +### Deploy an instance for development + +::: warning +Make sure to `create a database ` before running this step. + +The format of the database url is also described in this page. +::: + +Now that you have setup a development environment, you can deploy a test instance and run it: + +```shell +$ cd +$ modoboa-admin.py deploy --dburl default: --domain localhost --devel instance +$ python manage.py runserver +``` + +You're ready to go! You should be able to access Modoboa at `http://localhost:8000` using `admin:password` as credentials. + +## Frontend + +The 2.0 version of Modoboa introduces a completely new interface written with the [Vue.js](https://vuejs.org/) framework. + +The source files are located in the `frontend/`{.interpreted-text role="file"} directory. + +To set it up, you will need to install NodeJS and Yarn - to manage the +dependencies. + +Then, navigate to the `frontend/` directory and run: + +```shell +$ yarn install +``` + +You can now build it and serve it - while running your instance too to +serve the API - with: + +```shell +$ yarn serve +``` + +## Tests + +If you deployed an instance for development, you can launch the tests from it with: + +```shell +$ python manage.py test modoboa +``` + +You could also test just some them, i.e.: + +```shell +$ python manage.py test modoboa.core.tests.test_authentication +``` + +Alternatively, you can use [tox](https://tox.readthedocs.io) +from the repository to run all the tests and check the coverage with: + +```shell +$ tox +``` + +You could limit the environment to a specific Python version with the `-e py` argument. + +Note that it is also possible to quickly run a test instance without any deployment - e.g. to preview some changes - by running: + +```shell +$ tox -e serve +``` + +## Documentation + +The source files are located in the `/doc-ng/`[ folder and are +written in markdown. + +They are formatted in HTML and compiled thanks to VitePress + +To build it and see the result, run: + +```shell +$ npm run dev +``` \ No newline at end of file diff --git a/doc-ng/contributing/plugin_api.md b/doc-ng/contributing/plugin_api.md new file mode 100644 index 000000000..5114ac3c9 --- /dev/null +++ b/doc-ng/contributing/plugin_api.md @@ -0,0 +1,193 @@ +# Create a new plugin + +## Introduction + +Modoboa offers a plugin API to expand its capabilities. + +The current implementation provides the following possibilities: + +- Expand navigation by adding entry points to your plugin inside the GUI +- Access and modify administrative objects (domains, mailboxes, etc.) +- Register callback actions for specific events + +::: tip +Plugins are nothing more than Django applications with an extra piece of +code that integrates them into Modoboa. +::: + +The `modo_extension.py` file will contain a complete description of the plugin: + +- Admin and user parameters +- Custom menu entries + +The communication between both applications is provided by +[Django signals](https://docs.djangoproject.com/en/5.2/topics/signals/). + +The following subsections describe the plugin architecture and explain +how you can create your own. + +## The required glue + +To create a new plugin, just start a new django application like this +(into Modoboa's directory): + +```shell +$ python manage.py startapp +``` + +Then, you need to register this application using the provided API. + +Just copy/paste the following example into the `modo_extension.py` file of the future extension: + +```python +from modoboa.core.extensions import ModoExtension, exts_pool + + +class MyExtension(ModoExtension): + """My custom Modoboa extension.""" + + name = "myext" + label = "My Extension" + version = "0.1" + description = "A description" + url = "myext_root_location" # optional, name is used if not defined + + def load(self): + """This method is called when Modoboa loads available and activated plugins. + + Declare parameters and register events here. + """ + pass + + def load_initial_data(self): + """Optional: provide initial data for your extension here.""" + pass + +exts_pool.register_extension(MyExtension) +``` + +Once done, simply add your extension's module name to the `MODOBOA_APPS` +variable located inside `settings.py`. + +Finally, run the following commands: + +```shell +$ python manage.py migrate +$ python manage.py load_initial_data +$ python manage.py collectstatic +``` + +### Parameters + +A plugin can declare its own parameters. There are two levels available: + +* `Global` parameters: used to configure the plugin, editable inside + the *Admin > Settings > Parameters* page +* `User` parameters: per-user parameters (or preferences), editable + inside the *Options > Preferences* page + +#### Playing with parameters + +Parameters are defined using [Django forms](https://docs.djangoproject.com/en/1.9/topics/forms/) +and Modoboa adds two special forms you can inherit depending on the level of parameter(s) you want to add: + +* `modoboa.parameters.forms.AdminParametersForm`: for general parameters +* `modoboa.parameters.forms.UserParametersForm`: for user parameters + +To register new parameters, add the following line into the `load` method of your plugin class: + +```python +from modoboa.parameters import tools as param_tools +param_tools.registry.add(LEVEL, YourForm, ugettext_lazy("Title")) +``` +Replace: + +* `LEVEL` (str): `"global"` or `"user"`. + +### Custom role permissions + +Modoboa uses Django's internal permission system. + +Administrative roles are nothing more than groups (`Group` instances). + +An extension can add new permissions to a group by listening to the `extra_role_permissions` signal. Here is an example: + +``` python +from django.dispatch import receiver +from modoboa.core import signals as core_signals + +PERMISSIONS = { + "Resellers": [ + ("relaydomains", "relaydomain", "add_relaydomain"), + ("relaydomains", "relaydomain", "change_relaydomain"), + ("relaydomains", "relaydomain", "delete_relaydomain"), + ("relaydomains", "service", "add_service"), + ("relaydomains", "service", "change_service"), + ("relaydomains", "service", "delete_service") + ] +} + +@receiver(core_signals.extra_role_permissions) +def extra_role_permissions(sender, role, **kwargs): + """Add permissions to the Resellers group.""" + return constants.PERMISSIONS.get(role, []) +``` + +### Extending admin forms + +The forms used to edit objects (account, domain, etc.) through the admin panel are composed of tabs. + +You can extend them (ie. add new tabs) in a pretty easy way thanks to custom signals. + +#### Account + +To add a new tab to the account edition form, define new listeners +(handlers) for the following signals: + +* `modoboa.admin.signals.extra_account_forms` +* `modoboa.admin.signals.get_account_form_instances` +* `modoboa.admin.signals.check_extra_account_form` (optional) + +Example: + +``` python +from django.dispatch import receiver +from modoboa.admin import signals as admin_signals + + +@receiver(admin_signals.extra_account_forms) +def extra_account_form(sender, user, account, **kwargs): + return [ + {"id": "tabid", "title": "Title", "cls": MyFormClass} + ] + +@receiver(admin_signals.get_account_form_instances) +def fill_my_tab(sender, user, account, **kwargs): + return {"id": my_instance} +``` + +#### Domain + +To add a new tab to the domain edition form, define new listeners +(handlers) for the following signals: + +* `modoboa.admin.signals.extra_domain_forms` +* `modoboa.admin.signals.get_domain_form_instances` + +Example: + +``` python +from django.dispatch import receiver +from modoboa.admin import signals as admin_signals + + +@receiver(admin_signals.extra_domain_forms) +def extra_account_form(sender, user, domain, **kwargs): + return [ + {"id": "tabid", "title": "Title", "cls": MyFormClass} + ] + +@receiver(admin_signals.get_domain_form_instances) +def fill_my_tab(sender, user, domain, **kwargs): + return {"id": my_instance} +``` diff --git a/doc-ng/contributing/translation.md b/doc-ng/contributing/translation.md new file mode 100644 index 000000000..8bf6ce1f3 --- /dev/null +++ b/doc-ng/contributing/translation.md @@ -0,0 +1,20 @@ +# Translation + +Modoboa needs you to translate the project ! Feel free to join the +translation team. + +The project is using Transifex. To start with translation, you will need +to create an account and join [modoboa\'s +team](https://app.transifex.com/tonio/modoboa/dashboard/) + +## Frontend + +When editing the frontend, you may need to update localization files for +transifex to pick up your changes. + +To do so, after finishing editing, go to the frontend directory and run +the following command: + +```shell +$ make makemessages +``` \ No newline at end of file diff --git a/doc-ng/contributors.md b/doc-ng/contributors.md new file mode 100644 index 000000000..3f87980b4 --- /dev/null +++ b/doc-ng/contributors.md @@ -0,0 +1,7 @@ +# Contributors + +- [Antidot](http://antidot.com) +- [Bearstech](http://bearstech.com) +- [CAP-REL](https://cap-rel.fr/) +- [Dalnix](https://www.dalnix.se/) +- [Mirounga](https://mirounga.fr) diff --git a/doc-ng/index.md b/doc-ng/index.md new file mode 100644 index 000000000..5e03f6958 --- /dev/null +++ b/doc-ng/index.md @@ -0,0 +1,41 @@ +--- +# https://vitepress.dev/reference/default-theme-home-page +layout: home + +hero: + name: "Modoboa" + text: "Mail hosting and management platform" + tagline: "simplified Web User Interface designed to work with Postfix and Dovecot" + image: + src: modoboa_logo.png + alt: Modoboa Logo + actions: + - theme: brand + text: QuickStart + link: /installation/modoboa + - theme: alt + text: Contribute + link: /contributing/getting_start + - theme: alt + text: Translate + link: /contributing/translation + - theme: alt + text: Demo + link: https://demo.modoboa.org + +features: + - title: Sovereignty + details: Take back control of your emails to guarantee your security + - title: DKIM, DMARC, SPF + details: increase the deliverability of your emails + - title: API + details: To ease the integration with external sources (software or other), Modoboa provides a REST API +--- + +## Admin Dashboard + +![Admin dashboard](/admin_dashboard.png "Admin dashboard") + +## Webmail + +![User Webmail](/user_webmail.png "User Webmail") \ No newline at end of file diff --git a/doc-ng/installation/amavis.md b/doc-ng/installation/amavis.md new file mode 100644 index 000000000..4cf53b4c4 --- /dev/null +++ b/doc-ng/installation/amavis.md @@ -0,0 +1,272 @@ +# Amavis + +## Quick Amavis setup + +By default, amavis doesn\'t use a database. To configure this behaviour, +you first need to create a dedicated database. This step is a bit manual +since no *ready-to-use* SQL schema is provided by amavis. The +information is located inside README files, one for +[MySQL](http://www.amavis.org/README.sql-mysql.txt) and one for +[PostgreSQL](http://www.amavis.org/README.sql-pg.txt). + +Then, you must tell amavis to use this database for lookups and +quarantined messages storing. Here is a working configuration sample: + +```ini [amavis] +@lookup_sql_dsn = + (['DBI::database=;host=;port=', '', ']']); + +@storage_sql_dsn = + (['DBI::database=;host=;port=', '', ']']); + +# PostgreSQL users NEED this parameter! +# MySQL users only need this parameter is email addresses are stored +# using the VARBINARY type. +$sql_allow_8bit_address = 1; + +$virus_quarantine_method = 'sql:'; +$spam_quarantine_method = 'sql:'; +$banned_files_quarantine_method = 'sql:'; +$bad_header_quarantine_method = 'sql:'; + +$virus_quarantine_to = 'virus-quarantine'; +$banned_quarantine_to = 'banned-quarantine'; +$bad_header_quarantine_to = 'bad-header-quarantine'; +$spam_quarantine_to = 'spam-quarantine'; +``` + +Replace values between `<>` by yours. To know how to configure amavis to +allow quarantined messages release, read this `section +`{.interpreted-text role="ref"}. + + +::: info +Amavis configuration allows for separate lookup and storage databases +but Modoboa doesn\'t support it yet. +::: + +::: info +`$sql_partition_tag` should remain undefined in `amavisd.conf`. Modoboa +does not support the use of `sql_partition_tag`, setting this value can +result in quarantined messages not showing or the wrong messages being +released or learnt as ham/spam. +::: + +## Connect Modoboa and Amavis + +You must tell to Modoboa where it can find the amavis database. Inside +`settings.py`{.interpreted-text role="file"}, add a new connection to +the `DATABASES` variable like this: + +```python{5-9} [settings.py] +DATABASES = { + # Stuff before + # + "amavis": { + "ENGINE" : "", + "HOST" : "", + "NAME" : "", + "USER" : "", + "PASSWORD" : "" + } +} +``` + +Replace values between `<>` with yours. + +### Cleanup + +Storing quarantined messages to a database can quickly become a +perfomance killer. Modoboa provides a simple script to periodically +purge the quarantine database. To use it, add the following line inside +root\'s crontab: + +```txt +0 0 * * * /manage.py qcleanup +# +# Or like this if you use a virtual environment: +# 0 0 * * * /manage.py qcleanup +``` + +Replace `modoboa_site` with the path of your Modoboa instance. + +By default, messages older than 14 days are automatically purged. You +can modify this value by changing the `MAX_MESSAGES_AGE` parameter in +the online panel. + +## Release messages {#amavis_release} + +To release messages, first take a look at [this +page](http://www.ijs.si/software/amavisd/amavisd-new-docs.html#quar-release). + +It explains how to configure amavisd-new to listen somewhere for the +AM.PDP protocol. This protocol is used to send requests. + +Below is an example of a working configuration: + +```txt{1,2,10} +$interface_policy{'SOCK'} = 'AM.PDP-SOCK'; +$interface_policy{'9998'} = 'AM.PDP-INET'; + +$policy_bank{'AM.PDP-SOCK'} = { + protocol => 'AM.PDP', + auth_required_release => 0, +}; +$policy_bank{'AM.PDP-INET'} = { + protocol => 'AM.PDP', + inet_acl => [qw( 127.0.0.1 [::1] )], +}; +``` + +Don't forget to update the `inet_acl` list if you plan to release from +the network. + +Once amavisd-new is configured, just tell Modoboa where it can find the +*release server* by modifying the following parameters in the online +panel: + +| Name | Description | Default value | +| ---------------------- | -------------------------------------------- | :----------------------- | +| Amavis connection mode | Mode used to access the PDP server | unix | +| PDP server address | PDP server address (if inet mode) | localhost | +| PDP server port | PDP server port (if inet mode) | 9998 | +| PDP server socket | Path to the PDP server socket (if unix mode) | /var/amavis/amavisd.sock | + +### Deferred release + +By default, simple users are not allowed to release messages themselves. +They are only allowed to send release requests to administrators. + +As administrators are not always available or logged into Modoboa, +a notification tool is available. + +It sends reminder e-mails to every administrators or domain administrators. +To use it, add the following example line to root's crontab: + +```txt +0 12 * * * /manage.py amnotify --baseurl='' +# +# Or like this if you use a virtual environment: +# 0 12 * * * /manage.py amnotify --baseurl='' +```` + +You are free to change the frequency. + +::: info +If you want to let users release their messages alone (not recommended), +go to the admin panel. +::: + +The following parameters are available to let you customize this feature: + +| Name | Description | Default value | +| ---------------------------- | ---------------------------------------------- | -------------------------- | +| Check requests interval | Interval between two release requests checks | 30 | +| Allow direct release release | Allow users to directly release their messages | no | +| Notifications sender | The e-mail address used to send notitications | | + +## Self-service mode {#selfservice} + +The *self-service* mode lets users act on quarantined messages without beeing authenticated. They can: + +- View messages +- Remove messages +- Release messages (or send release requests) + +To access a specific message, they only need the following information: + +- Message's unique identifier +- Message's secret identifier + +This information is controlled by *amavis*, which is in charge of +notifying users when new messages are put into quarantine. + +Each notification (one per message) must embark a direct link containing the +required identifiers. + +To activate this feature, go the administration panel and set the +**Enable self-service mode** parameter to yes. + +The last step is to customize the notification messages amavis sends. +The most important is to embark a direct link. Take a look at the +[README.customize](http://amavis.org/README.customize.txt) file to learn +what you're allowed to do. + +Here is a link example: + + https:///user/selfquarantine/%i/%R?secret_id=[:secret_id] + +## Manual SpamAssassin learning {#sa_manual_learning} + +It is possible to manually train [SpamAssassin](http://spamassassin.apache.org/) +using the quarantine's content. By train, we mean: + +- Mark message(s) as spam (false negative(s)) +- Mark message(s) as non-spam (false positive(s)) + +This feature is available to all users (from super administrators to +simple users) but not enabled by default. + +### SpamAssassin configuration + +For better performance and to enable the per-user level, SpamAssassin +must store bayes information into a SQL database. + +Create a new database and a new user/password (using your favorite +database server) and edit the default configuration file +(`/etc/spamassassin/local.cf`{.interpreted-text role="file"}) +to add the following lines inside: + +::: code-group + +``` perl [PostgreSQL] +bayes_store_module Mail::SpamAssassin::BayesStore::PgSQL +bayes_sql_dsn +bayes_sql_username +bayes_sql_password +``` + +``` perl [MySQL] +bayes_store_module Mail::SpamAssassin::BayesStore::MySQL +bayes_sql_dsn +bayes_sql_username +bayes_sql_password +``` + +::: + +Replace values between `<>` by yours. Possible values for `Driver` are +`PgSQL` or `MySQL` (non exhaustive list). The syntax for `DSN` depends +on the driver you choose. Please consult the official documentation. + +### Enable the feature through Modoboa + +Manual learning is disabled by default. You can activate it through the +administration panel (*Modoboa \> Paremeters \> Amavis*). There two +learning levels: + +1. Global: available to administrators only. A single (global) bayes + database is shared between everyone. +2. Per domain: available to administrators and domain administrators. + Each domain can have a dedicated database. +3. Per user: each user can create its own database to customize the way + SpamAssassin will detect spam. + +The domain and user levels are not activated by default, dedicated +parameters are available through the panel. + +::: info +Domain and user databases are only created the first time someone calls +the learning feature through the quarantine. +::: + +::: warning +A bayes database needs to reach pre-defined thresholds before it can be +used by SpamAssassin. The default values are **200** spams and **200** +hams. +::: + +You will find other paramaters related to this feature. You won\'t need +to change them most of the time, unless SpamAssassin is hosted on a +different machine than Modoboa. (in this case, `spamc` will be used +instead of `sa-learn`). \ No newline at end of file diff --git a/doc-ng/installation/dovecot.md b/doc-ng/installation/dovecot.md new file mode 100644 index 000000000..2a724bea4 --- /dev/null +++ b/doc-ng/installation/dovecot.md @@ -0,0 +1,656 @@ +# Dovecot + +## introduction + +Modoboa requires Dovecot 2+ to work so the following documentation is +suitable for this combination. + +In this section, we assume dovecot's configuration resides in +`/etc/dovecot`, all pathes will be relative to this directory. + +You can find configuration documentation [here](https://doc.dovecot.org/configuration_manual/). + +This proposed configuration is the one you can find with modoboa-installer. + +For up to date configuration, +[take a look here](https://github.com/modoboa/modoboa-installer/tree/master/modoboa_installer/scripts/files/dovecot/conf.d) + +## Packages + +For debian and derivative, recommended packages are: + +```shell +apt install dovecot-imapd dovecot-lmtpd dovecot-managesieved dovecot-sieve +``` + +## Mailboxes + +First, edit the `conf.d/10-mail.conf` and +set the `mail_location` variable: + +```txt{2} +# maildir +mail_location = maildir:~/Maildir +``` + +Then, edit the `inbox` namespace and add the following line: + +```txt +inbox = yes +``` + +In order for dovecot to automaticaly create usual folders on account +creation, edit the `inbox` namespace of `15-mailboxes.conf`: + +```txt +mailbox Drafts { + auto = subscribe + special_use = \Drafts +} +mailbox Junk { + auto = subscribe + special_use = \Junk +} +mailbox Sent { + auto = subscribe + special_use = \Sent +} +mailbox Trash { + auto = subscribe + special_use = \Trash +} +``` + +Starting dovecot 2.2.20+, you can add `autoexpunge` parameter to delete older message, for instance: + +```txt +mailbox Trash { + auto = subscribe + special_use = \Trash + autoexpunge=30d +} +``` + +With dovecot 2.1+, it ensures all the special mailboxes will be automaticaly created for new accounts. + +For dovecot 2.0 and older, use the [autocreate](http://wiki2.dovecot.org/Plugins/Autocreate) plugin. + +### Operations on the file system {#fs_operations} + +::: warning +Modoboa needs to access the `dovecot` binary to check its version. To +find the binary path, we use the `which` command first and then try +known locations (`/usr/sbin/dovecot` and +`/usr/local/sbin/dovecot` ). If you +installed dovecot in a custom location, please tell us where the binary +is by using the `DOVECOT_LOOKUP_PATH` setting (see +`settings.py` ). +::: + +Three operation types are considered: + +1. Mailbox creation +2. Mailbox renaming +3. Mailbox deletion + +The first one is managed by Dovecot. The last two ones may be managed by +Modoboa if it can access the file system where the mailboxes are stored +(see `admin-params` to activate this +feature). + +Those operations are treated asynchronously by a cron script. For +example, when you rename an e-mail address through the web UI, the +associated mailbox on the file system is not modified directly. Instead +of that, a *rename* order is created for this mailbox. The mailbox will +be considered unavailable until the order is executed (see +`Postfix configuration `). + +Edit the crontab of the user who owns the mailboxes on the file system: + +```shell +$ crontab -u -e +``` + +And add the following line inside: + +```txt +* * * * * python /manage.py handle_mailbox_operations +``` + +::: warning +The cron script must be executed by the system user owning the +mailboxes. +::: + +::: warning +The user running the cron script must have access to the +`settings.py` file of the modoboa +instance. +::: + +The result of each order is recorded into Modoboa's log. + +Go to *Modoboa > Logs* to consult them. + +## Authentication + +To make the authentication work, edit the +`conf.d/10-auth.conf` and uncomment the +following line at the end: + +```txt{2} +#!include auth-system.conf.ext +!include auth-sql.conf.ext +#!include auth-ldap.conf.ext +#!include auth-passwdfile.conf.ext +#!include auth-checkpassword.conf.ext +#!include auth-vpopmail.conf.ext +#!include auth-static.conf.ext +``` + +Then, edit the `conf.d/auth-sql.conf.ext` +file and add the following content inside: +```txt +passdb sql { + driver = sql + # Path for SQL configuration file, see example-config/dovecot-sql.conf.ext + args = /etc/dovecot/dovecot-sql.conf.ext +} + +userdb sql { + driver = sql + args = /etc/dovecot/dovecot-sql.conf.ext +} +``` + +Make sure to activate only one backend (per type) inside your +configuration (just comment the other ones). + +Edit the `dovecot-sql.conf.ext` and modify the configuration according to your database engine. + +::: code-group + +``` shell [PostgreSQL] +# For debian and derivative, you need to install postgres connector for dovecot:: +sudo apt install dovecot-pgsql +``` + +``` shell [MySQL] +# For debian and derivative, you need to install mysql connector for dovecot:: +sudo apt install dovecot-mysql +``` + +``` shell [SQLite] +# For debian and derivative, you need to install sqlite connector for dovecot:: +sudo apt install dovecot-sqlite +``` + +::: + +::: code-group +``` txt [PostgreSQL] +driver = pgsql + +connect = host= dbname= user= password= + +default_pass_scheme = CRYPT + +user_query = \ + SELECT '%{home_dir}/%%d/%%n' AS home, %mailboxes_owner_uid as uid, \ + %mailboxes_owner_gid as gid, '*:bytes=' || mb.quota || 'M' AS quota_rule \ + FROM admin_mailbox mb \ + INNER JOIN admin_domain dom ON mb.domain_id=dom.id \ + INNER JOIN core_user u ON u.id=mb.user_id \ + WHERE (mb.is_send_only IS NOT TRUE OR '%s' NOT IN ('imap', 'pop3', 'lmtp')) \ + AND mb.address='%%n' AND dom.name='%%d' + +password_query = \ + SELECT email AS user, password, '%{home_dir}/%%d/%%n' AS userdb_home, \ + %mailboxes_owner_uid AS userdb_uid, %mailboxes_owner_gid AS userdb_gid, \ + CONCAT('*:bytes=', mb.quota, 'M') AS userdb_quota_rule \ + FROM core_user u \ + INNER JOIN admin_mailbox mb ON u.id=mb.user_id \ + INNER JOIN admin_domain dom ON mb.domain_id=dom.id \ + WHERE (mb.is_send_only IS NOT TRUE OR '%s' NOT IN ('imap', 'pop3'))\ + AND email='%%u' AND is_active AND dom.enabled + +iterate_query = SELECT email AS user FROM core_user WHERE is_active +``` + +``` txt [MySQL] +driver = mysql + +connect = host= dbname= user= password= + +default_pass_scheme = CRYPT + +user_query = \ + SELECT '%{home_dir}/%%d/%%n' AS home, %mailboxes_owner_uid as uid, \ + %mailboxes_owner_gid as gid, \ + CONCAT('*:bytes=', mb.quota, 'M') AS quota_rule \ + FROM admin_mailbox mb \ + INNER JOIN admin_domain dom ON mb.domain_id=dom.id \ + INNER JOIN core_user u ON u.id=mb.user_id \ + WHERE (mb.is_send_only=0 OR '%s' NOT IN ('imap', 'pop3', 'lmtp')) \ + AND mb.address='%%n' AND dom.name='%%d' + +password_query = \ + SELECT email AS user, password, '%{home_dir}/%%d/%%n' AS userdb_home, \ + %mailboxes_owner_uid AS userdb_uid, %mailboxes_owner_gid AS userdb_gid, \ + CONCAT('*:bytes=', mb.quota, 'M') AS userdb_quota_rule \ + FROM core_user u \ + INNER JOIN admin_mailbox mb ON u.id=mb.user_id \ + INNER JOIN admin_domain dom ON mb.domain_id=dom.id \ + WHERE (mb.is_send_only=0 OR '%s' NOT IN ('imap', 'pop3')) \ + AND u.email='%%u' AND u.is_active=1 AND dom.enabled=1 + +iterate_query = SELECT email AS user FROM core_user WHERE is_active +``` + +```txt [SQLite] +driver = sqlite + +connect = + +default_pass_scheme = CRYPT + +password_query = SELECT email AS user, password FROM core_user u INNER JOIN admin_mailbox mb ON u.id=mb.user_id INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE u.email='%Lu' AND u.is_active=1 AND dom.enabled=1 + +user_query = SELECT '/%Ld/%Ln' AS home, as uid, as gid, ('*:bytes=' || mb.quota || 'M') AS quota_rule FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.address='%Ln' AND dom.name='%Ld' + +iterate_query = SELECT email AS user FROM core_user +``` +::: + + +::: info +Replace values between `<>` with yours. +::: + +## OAuth 2 authentication + +You first need to register Dovecot as an authorized consumer of the +OAuth2 authentication service provided by Modoboa. + +To do so, create an application with the following commands: + +``` shell +cd +python manage.py createapplication --name=Dovecot --skip-authorization --client-id=dovecot confidential client-credentials +``` + +On success, you should see an output similar to: + +``` txt +New application Dovecot created successfully. +client_secret: XXXX +``` + +To enable OAuth2 authentication in Dovecot, edit the `conf.d/10-auth.conf` +file and update the auth_mechanisms line as follows: + +``` txt +auth_mechanisms = plain login oauthbearer xoauth2 +``` + +Then, add the following line at the end: + +``` txt +!include auth-oauth2.conf.ext +``` + +Then, create a file named `conf.d/auth-oauth2.conf.ext` with the following content: + +``` txt +passdb { + driver = oauth2 + mechanisms = xoauth2 oauthbearer + args = /etc/dovecot/conf.d/dovecot-oauth2.conf.ext +} +``` + +Finally, create a file named `conf.d/dovecot-oauth2.conf.ext` with the following content: + +``` txt +introspection_mode = post +introspection_url = https://dovecot:@/api/o/introspect/ +username_attribute = username +tls_ca_cert_file = /etc/ssl/certs/ca-certificates.crt +active_attribute = active +active_value = true +``` + +Replace `` with the value you obtained earlier. + +## LDAP + +To make the LDAP authentication work, edit the `conf.d/10-auth.conf` +and uncomment the following line at the end: + +``` txt +!include auth-ldap.conf.ext +``` + +Then edit the `conf.d/auth-ldap.conf.ext` and edit the passdb section as following. + +You should comment the userdb section, which will be managed by SQL with modoboa database: + +``` txt +passdb { + driver = ldap + + # Path for LDAP configuration file, see example-config/dovecot-ldap.conf.ext + args = /etc/dovecot/dovecot-ldap.conf.ext +} + +#userdb { + #driver = ldap + #args = /etc/dovecot/dovecot-ldap.conf.ext + + # Default fields can be used to specify defaults that LDAP may override + #default_fields = home=/home/virtual/%u +#} +``` + +Your own dovecot LDAP configuration file is now `/etc/dovecot/dovecot-ldap.conf.ext`. + +You can add your default LDAP conf in it, following the +[official documentation](https://doc.dovecot.org/configuration_manual/authentication/ldap/). + +### Synchronize dovecot LDAP conf with modoboa LDAP conf + +To make dovecot LDAP configuration synchronized with modoboa LDAP configuration, +you should create a dedicated dovecot conf file. + +At the end of your dovecot configuration file (`dovecot-ldap.conf.ext` ), add the following line: + +``` txt +!include_try dovecot-modoboa.conf.ext +``` + +Then, set modoboa parameter *Enable Dovecot LDAP sync* to *Yes*. +Then set the *Dovecot LDAP config file* following the previous step +(*/etc/dovecot/dovecot-modoboa.conf.ext* in the example) + +The last step is to add the command **update_dovecot_conf** to the cron +job of modoboa. Then, each time your modoboa LDAP configuration is +updated, your dovecot LDAP configuration will also be. + +## LMTP + +[Local Mail Transport Protocol](http://en.wikipedia.org/wiki/Local_Mail_Transfer_Protocol) +is used to let Postfix deliver messages to Dovecot. + +First, make sure the protocol is activated by looking at the `protocols` +setting (generally inside `dovecot.conf`). + +It should be similar to the following example: + +```txt +protocols = imap pop3 lmtp +``` + +Then, open the `conf.d/10-master.conf` , +look for `lmtp` service definition and add the following content inside: + +```txt +service lmtp { + # stuff before + unix_listener /var/spool/postfix/private/dovecot-lmtp { + mode = 0600 + user = postfix + group = postfix + } + # stuff after +} +``` + +We assume here that Postfix is *chrooted* within +`/var/spool/postfix` . + +Finally, open the `conf.d/20-lmtp.conf` and modify it as follows: + +```txt +protocol lmtp { + postmaster_address = postmaster@ + mail_plugins = $mail_plugins quota sieve +} +``` + +Replace `` by the appropriate value. + +::: info +If you don't plan to apply quota or to use filters, just adapt the +content of the `mail_plugins` setting. +::: + +## Quota + +Modoboa lets adminstrators define per-domain and/or per-account limits +(quota). It also lists the current quota usage of each account. Those +features require Dovecot to be configured in a specific way. + +Inside `conf.d/10-mail.conf` , +add the `quota` plugin to the default activated ones: + +```txt +mail_plugins = quota +``` + +Inside `conf.d/10-master.conf` , update +the `dict` service to set proper permissions: + +```txt +service dict { + # If dict proxy is used, mail processes should have access to its socket. + # For example: mode=0660, group=vmail and global mail_access_groups=vmail + unix_listener dict { + mode = 0600 + user = + #group = + } +} +``` + +Inside `conf.d/20-imap.conf` , activate +the `imap_quota` plugin: + +```txt{4} +protocol imap { + # ... + + mail_plugins = $mail_plugins imap_quota + + # ... +} +``` + +Inside `dovecot.conf` , activate the quota +SQL dictionary backend: + +```txt{2} +dict { + quota = :/etc/dovecot/dovecot-dict-sql.conf.ext +} +``` + +Inside `conf.d/90-quota.conf` , activate +the *quota dictionary* backend: + +```txt +plugin { + quota = dict:User quota::proxy::quota +} +``` + +It will tell Dovecot to keep quota usage in the SQL dictionary. + +Finally, edit the `dovecot-dict-sql.conf.ext`{.interpreted-text +role="file"} file and put the following content inside: + +```txt +connect = host= dbname= user= password= +# SQLite users +# connect = /path/to/the/database.db + +map { + pattern = priv/quota/storage + table = admin_quota + username_field = username + value_field = bytes +} +map { + pattern = priv/quota/messages + table = admin_quota + username_field = username + value_field = messages +} +``` + +### PostgreSQL users + +#### Database schema update + +The `admin_quota` table is created by Django but unfortunately it +doesn't support `DEFAULT` constraints (it only simulates them when the +ORM is used). + +As PostgreSQL is a bit strict about constraint violations, +you must execute the following query manually: + +```sql +db=> ALTER TABLE admin_quota ALTER COLUMN bytes SET DEFAULT 0; +db=> ALTER TABLE admin_quota ALTER COLUMN messages SET DEFAULT 0; +```` + +#### Trigger + +As indicated on [Dovecot's doc](https://doc.dovecot.org/2.4.2/core/plugins/quota.html#quota-plugin-quota), +you need a trigger to properly update the quota. + +A working copy of this trigger is available on +[Github](https://raw.githubusercontent.com/modoboa/modoboa-installer/master/modoboa_installer/scripts/files/dovecot/install_modoboa_postgres_trigger.sql). + +Download this file and copy it on the server running postgres. + +Then, execute the following commands: + +```shell +$ su - postgres +$ psql [modoboa database] < /path/to/modoboa_postgres_trigger.sql +$ exit +``` + +Replace `[modoboa database]` by the appropriate value. + +### Forcing recalculation + +For existing installations, *Dovecot* (\> 2) offers a command to +recalculate the current quota usages. For example, if you want to update +all usages, run the following command: + +```shell +$ doveadm quota recalc -A +``` + +Be carefull, it can take a while to execute. + +## ManageSieve/Sieve + +Modoboa lets users define filtering rules from the web interface. + +To do so, it requires *ManageSieve* to be activated on your server. + +Inside `conf.d/20-managesieve.conf`, make sure the following lines are uncommented: + +```txt +protocols = $protocols sieve + +service managesieve-login { + # ... +} + +service managesieve { + # ... +} + +protocol sieve { + # ... +} +``` + +Finally, configure the `sieve` plugin by editing the `conf.d/90-sieve.conf` file. +Put the follwing caontent inside: + +```txt +plugin { + # Location of the active script. When ManageSieve is used this is actually + # a symlink pointing to the active script in the sieve storage directory. + sieve = ~/.dovecot.sieve + + # + # The path to the directory where the personal Sieve scripts are stored. For + # ManageSieve this is where the uploaded scripts are stored. + sieve_dir = ~/sieve +} +``` + +Restart Dovecot. + +Now, you can go to the `postfix` section to finish the installation. + +## Last-login tracking + +To update the `last_login` attribute of an account after a succesful +IMAP or POP3 login, you can configure a [post-login +script](https://wiki.dovecot.org/PostLoginScripting). + +Open `conf.d/10-master.conf` add the following configuration +(`imap` and `pop3` services are already defined, +you just need to update them): + +```txt +service imap { + executable = imap postlogin +} + +service pop3 { + executable = pop3 postlogin +} + +service postlogin { + executable = script-login /usr/local/bin/postlogin.sh + user = modoboa + unix_listener postlogin { + } +} +``` + +Then, you must create a script named +`/usr/local/bin/postlogin.sh` . + +According to your database engine, the content will differ. + +::: code-group + +``` bash [PostgreSQL] +#!/bin/sh + +psql -c "UPDATE core_user SET last_login=now() WHERE username='$USER'" > /dev/null + +exec "$@" +``` + +``` bash [MySQL] +#!/bin/sh + +DBNAME=XXX +DBUSER=XXX +DBPASSWORD=XXX + +echo "UPDATE core_user SET last_login=now() WHERE username='$USER'" | mysql -u $DBUSER -p$DBPASSWORD $DBNAME + +exec "$@" +``` + +::: \ No newline at end of file diff --git a/doc-ng/installation/installer.md b/doc-ng/installation/installer.md new file mode 100644 index 000000000..d7007de09 --- /dev/null +++ b/doc-ng/installation/installer.md @@ -0,0 +1,54 @@ +# Automatic Installer + + +## Introduction + +If you start from scratch and want to deploy a complete mail server, + +you will love the [modoboa installer](https://github.com/modoboa/modoboa-installer)! + +It is the easiest and the quickest way to setup a fully functional server +(modoboa, postfix, dovecot, amavis and more) on one machine. + +## How to start + +::: warning +For now, only Debian based Linux distributions are supported. + +We do our best to improve compatibility but if you use another `Linux` +or a `UNIX` system, you will have to install Modoboa `manually `. +::: + +To use it, just run the following commands in your terminal: + +``` shell +$ git clone https://github.com/modoboa/modoboa-installer +$ cd modoboa-installer +$ sudo ./run.py +``` + +::: warning +if you get this warning + +```console +'/usr/bin/env: 'python': No such file or directory', +``` + +do make sure sure python is installed on your server. + +Sometimes python is installed but the installer can't detect it or which python version to run, +especially on a debian based system, check the `PATH` variable for the current user. + +Then run this command first. + +``` shell +$ sudo apt-get install python3-virtualenv python3-pip +``` + +::: + +For the sake of simplicity, you can also install the `python-is-python3` package. + +It allows you to use `python` command and point to the python3 runtime. + +Wait a few minutes and you're done 😎 \ No newline at end of file diff --git a/doc-ng/installation/manual.md b/doc-ng/installation/manual.md new file mode 100644 index 000000000..e0ae00e30 --- /dev/null +++ b/doc-ng/installation/manual.md @@ -0,0 +1,13 @@ +# Manual installation + +For those who need a manual installation or who just want to setup a specific part, + +here are the steps you must follow: + +* [Modoboa](modoboa) +* [Web Server](webserver) +* [Dovecot](dovecot) +* [Postfix](postfix) +* [OpenDKIM](opendkim) +* [Radicale](radicale) +* [Amavis](amavis) diff --git a/doc-ng/installation/modoboa.md b/doc-ng/installation/modoboa.md new file mode 100644 index 000000000..58f59f705 --- /dev/null +++ b/doc-ng/installation/modoboa.md @@ -0,0 +1,317 @@ +# Modoboa + +This section describes the installation of the web interface (a [Django](https://www.djangoproject.com/) project). + +This proposed configuration is the one you can find with `modoboa-installer`. + +For up to date configuration, [take a look here](https://github.com/modoboa/modoboa-installer/tree/master/modoboa_installer/scripts/files/modoboa/) + +## Prepare the system + +First of all, we recommend the following context: + +* A dedicated system user +* A [virtual environment](https://packaging.python.org/tutorials/installing-packages/#creating-virtual-environments) + to install the application because it will isolate it (and its dependencies) from the rest of your system + +The following example illustrates how to realize this on Debian-based distributions using [virtualenv](https://virtualenv.pypa.io/en/stable/): + +``` shell +$ sudo apt-get install virtualenv python3-pip +$ sudo useradd modoboa # create a dedicated user +$ sudo -u modoboa -i bash # log in as the newly created user +$ virtualenv --python python3 ./env # create the virtual environment +$ source ./env/bin/activate # activate the virtual environment +``` + +::: warning +Modoboa depends on external tools and some of them require compilation +so you need a compiler and a few C libraries. +::: + +::: tip INFORMATION +Make sure to install the following system packages according to your distribution: + +* Debian / Ubuntu + * build-essential + * python3-dev + * libxml2-dev + * libxslt-dev + * libjpeg-dev + * librrd-dev + * rrdtool + * libffi-dev + * libssl-dev pkg-config + * libcairo2-dev + +* CentOS / Redhat + * gcc + * gcc-c++ + * python3-devel + * libxml2-devel + * libxslt-devel + * libjpeg-turbo-devel + * rrdtool-devel rrdtool + * libffi-devel +::: + +::: info + +Alternatively, you could rely on your distribution packages for the +Modoboa dependencies which require compilation +* e.g. `rrdtool` - if the version is compatible. + In this case, you have to create your virtual environment + with the `--system-site-packages` option, and the required + system packages will be: + + * python3-wheel + * python3-rrdtool + * rrdtool +::: + +Then, install Modoboa by running: + +``` console +(env)$ pip install modoboa +``` + +## Database + +::: warning +This documentation does not cover the installation of a database server +but only the setup of a functional database that Modoboa will use. +::: + +Thanks to Django, Modoboa is compatible with the following databases: + +- PostgreSQL +- MySQL / MariaDB +- SQLite + +Since the last one does not require particular actions, only the first +two ones are described. + +You should also read the notes for those database backends on the +[official Django documentation](https://docs.djangoproject.com/en/stable/ref/databases/). + +### PostgreSQL + +Install the corresponding Python binding: + +``` shell +(env)$ pip install psycopg[binary]>=3.1 +``` + +::: info +Ubuntu and Debian repositories contain python-psycopgX package but +starting modoboa 2.2.0 minimum version required is 3.1 and it has not +yet it these distro yet Prior modoboa version can run with `python3-psycopg2`. +::: + +Then, create a user and a database. + +For example, to create the `modoboa` database owned by a `modoboa` user, +run the following commands on your PostgreSQL server as `root` or admin user: + +``` bash +$ sudo -l -u postgres createuser --no-createdb modoboa +$ sudo -l -u postgres createdb --owner=modoboa modoboa +``` + +### MySQL / MariaDB + +Install the corresponding Python binding: + +``` console +(env)$ pip install mysqlclient +``` + +::: tip +Alternatively, you can install the `python3-mysqldb` package instead on +Debian-based distributions if your virtual environment was created with +`--system-site-packages` option. +::: + +:::: info NOTE +MariaDB 10.2 (and newer) require mysqlclient 1.3.11 (or newer). +:::: + +Then, create a user and a database. For example, to create the `modoboa` +database owned by a `modoboa` user, run the following SQL commands: + +``` sql +CREATE DATABASE modoboa; +CREATE USER 'modoboa'@'localhost' IDENTIFIED BY 'my-strong-password-here'; +GRANT ALL PRIVILEGES ON modoboa.* TO 'modoboa'@'localhost'; +``` + +## Deploy an instance {#deployment} + +`modoboa-admin.py` is a command line tool that lets you deploy a +*ready-to-use* Modoboa site. To create a new instance into `./instance`, +you just have to run the following command: + +``` shell +(env)$ modoboa-admin.py deploy instance --collectstatic \ + --domain --dburl default: +``` + +::: info + +You can install additional extensions during the deploy process. To do +so, use the `--extensions` option which accepts a list of names as +argument (`--extensions ext1 ext2 ...`). If you want to install all +extensions, just use the `all` keyword like this `--extensions all`. + +If you choose to install extensions one at a time, you will have to add +their names in settings.py to `MODOBOA_APPS`. Also ensure that you have +the line `from modoboa_amavis.settings import *` at the end of this file +and possibly perform more steps. Check the documentation/readme of each +extensions for specific Instructions. + +The list of available extensions can be found on the `index +page <../index>`{.interpreted-text role="doc"}. Instructions to install +them are available on each extensions page. +::: + +::: tip TIPS + +You can specify more than one database connection using the `--dburl` +option. Multiple connections are differentiated by a prefix. + +The primary connection must use the `default:` prefix (as shown in the +example above). For the [amavis](http://modoboa-amavis.readthedocs.org) +extension, use the `amavis:` prefix. For example: +`--dburl default: amavis:`. + +A database url should meet the following syntax +`://[user:pass@][host:port]/dbname` **OR** +`sqlite:////full/path/to/your/database/file.sqlite`. + +Fox example, if you were using postgres, you could setup your command +like this: +`modoboa-admin.py deploy instance_name --collectstatic --domain example.com --dburl default:postgres://user:pass@[localhost]/modoboa` +::: + +The command will ask you a few questions, answer them and you\'re done. + +If you need a **silent installation** (e.g. if you\'re using Salt-Stack, +Ansible or whatever), it's possible to supply the database credentials +as command line arguments. + +You can consult the complete option list by running the following command: + +``` shell +(env)$ modoboa-admin.py help deploy +``` + +## Cron jobs + +A few recurring jobs must be configured to make Modoboa works as expected. + +Create a new file, for example `/etc/cron.d/modoboa` and put the following content inside: + +``` shell +# +# Modoboa specific cron jobs +# +PYTHON= +INSTANCE= + +# Operations on mailboxes +* * * * * $PYTHON $INSTANCE/manage.py handle_mailbox_operations + +# Generate DKIM keys (they will belong to the user running this job) +* * * * * root umask 077 && $PYTHON $INSTANCE/manage.py modo manage_dkim_keys + +# Sessions table cleanup +0 0 * * * root $PYTHON $INSTANCE/manage.py clearsessions +# Logs table cleanup +0 0 * * * root $PYTHON $INSTANCE/manage.py cleanlogs +# Logs parsing +*/15 * * * * root $PYTHON $INSTANCE/manage.py logparser &> /dev/null +0 * * * * modoboa $PYTHON $INSTANCE/manage.py update_statistics +# DNSBL checks +*/30 * * * * modoboa $PYTHON $INSTANCE/manage.py modo check_mx +# Public API communication +0 * * * * modoboa $PYTHON $INSTANCE/manage.py communicate_with_public_api +``` + +::: tip TIPS + +**🥵 potential high load configuration** +Please note that above crontab might not be ideal on high load systems. +If you receive a fairly high amount of emails per day, you may want to +run modoboas logparser tasks *once per night*. + +This has the down side that the statistic graph and message log within +the UI are updated once per day only. +::: + +## Policy daemon {#policy_daemon} + +Modoboa comes with a built-in +[Policy Daemon for Postfix](http://www.postfix.org/SMTPD_POLICY_README.html). +Current features are: + +* Define daily sending limits for domains and/or accounts + +A [redis server](https://redis.io/) is required to run this new daemon. + +You can launch it manually using the following command: + +``` shell +(env) $ python manage.py policy_daemon +``` + +But we recommend an automatic start using `systemd` or `supervisor`. + +Here is a configuration example for `supervisor`: + +``` ini +[program:policyd] +autostart=true +autorestart=true +command= /manage.py policy_daemon +directory= +redirect_stderr=true +user=modoboa +numprocs=1 +``` + +It will listen by default on `127.0.0.1` and port `9999`. + +The policy daemon won't do anything unless you tell +`postfix ` to use it. + +## RQ daemon + +Modoboa uses [RQ](https://python-rq.org) as a job handler for async task. + +A [redis server](https://redis.io/) is required to run this new daemon. + +A worker needs to be launched in the venv. + +You can launch it manually using the following command: + +``` shell +(env)> rq worker high default low +``` + +But we recommend an automatic start using `systemd` or `supervisor`. + +Here is a configuration example for `supervisor`: + +``` ini +[program:modoboa-dkim-worker] +autostart=true +autorestart=true +command= /manage.py rqworker dkim +directory= +user= +redirect_stderr=true +numprocs=1 +stopsignal=TERM +``` + +Now you can continue to the [webserver](webserver) section. \ No newline at end of file diff --git a/doc-ng/installation/opendkim.md b/doc-ng/installation/opendkim.md new file mode 100644 index 000000000..bd914a810 --- /dev/null +++ b/doc-ng/installation/opendkim.md @@ -0,0 +1,79 @@ +# OpenDKIM + +Modoboa can generate [DKIM](https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail) keys +for the hosted domains but it won't sign or check messages. + +To do that, you need a dedicated software like [OpenDKIM](http://opendkim.org/). + +::: warning +The cron job in charge of creating DKIM keys must be run using the same +user than `OpenDKIM` (ie. opendkim in most cases). +::: + +## Database + +Since keys related information is stored in Modoboa's database, you +need to tell OpenDKIM how it can access it. + +First, make sure to install the required additional packages on your +system (`libopendbx1-*` on debian based distributions or `opendbx-*` on +CentOS, the complete name depends on your database engine). + +Then, insert the following SQL view into Modoboa\'s database: + +::: code-group + +```sql [PostgreSQL] +CREATE OR REPLACE VIEW dkim AS ( + SELECT id, name as domain_name, dkim_private_key_path AS private_key_path, + dkim_key_selector AS selector + FROM admin_domain WHERE enable_dkim +); +``` + +```sql [MySQL] +CREATE OR REPLACE VIEW dkim AS ( + SELECT id, name as domain_name, dkim_private_key_path AS private_key_path, + dkim_key_selector AS selector + FROM admin_domain WHERE enable_dkim=1 +); +``` +::: + +## Configuration + +You should find OpenDKIM\'s configuration file at `/etc/opendkim.conf`. + +Add the following content to it: + +``` txt +KeyTable dsn:://:@//table=dkim?keycol=id?datacol=domain_name,selector,private_key_path +SigningTable dsn:://:@//table=dkim?keycol=domain_name?datacol=id +Socket inet:12345@localhost +``` + +Replace values between `<>` by yours. Accepted values for `driver` are +`pgsql` or `mysql`. Make sure the user you specify has read permission +on the view created previously. + +If you run a debian based system, make sure to adjust the following +setting in the `/etc/default/opendkim` file: + +``` txt +SOCKET=inet:12345@127.0.0.1 +``` + +Eventually, `reload OpenDKIM`. + +## Postfix integration + +Add the following lines to the `/etc/postfix/main.cf` file: + +``` txt{1,2} +smtpd_milters = inet:127.0.0.1:12345 +non_smtpd_milters = inet:127.0.0.1:12345 +milter_default_action = accept +milter_content_timeout = 30s +``` + +and `reload postfix`. \ No newline at end of file diff --git a/doc-ng/installation/postfix.md b/doc-ng/installation/postfix.md new file mode 100644 index 000000000..2cdf41067 --- /dev/null +++ b/doc-ng/installation/postfix.md @@ -0,0 +1,103 @@ +# Postfix + +This section gives an example about building a simple virtual hosting +configuration with *Postfix*. +Refer to the [official documentation](http://www.postfix.org/VIRTUAL_README.html) +for more explanation. + +## Map files + +You first need to create configuration files (or map files) that will be +used by Postfix to lookup into Modoboa tables. + +To automaticaly generate the requested map files and store them in a +directory, run the following command: + +``` bash +$ cd +$ python manage.py generate_postfix_maps --destdir +``` + +`` is the directory where the files will be stored. Answer +the few questions and you\'re done. + +## Configuration + +Use the following configuration in the `/etc/postfix/main.cf` file. + +For TLS configuration, you can look at the +[used configuration](https://github.com/modoboa/modoboa-installer/blob/master/modoboa_installer/scripts/files/postfix/main.cf.tpl) + +for [modoboa installer](https://github.com/modoboa/modoboa-installer). (this is just one possible configuration): + +```txt +# Stuff before +unknown_local_recipient_reject_code = 550 +unverified_recipient_reject_code = 550 + +# appending .domain is the MUA's job. +append_dot_mydomain = no + +## Proxy maps +proxy_read_maps = + proxy:unix:passwd.byname + proxy::/etc/postfix/sql-domains.cf + proxy::/etc/postfix/sql-domain-aliases.cf + proxy::/etc/postfix/sql-aliases.cf + proxy::/etc/postfix/sql-relaydomains.cf + proxy::/etc/postfix/sql-maintain.cf + proxy::/etc/postfix/sql-relay-recipient-verification.cf + proxy::/etc/postfix/sql-sender-login-map.cf + proxy::/etc/postfix/sql-spliteddomains-transport.cf + proxy::/etc/postfix/sql-transport.cf + +## Virtual transport settings +virtual_transport = lmtp:unix:private/dovecot-lmtp + +virtual_mailbox_domains = proxy::/etc/postfix/sql-domains.cf +virtual_alias_domains = proxy::/etc/postfix/sql-domain-aliases.cf +virtual_alias_maps = proxy::/etc/postfix/sql-aliases.cf + +## Relay domains +relay_domains = proxy::/etc/postfix/sql-relaydomains.cf +transport_maps = + proxy::/etc/postfix/sql-transport.cf + proxy::/etc/postfix/sql-spliteddomains-transport.cf + +smtpd_recipient_restrictions = + permit_mynetworks + permit_sasl_authenticated + check_recipient_access + proxy::/etc/postfix/sql-maintain.cf + proxy::/etc/postfix/sql-relay-recipient-verification.cf + reject_unverified_recipient + reject_unauth_destination + reject_non_fqdn_sender + reject_non_fqdn_recipient + reject_non_fqdn_helo_hostname + +smtpd_sender_login_maps = proxy::/etc/postfix/sql-sender-login-map.cf + +# Stuff after +``` + +Replace `` by the name of the database you use, and `Restart Postfix`. + +## Policy daemon + +If you want to enable the built-in policy daemon, add the following +content to the `/etc/postfix/main.cf` file: + +```txt{3} + smtpd_recipient_restrictions = + # ... + check_policy_service inet:localhost:9999 + # ... +``` + +And `reload postfix`. + +::: warning +The `check_policy_service` line must be placed before the +`permit_mynetworks` one, otherwise the daemon won't be called. +::: diff --git a/doc-ng/installation/radicale.md b/doc-ng/installation/radicale.md new file mode 100644 index 000000000..e89d4f756 --- /dev/null +++ b/doc-ng/installation/radicale.md @@ -0,0 +1,142 @@ +# Radicale + +This page is displaying how to install and use radicale when either +having installed modoboa manually or with the dedicated installer. + +## OAuth 2 authentication + +::: tip +These steps are automatic if you use an up-to-date modoboa-installer to +install/upgrade. +::: + +### Preparing the usage of radicale (manual install only) + +::: warning +It is assumed you have not installed in the past radicale from your +package manager and your modoboa installation is working correctly. + +If so, you may skip this section and proceed to the next one. +::: + +You will need +to create directories for the radicale user, needed files and set-up the +account (location may change depending on your OS and initial set-up). + +``` shell +$ sudo mkdir /var/lib/radicale /etc/radicale +$ sudo touch /etc/radicale/{rights,config} +$ sudo adduser --system --home /var/lib/radicale/ --no-create-home --gecos "Radicale CalDAV server" --group --disabled-password --quiet radicale +$ sudo chown -R radicale /etc/radicale +$ sudo chown /var/lib/radicale +``` + +### Creation of the virtual environment (manual install only) + +``` shell +$ sudo -u radicale virtualenv /var/lib/radicale/env +``` + +### Installing radicale oauth plugin + +You need to install `radicale-modoboa-auth-oauth2` python package inside +radicale venv. If you used the installer you can use the following +commands: + +``` shell +$ sudo -u radicale -i bash +$ cd ~ +$ env/bin/pip install radicale-modoboa-auth-oauth2 +``` + +For manual installations, this will do the job: + +``` shell +$ sudo -u radicale /var/lib/radicale/env/bin/pip install radicale-modoboa-auth-oauth2 +``` + +Please note because the plugin offered by modoboa's team is listing +radicale as a dependency you do not need to explicitely install +radicale. + +### Oauth application creation + +You first need to register Radicale as an authorized consumer of the +OAuth2 authentication service provided by Modoboa. To do so, create an +application with the following commands: + +``` shell +$ cd +$ python manage.py createapplication --name=Radicale --skip-authorization --client-id=radicale confidential client-credentials +``` + +On success, you should see an output similar to: + +```console +New application Radicale created successfully. +client_secret: XXXX +``` + +To enable OAuth2 authentication in Radicale, edit the `/etc/radicale/config` file +and update the [type] in [\[auth\]] to: +```txt +type = radicale_modoboa_auth_oauth2 +```` + +Then, add the following line in \`\[auth\]\`: + +```txt +oauth2_introspection_endpoint = https://radicale:@/api/o/introspect/ +```` + +Replace `` with the value you obtained earlier. + +Your `/etc/radicale/config`{.interpreted-text role="file"} should look +like this (listening address and port may vary): + +```ini +[server] +hosts = 0.0.0.0:5232,[::]:5232 + +[auth] +type = radicale_modoboa_auth_oauth2 +oauth2_introspection_endpoint = https://radicale:@/api/o/introspect/ + +[rights] +type = from_file +file = /etc/radicale/rights + +[storage] +filesystem_folder = /var/lib/radicale/collections +```` + +With that set-up, radicale should be working when managing calendars +through modoboa's web interface but [not]{#not}\_ when using other +clients like Thunderbird. + +### Modifications for using external clients (optional) + +To ensure other clients can identify, you need to modify radicale by +adding in the config file under the \[auth\] section: + +```txt +dovecot_socket = /run/dovecot/auth-radicale +``` + +and in `10-master.conf` for dovecot: + +```txt +unix_listener auth-radicale { + mode = 0660 + user = radicale + group = dovecot +} +``` + +Then, restart both `modoboa` and `radicale` to enjoy managing calendars (ICS type) with your favorite client. + +### Reverse proxy for radicale (optional) + +At this stage, no encryption is provided, and you may use a reverse proxy as described in + +[Radicale's documentation](https://radicale.org/v3.html#reverse-proxy). \ No newline at end of file diff --git a/doc-ng/installation/requirements.md b/doc-ng/installation/requirements.md new file mode 100644 index 000000000..b8a26c5da --- /dev/null +++ b/doc-ng/installation/requirements.md @@ -0,0 +1,26 @@ +# Requirements + +You will need a Server to perform well with at least: + +- CPU: 2 +- RAM: 2GB +- Disk: 10GB +- Python 3.8+ +- Postgres 12+ or MariaDB 10.4+ or MySQL 8+ or sqlite3 or LDAP + +::: warning + +**👀 Heads up for modoboa versions 1.15 and later** + +Python 2 support has been dropped with modoboa version 1.15. If you +still have Python 2 installed on your system either uninstall it or +force modoboa user to run with Python 3. +::: + +If you want to run virus scan on your server you will need **4GB** of +RAM. + +You have 2 possibilities to install Modoboa + +* [Manual Installation](manual) +* [Automatic Install](installer) \ No newline at end of file diff --git a/doc-ng/installation/upgrade.md b/doc-ng/installation/upgrade.md new file mode 100644 index 000000000..95f03c1ec --- /dev/null +++ b/doc-ng/installation/upgrade.md @@ -0,0 +1,2113 @@ +# Upgrade + +## Modoboa + +::: tip +In this doc, `by default` means that you used [modoboa installer](https://github.com/modoboa/modoboa-installer) +to install modoboa and that your didn't change your configuration. +::: + +::: warning +The new version you are going to install may need to modify your database. + +Before you start, make sure to backup everything !!! +:::: + +Most of the time, upgrading your installation to a newer Modoboa version only requires a few actions. + +In every case, you will need to apply the specific actions if the version you are installing requires it, +and then apply the general ones. + +In case you use a dedicated user and/or a virtualenv, do not forget to use them: + +``` bash +$ sudo -u -i bash +$ source /bin/activate +``` + +::: info Be Careful +Check the `specific_upgrade_instructions` +before running the following commands. +::: + +Then, run the following commands: + +``` shell +$ pip install modoboa== +$ cd +$ python manage.py migrate +$ python manage.py collectstatic +$ python manage.py check --deploy +$ python manage.py load_initial_data +``` + +Then when everything has been updated, restart modoboa's services: + +``` shell +$ sudo systemctl restart uwsgi +$ sudo systemctl restart supervisor +``` + +Finally, restart your web server. + +Sometimes, you might need to upgrade postfix map files too. + +To do so, just run the `generate_postfix_maps` command on the same directory than +the one used for installation (`/etc/postfix` by default). + +Make sure to use root privileges and run the following command: + +``` bash +> python manage.py generate_postfix_maps --destdir +``` + +Then, `reload postfix`. + +## Modoboa installer + +::: warning +It is important to keep the installer updated because it works on rolling release. +::: + +To do that, go to modoboa-installer directory then: + +``` shell +$ git fetch && git pull +``` + +If some error appears, you may try this: + +``` shell +$ git reset --hard origin/master +``` + +## Extensions + +::: warning +Do not open an issue after an upgrade if you have not updated your extensions ! +::: + +If a new version is available for an extension you're using, it is recommanded to install it. + +It may even be required. Upgrading an extensions is pretty and the procedure is almost +the same than the one used for Modoboa. + +In case you use a dedicated user and/or a virtualenv, **do not forget** to use them: + +``` bash +$ sudo -u -i bash +$ source /bin/activate +``` + +Then, run the following commands: + +``` bash +$ pip install == +$ cd +$ python manage.py migrate +$ python manage.py collectstatic +$ python manage.py check --deploy +``` + +Finally, restart your web server. + +It is a generic upgrade procedure which will be enough most of the time +but it is generally a good idea to check the associated documentation. + +## Rebuild Virtual Environment + +If you need to rebuild you virtualenv, you can check this [part of the documentation](/rebuild_venv) + +## Specific instructions + +The section below containt specific migration steps to upgrade Modoboa instance. + +## Version 2.6.1 + +### Required changes to `settings.py` + +* Add the following lines to `LOGGING` variable: + +``` python +'handlers': { + ... + 'syslog': { + 'class': 'logging.handlers.SysLogHandler', + 'facility': SysLogHandler.LOG_SYSLOG, + 'formatter': 'syslog' + }, + ... +}, +'loggers': { + ... + 'modoboa.dns': { + 'handlers': ['syslog'], + 'level': 'INFO', + 'propagate': False + }, + ... +} +``` + +## Version 2.6.0 + +New built-in auto-configuration service. + +### Required changes to `settings.py` + +* Add the following line to `MODOBOA_APPS` variable: + + ``` python + 'modoboa.autoconfig' + ``` + +* Add the following hostnames to `ALLOWED_HOSTS` variable: + + ``` python + 'autoconfig.', + 'autodiscover.', + ``` + +* Add the following variable: + + ``` python + EMAIL_CLIENT_CONNECTION_SETTINGS = { + "imap": { + "HOSTNAME": "", + "SOCKET_TYPE": "SSL", + "PORT": 993, + }, + "smtp": {"HOSTNAME": "", "SOCKET_TYPE": "STARTTLS", "PORT": 587}, + } + ``` + +* Remove `rest_framework.authentication.SessionAuthentication` from + `REST_FRAMEWORK` variable: + + ``` python + REST_FRAMEWORK = { + # Stuff before... + "DEFAULT_AUTHENTICATION_CLASSES": ( + "oauth2_provider.contrib.rest_framework.OAuth2Authentication", + "rest_framework.authentication.TokenAuthentication", + ), + # Stuff after... + } + ``` + +## Version 2.5.0 + +Amavis plugin integrated into Modoboa core repository. + +If you are using Amavis content filter and the quarantine plugin, +the following upgrade instructions, otherwise proceed with regular upgrade instructions. + +### Pre update + +* If you are using Postegresql (default option with the installer), run + as a root user: + + ``` bash + $ sudo -u postgres -i psql -d "modoboa" -c "UPDATE django_content_type SET app_label='amavis' WHERE app_label='modoboa_amavis'" + $ sudo -u postgres -i psql -d "modoboa" -c "UPDATE django_migrations SET app='amavis' WHERE app='modoboa_amavis'" + ``` + +* If you are using Mysql, run as a root user: + + ``` bash + $ echo "UPDATE django_content_type SET app_label='amavis' WHERE app_label='modoboa_amavis'" | mysql -u root -p modoboa + $ echo "UPDATE django_migrations SET app='amavis' WHERE app='modoboa_amavis'" | mysql -u root -p modoboa + ``` + +### Required changes to `settings.py` + +* Rename the following extensions in the `MODOBOA_APPS` variable: + + - `modoboa_amavis` -\> `modoboa.amavis` + +* Add the following variables: + + ``` python + DATABASE_ROUTERS = ["modoboa.amavis.dbrouter.AmavisRouter"] + + AMAVIS_DEFAULT_DATABASE_ENCODING = "LATIN1" # or any value matching your database config + ``` + +* Remove configuration block related to `modoboa_amavis` plugin (these + should be at the end of the file) + +### Post update + +* Run the following command: + ```shell + $ pip uninstall modoboa_amavis + ``` + +## Version 2.4.2 + +### Changes to `settings.py` + +- Make sure `MODOBOA_APPS` variable contains the following line: + + ``` python + 'modoboa.webmail' + ``` + +## Version 2.4.0 + +::: warning +The `modoboa-amavis` extension has not been migrated into the core Modoboa package yet. + +If you are using it, **you cannot install version 2.4.0 yet**. +::: + +::: tip INFORMATION +Legacy web interface has been removed and almost all extensions have been integrated to the core Modoboa package. +::: + +### Pre update + +* If you are using Postegresql (default option with the installer), run + as a root user: + + ``` shell + $ sudo -u postgres -i psql -d "modoboa" -c "UPDATE django_content_type SET app_label='autoreply' WHERE app_label='postfix_autoreply'" + $ sudo -u postgres -i psql -d "modoboa" -c "UPDATE django_migrations SET app='autoreply' WHERE app='postfix_autoreply'" + $ sudo -u postgres -i psql -d "modoboa" -c "UPDATE django_content_type SET app_label='contacts' WHERE app_label='modoboa_contacts'" + $ sudo -u postgres -i psql -d "modoboa" -c "UPDATE django_migrations SET app='contacts' WHERE app='modoboa_contacts'" + $ sudo -u postgres -i psql -d "modoboa" -c "UPDATE django_content_type SET app_label='calendars' WHERE app_label='modoboa_radicale'" + $ sudo -u postgres -i psql -d "modoboa" -c "UPDATE django_migrations SET app='calendars' WHERE app='modoboa_radicale'" + ``` + +* If you are using Mysql, run as a root user: + + ``` bash + $ echo "UPDATE django_content_type SET app_label='autoreply' WHERE app_label='postfix_autoreply'" | mysql -u root -p modoboa + $ echo "UPDATE django_migrations SET app='autoreply' WHERE app='postfix_autoreply'" | mysql -u root -p modoboa + $ echo "UPDATE django_content_type SET app_label='contacts' WHERE app_label='modoboa_contacts'" | mysql -u root -p modoboa + $ echo "UPDATE django_migrations SET app='contacts' WHERE app='modoboa_contacts'" | mysql -u root -p modoboa + $ echo "UPDATE django_content_type SET app_label='calendars' WHERE app_label='modoboa_radicale'" | mysql -u root -p modoboa + $ echo "UPDATE django_migrations SET app='calendars' WHERE app='modoboa_radicale'" | mysql -u root -p modoboa + ``` + +::: warning +MariaDB 10.7+ users might have login issues (see this [discussion](https://github.com/modoboa/modoboa/discussions/3582)). + +For those impacted, run the followed SQL queries: + +``` shell +$ echo "ALTER TABLE `oauth2_provider_idtoken` MODIFY `jti` uuid;" | mysql -u root -p modoboa +$ echo "ALTER TABLE `oauth2_provider_refreshtoken` MODIFY `token_family` uuid;" | mysql -u root -p modoboa +``` +::: + +### Required changes to `settings.py` + +* Rename the following extensions in the `MODOBOA_APPS` variable: + + * `modoboa.postfix_autoreply` -\> `modoboa.autoreply` + * `modoboa_contacts` -\> `modoboa.contacts` + * `modoboa_radicale` -\> `modoboa.calendars` + * `modoboa_webmail` -\> `modoboa.webmail` + * `modoboa_rspamd` -\> `modoboa.rspamd` + +* Remove configuration blocks related to `modoboa_contacts` and `modoboa_radicale` plugins (these should be at the end of the file) + +* Remove `AjaxLoginRedirect` from `MIDDLEWARE` variable: + + ``` python + MIDDLEWARE = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', + 'x_forwarded_for.middleware.XForwardedForMiddleware', + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'modoboa.core.middleware.LocalConfigMiddleware', + 'modoboa.lib.middleware.CommonExceptionCatcher', + 'modoboa.lib.middleware.RequestCatcherMiddleware', + ) + ``` + +* Remove `NEW_ADMIN_URL` variable if found + +* Remove `new_admin_url` and `top_notifications` context processors + +* Remove `ckeditor` related apps from `INSTALLED_APPS` variable and + `CKEDITOR_*` settings + +* Remove `STATICFILE_DIRS` variable + +### Radicale configuration changes + +You must enable oauth2 authentication in Radicale to make the calendar and contact feature work. + +Follow this `guide `. + +### Post update + +* Run the following command: + ``` shell + $ python manage.py load_initial_data + ```` +* Update your `webserver ` configuration + +* Remove `autoreply` service definition from postfix's `master.cf` file if you were using it + +* Make sure `/etc/uwsgi/sites-enabled/` contains `max-requests=5000` and that + `process` is superior or equal to 4. + + Don't forget to restart uwsgi service after the configuration change. + +* Then proceed with regular upgrade instructions + +## Version 2.3.3 + +::: warning +The `crypt ({CRYPT})` password scheme has been marked as deprecated and +will be removed from Modoboa starting with version **2.4.0**. + +If you are still using this weak scheme, you must change to something safer **ASAP**. + +Installations still using this scheme won't be able to install Modoboa 2.4 and upper... +::: + +Update your `settings.py` file with following content: + +``` python +SILENCED_SYSTEM_CHECKS = [ + "security.W019", # modoboa uses iframes to display e-mails + "ckeditor.W001", # CKEditor 4.22.1 warning +] +``` + +This will silence a [warning sent by django-ckeditor](https://github.com/modoboa/modoboa/issues/3316). + +## Version 2.3.0 + +::: warning +For this particular version, it is really important that you apply new migrations **AFTER** the following instructions. + +If you don't, you'll get into trouble\... +::: + +### Pre update + +::: tip INFORMATION +To check that you were actually using modoboa-postfix-autoreply you can +check if this command returns something: + +``` bash +$ cat /srv/modoboa/instance/instance/settings.py | grep "modoboa_postfix_autoreply" +``` +::: + +If you were using [modoboa-postfix-autoreply](https://github.com/modoboa/modoboa-postfix-autoreply) +extension, you need to perfome a few commands: + +* If you are using Postegresql (default option with the installer), run as a root user: + + ``` shell + $ sudo -u postgres -i psql -d "modoboa" -c "UPDATE django_content_type SET app_label='postfix_autoreply' WHERE app_label='modoboa_postfix_autoreply'" + $ sudo -u postgres -i psql -d "modoboa" -c "UPDATE django_migrations SET app='postfix_autoreply' WHERE app='modoboa_postfix_autoreply'" + ``` + +* If you are using Mysql, run as a root user: + + ``` shell + $ echo "UPDATE django_content_type SET app_label='postfix_autoreply' WHERE app_label='modoboa_postfix_autoreply'" | mysql -u root -p modoboa + $ echo "UPDATE django_migrations SET app='postfix_autoreply' WHERE app='modoboa_postfix_autoreply'" | mysql -u root -p modoboa + ``` + +### Required changes to `settings.py` + +Some changes are required to your `settings.py` file. + +These changes will not be performed by the installer. + +The location of this file is `/srv/modoboa/instance/instance` by default. + +* Append `'import environ'` after `'import os'`: + + ``` python + import os + import environ + ``` + +* Append the following lines after the line starting with `'BASE_DIR'`: + + ``` python + # Build paths inside the project like this: os.path.join(BASE_DIR, ...) + BASE_DIR = os.path.realpath(os.path.dirname(os.path.dirname(__file__))) + env = environ.Env() + environ.Env.read_env(os.path.join(BASE_DIR, '.env')) + ``` + +* Add `oauth2_provider` and `corsheaders` to the `INSTALLED_APPS` list: + + ``` python + INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.sites', + 'django.contrib.staticfiles', + 'reversion', + 'ckeditor', + 'ckeditor_uploader', + 'oauth2_provider', # [!code ++] + 'corsheaders', # [!code ++] + 'rest_framework', + 'rest_framework.authtoken', + 'drf_spectacular', + 'phonenumber_field', + 'django_otp', + 'django_otp.plugins.otp_totp', + 'django_otp.plugins.otp_static', + 'django_rename_app', + 'django_rq', + ) + ``` + +* The `modoboa-postfix-autoreply` and `modoboa-sievefilters` plugins have been merged into the core. + + Add `'modoboa.postfix_autoreply'` and `modoboa.sievefilters` to `MODOBOA_APPS`: + + ``` python + MODOBOA_APPS = ( + 'modoboa', + 'modoboa.core', + 'modoboa.lib', + 'modoboa.admin', + 'modoboa.transport', + 'modoboa.relaydomains', + 'modoboa.limits', + 'modoboa.parameters', + 'modoboa.dnstools', + 'modoboa.policyd', + 'modoboa.maillog', + 'modoboa.dmarc', + 'modoboa.pdfcredentials', + 'modoboa.imap_migration', + 'modoboa_postfix_autoreply', # [!code --] + 'modoboa_sievefilters', # [!code --] + 'modoboa.postfix_autoreply', # [!code ++] + 'modoboa.sievefilters', # [!code ++] + ) + ``` + + ::: warning + Remove any reference to `'modoboa_postfix_autoreply'` AND `modoboa_sievefilters` in this same variable. + ::: + +* Due to the changes to the authentication flow, you need to replace MIDDLEWARE with the following: + + ``` python + MIDDLEWARE = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', + 'x_forwarded_for.middleware.XForwardedForMiddleware', + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'modoboa.core.middleware.LocalConfigMiddleware', + 'modoboa.lib.middleware.AjaxLoginRedirect', + 'modoboa.lib.middleware.CommonExceptionCatcher', + 'modoboa.lib.middleware.RequestCatcherMiddleware', + ) + ``` + +* Replace `'modoboa.core.drf_authentication.JWTAuthenticationWith2FA'` by `'oauth2_provider.contrib.rest_framework.OAuth2Authentication'` + at the top of the list of `DEFAULT_AUTHENTICATION_CLASSES` (inside `REST_FRAMEWORK` section): + + ``` python + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'modoboa.core.drf_authentication.JWTAuthenticationWith2FA', # [!code --] + 'oauth2_provider.contrib.rest_framework.OAuth2Authentication', # [!code ++] + 'rest_framework.authentication.TokenAuthentication', + 'rest_framework.authentication.SessionAuthentication', + ), + ``` + +Save your modifications at this point and run the `post upgrade commands ` now. + +Then, apply this last modification to your settings: + +* Run the commands indicated `in the first section ` then add the following content after `MEDIA_ROOT`: + + ``` python + # oAuth2 settings + OAUTH2_PROVIDER = { + 'OIDC_ENABLED': True, + 'OIDC_RP_INITIATED_LOGOUT_ENABLED': True, + 'OIDC_RP_INITIATED_LOGOUT_ALWAYS_PROMPT': True, + 'OIDC_RSA_PRIVATE_KEY': env.str('OIDC_RSA_PRIVATE_KEY', multiline=True), + 'SCOPES': { + 'openid': 'OpenID Connect scope', + 'read': 'Read scope', + 'write': 'Write scope', + 'introspection': 'Introspect token scope', + }, + 'DEFAULT_SCOPES': ['openid', 'read', 'write'], + } + + # If CORS fail, you might want to try to set it to True + #CORS_ORIGIN_ALLOW_ALL = False + ``` + +### Docevot configuration changes + +You must enable oauth2 authentication in Dovecot to make the filters editor work in the new UI. +Follow this [guide](https://doc.dovecot.org/2.4.2/core/config/auth/databases/oauth2.html). + +### Post update + +Run this command to be sure to have everything set. + +``` bash +$ python manage.py modo repair +``` + +You need to set the `buffer-size` to `8192` in uwsgi config. + +Edit the value in `/etc/uwsgi/apps-available/automx_instance.ini` +to match at least 8192 or add it if it was not set: + +``` ini +buffer-size = 8192 +``` + +### SORBS DNS + +::: tip INFORMATION +The installer will take care of that if you use it to upgrade. +::: + +::: warning +[SORBS DNS has been deprecated](https://www.mail-archive.com/mailop@mailop.org/msg22064.html). + +If you were using them with postfix, you need to update your configuration by removing any reference to it. + +For example: + +``` txt +# Use some DNSBL +postscreen_dnsbl_sites = + zen.spamhaus.org=127.0.0.[2..11]*3 + bl.spameatingmonkey.net=127.0.0.2*2 + bl.spamcop.net=127.0.0.2 +``` +::: + +## Version 2.2.3 + +* Reduced the number of call to doveadm + +You need to setup a new worker : + +1. Create a new supervisord config (`/etc/supervisor/conf.d/modoboa-base-worker.conf` by default) : + +``` ini +[program:modoboa-base-worker] +autostart=true +autorestart=true +command={%python env path%} {% manage.py instance path%} rqworker modoboa +directory={%modoboa home dir%} +user=%{modoboa user} +numprocs=1 +stopsignal=TERM +``` + +* `python env path` : Python executable located in your virtual environment created for modoboa. +You will find it here `/srv/modoboa/venv/bin/python` by default. +* `manage.py instance path`: Path to manage.py of your modoboa instance. + You will find it here : `/srv/modoboa/instance/manage.py`by default. +* `Modoboa home dir`: Home dir of the user running modooba. + You will find it here `/srv/modoboa/` by default. +* `modoboa user` : User managing dkim signing (modoboa by default). + +You can help you with `/etc/supervisor/conf.d/policyd.conf` (by default). + +Then restart supervisor. + +```shell +$ supervisorctl reread && supervisorctl update` on Debian. +``` + +2. Update the RQ_QUEUES section bellow the `#REDIS` section: + +``` python +# RQ +RQ_QUEUES = { + ... + }, + 'modoboa': { + 'HOST': REDIS_HOST, + 'PORT': REDIS_PORT, + 'DB': 0, + }, +} +``` +3. Add the CACHE section bellow the `# RQ` one: + +``` python +# CACHE +CACHES = { + "default": { + "BACKEND": 'django.core.cache.backends.redis.RedisCache', + "LOCATION": REDIS_URL + } +} +``` + +## Version 2.2.0 + +::: tip INFORMATION +If you use the installer to perform the upgrade, you NEED to update it. +See above for instructions. After updating the installer and modoboa +with it, you only need to edit `urls.py` and `settings.py` +::: + +* Django has been updated to 4.2LTS. Please upgrade all your extensions alongside modoboa +* Support for Python 3.7 has been dropped, minimum Python version is now 3.8 +* Support for Postgres 11 has been dropped, minimum Postgres version is now 12 + +You need to change the first line in `urls.py` and replace +[url] with [path] (in `/srv/modoboa/instance/instance/urls.py` by default): + +``` python +# from django.conf.urls import include, url +from django.urls import include, path + +urlpatterns = [ + path(r'', include('modoboa.urls')), +] +``` + +::: tip +If you use Postgresql, you need to install pyscopg3+: + +``` bash +pip install psycopg[binary]>=3.1 +``` +::: + +RQ has been added. This should replace the use of cron jobs in the +future. + +For now, only `manage_dkim_keys` has been migrated. + +This will make the dkim key generation asynchronous +but the task will be started as soon as the generation is required. + +Follow these instructions to perform the update in case you used +supervisord for the installation (this apply if you used `modoboa-installer`): + +1. Edit settings.py (`/srv/modoboa/instance/instance/settings.py` by default) and add: + +``` python +INSTALLED_APPS = ( + ..., + 'django_rq', +) +``` + +and add the RQ section bellow the `#REDIS` section: + +``` python +# RQ +RQ_QUEUES = { + 'dkim': { + 'HOST': REDIS_HOST, + 'PORT': REDIS_PORT, + 'DB': 0, + }, +} +``` + +Then by default you will need to restart `uwsgi` service. + +```shell +$ systemctl restart uwsgi +``` + +2. Create a new supervisord config (`/etc/supervisor/conf.d/modoboa-worker.conf` by default) : + +``` ini +[program:modoboa-worker] +autostart=true +autorestart=true +command={%python env path%} {% manage.py instance path%} rqworker dkim +directory={%modoboa home dir%} +user=%{dkim manager user} +numprocs=1 +stopsignal=TERM +``` + +* `python env path` : Python executable located in your virtual environment created for modoboa. + You will find it here `/srv/modoboa/venv/bin/python` by default. +* `manage.py instance path`: Path to manage.py of your modoboa instance. + You will find it here : `/srv/modoboa/instance/manage.py`by default. +* `Modoboa home dir`: Home dir of the user running modooba. + You will find it here `/srv/modoboa/` by default. +* `dkim manager user` : User managing dkim signing (opendkim by default). + +You can help you with `/etc/supervisor/conf.d/policyd.conf` (by default). + +Then restart supervisor. + +```shell +$ supervisorctl reread && supervisorctl update +``` + +Admins now have the option to setup a send-only mailbox. + +Send-only mailboxes do not have access to IMAP. + +You need to change your dovecot configuration to enable it. + +::: warning +modoboa-webmail may not work with send-only user for now. +::: + +If you use `Postgres`: Change `user_query` and `password_query` in `/etc/dovecot/dovecot-sql.conf.ext`: + +```sql +user_query = SELECT '%{home_dir}/%%d/%%n' AS home, %mailboxes_owner_uid as uid, %mailboxes_owner_gid as gid, '*:bytes=' || mb.quota || 'M' AS quota_rule FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN core_user u ON u.id=mb.user_id WHERE (mb.is_send_only IS NOT TRUE OR '%s' NOT IN ('imap', 'pop3', 'lmtp')) AND mb.address='%%n' AND dom.name='%%d' + +password_query = SELECT email AS user, password, '%{home_dir}/%%d/%%n' AS userdb_home, %mailboxes_owner_uid AS userdb_uid, %mailboxes_owner_gid AS userdb_gid, CONCAT('*:bytes=', mb.quota, 'M') AS userdb_quota_rule FROM core_user u INNER JOIN admin_mailbox mb ON u.id=mb.user_id INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE (mb.is_send_only IS NOT TRUE OR '%s' NOT IN ('imap', 'pop3')) AND email='%%u' AND is_active AND dom.enabled +``` + +You basically simply need: + +* add `(mb.is_send_only IS NOT TRUE OR '%s' NOT IN ('imap', 'pop3', 'lmtp')) AND` for `user_query` after `WHERE` +* add`(mb.is_send_only IS NOT TRUE OR '%s' NOT IN ('imap', 'pop3')) AND` for `password_query` after `WHERE`. + +If you use `MySQL`: Change `user_query` and `password_query` in `/etc/dovecot/dovecot-sql-master.ext`: + +```sql + user_query = SELECT '%{home_dir}/%%d/%%n' AS home, %mailboxes_owner_uid as uid, %mailboxes_owner_gid as gid, CONCAT('*:bytes=', mb.quota, 'M') AS quota_rule FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN core_user u ON u.id=mb.user_id WHERE (mb.is_send_only=0 OR '%s' NOT IN ('imap', 'pop3', 'lmtp')) AND mb.address='%%n' AND dom.name='%%d' + + password_query = SELECT email AS user, password, '%{home_dir}/%%d/%%n' AS userdb_home, %mailboxes_owner_uid AS userdb_uid, %mailboxes_owner_gid AS userdb_gid, CONCAT('*:bytes=', mb.quota, 'M') AS userdb_quota_rule FROM core_user u INNER JOIN admin_mailbox mb ON u.id=mb.user_id INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE (mb.is_send_only=0 OR '%s' NOT IN ('imap', 'pop3')) AND u.email='%%u' AND u.is_active=1 AND dom.enabled=1 +``` + +You basically simply need +* add `(mb.is_send_only=0 OR '%s' NOT IN ('imap', 'pop3', 'lmtp')) AND` for `user_query` after `WHERE` +* `(mb.is_send_only=0 OR '%s' NOT IN ('imap', 'pop3'))` for `password_query` after `WHERE`. + +## Version 2.1.0 + +::: warning +For this particular version, it is really important that you apply new +migrations **AFTER** the following instructions. If you don\'t, you\'ll +get into trouble\... +::: + +The `modoboa-dmarc`, `modoboa-pdfcredentials` and `modoboa-imapmigration` plugins have been merged into the core. + +Add `'django_rename_app,'` To `INSTALLED_APPS`: + +``` python +INSTALLED_APPS = ( +..., +'django_rename_app', # [!code ++] +) +``` + +Add `'modoboa.dmarc'`, `'modoboa.pdfcredentials'` and `'modoboa.imap_migration'` to `MODOBOA_APPS`: + +``` python +MODOBOA_APPS = ( + 'modoboa', + 'modoboa.core', + 'modoboa.lib', + 'modoboa.admin', + 'modoboa.transport', + 'modoboa.relaydomains', + 'modoboa.limits', + 'modoboa.parameters', + 'modoboa.dnstools', + 'modoboa.policyd', + 'modoboa.maillog', + 'modoboa_dmarc', # [!code --] + 'modoboa_pdfcredentials', # [!code --] + 'modoboa_imap_migration', # [!code --] + 'modoboa.dmarc', # [!code ++] + 'modoboa.pdfcredentials', # [!code ++] + 'modoboa.imap_migration', # [!code ++] +) +``` + +And remove any reference to `'modoboa_dmarc'`, `'modoboa_pdfcredentials'` or `'modoboa_imap_migration'` in this samevariable. + +You need to add `'modoboa.imap_migration.auth_backends.IMAPBackend',` at the end of `AUTHENTICATION_BACKENDS`: + +``` python +AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.ModelBackend', + 'modoboa.imap_migration.auth_backends.IMAPBackend', # [!code ++] +) +``` + +::: info +It won't affect login if you don't use `imap_migration` feature. + +It is therefor highly recommended to add this authentication backend. +::: + +After upgrading modoboa, run the following commands from your virtual +environment: + +``` shell +$ python manage.py rename_app modoboa_dmarc dmarc +$ python manage.py rename_app modoboa_imap_migration imap_migration +``` + +## Version 2.0.4 + +The following modifications must be applied to the `settings.py` file: + +* Add `'modoboa.core.context_processors.new_admin_url',` at the end of [context_processors] list: + + ``` python + TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.template.context_processors.i18n', + 'django.template.context_processors.media', + 'django.template.context_processors.static', + 'django.template.context_processors.tz', + 'django.contrib.messages.context_processors.messages', + 'modoboa.core.context_processors.top_notifications', + 'modoboa.core.context_processors.new_admin_url', # [!code ++] + ], + 'debug': False, + }, + }, + ] + ``` + +* Add the following variable: + +``` python +PID_FILE_STORAGE_PATH = '/var/run' +``` + +* Update `LOGGING` variable to define an address for `syslog-auth` handler: + + ``` python{5} + 'syslog-auth': { + 'class': 'logging.handlers.SysLogHandler', + 'facility': SysLogHandler.LOG_AUTH, + 'formatter': 'syslog', + 'address': '/dev/log' + }, + ``` + +* You now have the possibility to customize the url of the new-admin interface: + To do so please head up to `the custom configuration chapter ` (advanced user). + +* Add `DEFAULT_THROTTLE_RATES` to `REST_FRAMEWORK`: + +``` python{2-10} +REST_FRAMEWORK = { + 'DEFAULT_THROTTLE_RATES': { + 'user': '300/minute', + 'ddos': '5/second', + 'ddos_lesser': '200/minute', + 'login': '10/minute', + 'password_recovery_request': '12/hour', + 'password_recovery_totp_check': '25/hour', + 'password_recovery_apply': '25/hour' + }, + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'modoboa.core.drf_authentication.JWTAuthenticationWith2FA', + 'rest_framework.authentication.TokenAuthentication', + 'rest_framework.authentication.SessionAuthentication', + ), + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', + 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning', +} +``` + +* You can edit the `DEFAULT_THROTTLE_RATES` to whatever value suits you: + + * `user` is for every endpoint, it is per user or per ip if not logged. + * `ddos` is per api endpoint and per user or per ip if not logged. + * `ddos_lesser` is for per api endpoint and per user or per ip if not logged. + This is for api endpoint that arelighter. + * `login` the number of time an ip can attempt to log. + The counter will reset on login success. + * `password_*` is for the recovery, it is divided per step in the recovery process. + +## Version 2.0.3 + +Update the new following setting in the `settings.py` file: + +``` python +DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' +``` + +## Version 2.0.0 + +Add `drf_spectacular` and `phonenumber_field` to `INSTALLED_APPS` in the `settings.py` file, as follows: + +``` python +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.sites', + 'django.contrib.staticfiles', + 'reversion', + 'ckeditor', + 'ckeditor_uploader', + 'rest_framework', + 'rest_framework.authtoken', + 'drf_spectacular', # [!code ++] + 'phonenumber_field', # [!code ++] + 'django_otp', + 'django_otp.plugins.otp_totp', + 'django_otp.plugins.otp_static', +) +``` + +Modify the `REST_FRAMEWORK` setting as follows: + +``` python +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'modoboa.core.drf_authentication.JWTAuthenticationWith2FA', + 'rest_framework.authentication.TokenAuthentication', + 'rest_framework.authentication.SessionAuthentication', + ), + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', + 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning', +} +``` + +Add the new following settings: + +``` python +SPECTACULAR_SETTINGS = { + 'SCHEMA_PATH_PREFIX': r'/api/v[0-9]', + 'TITLE': 'Modoboa API', + 'VERSION': None, + 'SERVE_PERMISSIONS': ['rest_framework.permissions.IsAuthenticated'], +} + +PHONENUMBER_DB_FORMAT = 'INTERNATIONAL' + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +``` + +#### Migration issue for Postgres/OpenDKIM users + +The [migration will probably fail](https://github.com/modoboa/modoboa/issues/2508) +because of the additional view created for OpenDKIM. + +To make it work, you first need to drop the view: + +``` shell +$ sudo su - postgres +$ psql +psql (12.10 (Ubuntu 12.10-0ubuntu0.20.04.1)) +Type "help" for help. + +postgres=# \c modoboa +You are now connected to database "modoboa" as user "postgres". +modoboa-# \d+ dkim + View "public.dkim" + Column | Type | Collation | Nullable | Default | Storage | Description +------------------+------------------------+-----------+----------+---------+----------+------------- + id | integer | | | | plain | + domain_name | character varying(100) | | | | extended | + private_key_path | character varying(254) | | | | extended | + selector | character varying(30) | | | | extended | + +View definition: + SELECT admin_domain.id, + admin_domain.name AS domain_name, + admin_domain.dkim_private_key_path AS private_key_path, + admin_domain.dkim_key_selector AS selector + FROM admin_domain + WHERE admin_domain.enable_dkim; + +modoboa=# drop view dkim; +DROP VIEW +``` + +Then, run the migration as usual: + +``` bash +python manage.py migrate +``` + +Finally, recreate the view: + +``` bash +modoboa=# CREATE OR REPLACE VIEW dkim AS +modoboa-# SELECT admin_domain.id, +modoboa-# admin_domain.name AS domain_name, +modoboa-# admin_domain.dkim_private_key_path AS private_key_path, +modoboa-# admin_domain.dkim_key_selector AS selector +modoboa-# FROM admin_domain +modoboa-# WHERE admin_domain.enable_dkim; +CREATE VIEW +modoboa=# grant select on dkim to opendkim; +GRANT +``` + +#### New admin interface + +::: tip INFORMATION +This new release brings a new admin interface written with Vue.js framework. +::: + +It is a work in progress and all features are not yet +implemented - i.e. extensions integration - but you could give it a try. +It uses a new API version but the old one is still available. + +You will need to copy the frontend files in the folder you specified in +your web server configuration. + +If you used the installer, the folder should be `/srv/modoboa/instance/frontend`: + +``` bash +mkdir /srv/modoboa/instance/frontend +cp -r /srv/modoboa/env/lib/pythonX.X/site-packages/modoboa/frontend_dist/* /srv/modoboa/instance/frontend +``` + +Then, edit the `/srv/modoboa/instance/frontend/config.json` and update the `API_BASE_URL` +setting according to the hostname of your server: + +``` json +{ + "API_BASE_URL": "https:///api/v2" +} +``` + +Finally, update the configuration of your web server to serve the frontend files. + +For NGINX, you should add the following in the `server` block: + +``` nginx +location ^~ /new-admin { + alias /srv/modoboa/instance/frontend/; + index index.html; + + expires -1; + add_header Pragma "no-cache"; + add_header Cache-Control "no-store, no-cache, must-revalidate, post-check=0, pre-check=0"; + + try_files $uri $uri/ /index.html = 404; +} +``` + +## Version 1.17.0 + +Modoboa now supports Two-Factor authentication using TOTP applications. + +To enable it, install the following new requirement in your virtualenv: + +``` bash +(env)> pip install django-otp qrcode +``` + +Then, open the `settings.py` file and add `django_otp` packages to `INSTALLED_APPS`: + +``` python +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.sites', + 'django.contrib.staticfiles', + 'reversion', + 'ckeditor', + 'ckeditor_uploader', + 'rest_framework', + 'rest_framework.authtoken', + 'django_otp', # [!code ++] + 'django_otp.plugins.otp_totp', # [!code ++] + 'django_otp.plugins.otp_static', # [!code ++] +) +``` + +Add new middlewares to `MIDDLEWARE`. It has to be after the SessionMiddleware and before the CommonMiddleware: + +``` python +MIDDLEWARE = ( + 'x_forwarded_for.middleware.XForwardedForMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', # [!code ++] + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django_otp.middleware.OTPMiddleware', + 'modoboa.core.middleware.TwoFAMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'modoboa.core.middleware.LocalConfigMiddleware', + 'modoboa.lib.middleware.AjaxLoginRedirect', + 'modoboa.lib.middleware.CommonExceptionCatcher', + 'modoboa.lib.middleware.RequestCatcherMiddleware', +) +``` + +And add the following new setting: + +``` python +# 2FA +OTP_TOTP_ISSUER = "" +``` + +## Version 1.16.0 + +A new `policy daemon ` has been added. + +Make sure to have a Redis instance running on your server. + +Add `'modoboa.policyd'` to `MODOBOA_APPS`: + +``` python +MODOBOA_APPS = ( + 'modoboa', + 'modoboa.core', + 'modoboa.lib', + 'modoboa.admin', + 'modoboa.transport', + 'modoboa.relaydomains', + 'modoboa.limits', + 'modoboa.parameters', + 'modoboa.dnstools', + 'modoboa.policyd' # [!code ++] +) +``` + +Add the following settings to your `settings.py` file: + +::: code-group +``` python{1-3} [TCP/IP] +REDIS_HOST = '' +REDIS_PORT = 6379 +REDIS_QUOTA_DB = 0 +REDIS_URL = 'redis://{}:{}/{}'.format(REDIS_HOST, REDIS_PORT, REDIS_QUOTA_DB) +``` + +``` python [UNIX Socket] +REDIS_HOST = '/path/to/redis/socket' +REDIS_PORT = 6379 +REDIS_QUOTA_DB = 0 +REDIS_URL = 'unix://{}?db={}'.format(REDIS_HOST, REDIS_QUOTA_DB) +``` +::: + +Once done, you can start the policy daemon using the following commands: + +``` shell +$ python manage.py policy_daemon +``` + +Don't forget to configure `_policyd_config ` if you want to use this feature. + +The `modoboa-stats` plugin has been merged into the core. + +Add `'modoboa.maillog'` to `MODOBOA_APPS`: + +``` python +MODOBOA_APPS = ( + 'modoboa', + 'modoboa.core', + 'modoboa.lib', + 'modoboa.admin', + 'modoboa.transport', + 'modoboa.relaydomains', + 'modoboa.limits', + 'modoboa.parameters', + 'modoboa.dnstools', + 'modoboa.policyd', + 'modoboa_stats', # [!code --] + 'modoboa.maillog', # [!code ++] +) +``` + +And remove any reference to `modoboa_stats` in this same variable. + +## Version 1.15.0 + +::: tip IMPORTANT +This version drops Python 2 support so don't forget to update all the extensions you use. +::: + +::: warning +If you upgrade an existing python 2 installation, you will need to +create a new Python 3 virtual environment. + +You can remove the existing virtual environment and replace it by the new one +so you won't have to modify your configuration files. +::: + +Add the following new setting: + +``` python +DISABLE_DASHBOARD_EXTERNAL_QUERIES = False +``` + +Reload uwsgi/gunicorn/apache depending on your setup. + +Finally, Make sure to use root privileges and run the following command: + +``` shell +$ python manage.py generate_postfix_maps --destdir +``` + +Then, reload postfix. + +## Version 1.14.0 + +This release introduces an optional LDAP synchronization process. + +If you want to use it, please follow the `dedicated procedure` #ldap-sync . + +## Version 1.13.1 + +Upgrade postfix maps files as follows: + +``` shell +$ python manage.py generate_postfix_maps --destdir --force-overwrite +``` + +## Version 1.13.0 + +Add `'modoboa.dnstools'` to `MODOBOA_APPS`: + +``` python +MODOBOA_APPS = ( + 'modoboa', + 'modoboa.core', + 'modoboa.lib', + 'modoboa.admin', + 'modoboa.transport', + 'modoboa.relaydomains', + 'modoboa.limits', + 'modoboa.parameters', + 'modoboa.dnstools', # [!code ++] +) +``` + +Add the following new settings: + +``` python +CSRF_COOKIE_SECURE = True +SESSION_COOKIE_SECURE = True +``` + +### modoboa-postfix-autoreply 1.5.0 + +Edit the `/etc/postfix/main.cf` file and remove the `sql-autoreplies-transport.cf` map from the `transport_maps` if present. + +Remove the corresponding `proxy_read_maps` entry if relevant. + +Reload postfix. + +## Version 1.10.0 + +::: warning +Upgrade installed extensions BEFORE running `check` or `migrate` commands. +::: + +Upgrade all your installed plugins to the following versions: + +::: warning +If you use the amavis plugin, make sure to include its configuration as follows into `settings.py` : + +``` python +from modoboa_amavis import settings as modoboa_amavis_settings +modoboa_amavis_settings.apply(globals()) +``` +::: + +| Name | Version | +| :--- | :-----: | +| modoboa-amavis | 1.2.0 | +| modoboa-contacts | 0.5.0 | +| modoboa-dmarc | 1.1.0 | +| modoboa-imap-migration | 1.2.0 | +| modoboa-pdfcredentials | 1.3.0 | +| modoboa-postfix-autoreply | 1.4.0 | +| modoboa-radicale | 1.2.0 | +| modoboa-sievefilters | 1.4.0 | +| modoboa-stats | 1.4.0 | +| modoboa-webmail | 1.4.0 | + +Edit the `settings.py` file and apply the following modifications. + +Add `'modoboa.transport'` to `MODOBOA_APPS`: + +``` python +MODOBOA_APPS = ( + 'modoboa', + 'modoboa.core', + 'modoboa.lib', + 'modoboa.admin', + 'modoboa.transport', # [!code ++] + 'modoboa.relaydomains', + 'modoboa.limits', + 'modoboa.parameters', +) +``` + +Replace the following line: + +``` python +MIDDLEWARE_CLASSES = ( # [!code --] +MIDDLEWARE = ( # [!code ++] +``` + +Update postfix map files as follows: + +``` shell +$ rm -f /modoboa-postfix-maps.chk +$ python manage.py generate_postfix_maps --force --destdir +``` + +Then, modify postfix's configuration as follows: + +```txt +smtpd_sender_login_maps = + :/sql-sender-login-map.cf + +transport_maps = + :/sql-transport.cf + :/sql-spliteddomains-transport.cf + # other map files... +```` + +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](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: + +```txt +# Generate DKIM keys (they will belong to the user running this job) +* * * * * root $PYTHON $INSTANCE/manage.py modo manage_dkim_keys +``` + +## Version 1.9.0 + +If you want to manage inactive accounts, look at `inactive_accounts` . + +## Version 1.8.3 + +Edit the `settings.py` file and replace the following line: + +``` python +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) # [!code --] +BASE_DIR = os.path.realpath(os.path.dirname(os.path.dirname(__file__))) # [!code ++] +``` + +## Version 1.8.0 + +Modoboa now relies on [Django's builtin password validation system](https://docs.djangoproject.com/en/1.10/topics/auth/passwords/#module-django.contrib.auth.password_validation) +to validate user passwords, instead of `django-passwords`. + +Remove `django-passwords` from your system: + +``` shell +$ sudo -u -i bash +$ source /bin/activate +$ pip uninstall django-passwords +``` + +Edit the `settings.py` file and remove the following content: + +``` python +# django-passwords + +PASSWORD_MIN_LENGTH = 8 # [!code --] + +PASSWORD_COMPLEXITY = { # [!code --] + "UPPER": 1, # [!code --] + "LOWER": 1, # [!code --] + "DIGITS": 1 # [!code --] +} # [!code --] +``` + +Add the following lines: + +``` python +# Password validation rules +AUTH_PASSWORD_VALIDATORS = [ # [!code ++] + { # [!code ++] + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', # [!code ++] + }, # [!code ++] + { # [!code ++] + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', # [!code ++] + }, # [!code ++] + { # [!code ++] + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', # [!code ++] + }, # [!code ++] + { # [!code ++] + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', # [!code ++] + }, # [!code ++] + { # [!code ++] + 'NAME': 'modoboa.core.password_validation.ComplexityValidator', # [!code ++] + 'OPTIONS': { # [!code ++] + 'upper': 1, # [!code ++] + 'lower': 1, # [!code ++] + 'digits': 1, # [!code ++] + 'specials': 0 # [!code ++] + } # [!code ++] + }, # [!code ++] +] # [!code ++] +``` + +## Version 1.7.2 + +API documentation has evolved (because of the upgrade to Django Rest Framework 3.6) +and CKeditor is now embedded by default (thanks to the `django-ckeditor` package). + +Some configuration changes are required. + +Edit your `settings.py` file and apply the following modifications: + +* Update the `INSTALLED_APPS` variable as follows: + +``` python +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.sites', + 'django.contrib.staticfiles', + 'reversion', + 'ckeditor', # [!code ++] + 'ckeditor_uploader', # [!code ++] + 'rest_framework', # [!code ++] + 'rest_framework.authtoken', # [!code ++] +) +``` + +* Update the `REST_FRAMEWORK` variable as follows: + +``` python +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework.authentication.TokenAuthentication', + 'rest_framework.authentication.SessionAuthentication', + ), +} +``` + +* Remove the `SWAGGER_SETTINGS` variable +* Add the following content + +``` python +# CKeditor + +CKEDITOR_UPLOAD_PATH = "uploads/" + +CKEDITOR_IMAGE_BACKEND = "pillow" + +CKEDITOR_RESTRICT_BY_USER = True + +CKEDITOR_BROWSE_SHOW_DIRS = True + +CKEDITOR_ALLOW_NONIMAGE_FILES = False + +CKEDITOR_CONFIGS = { + 'default': { + 'allowedContent': True, + 'toolbar': 'Modoboa', + 'width': None, + 'toolbar_Modoboa': [ + ['Bold', 'Italic', 'Underline'], + ['JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock'], + ['BidiLtr', 'BidiRtl', 'Language'], + ['NumberedList', 'BulletedList', '-', 'Outdent', 'Indent'], + ['Undo', 'Redo'], + ['Link', 'Unlink', 'Anchor', '-', 'Smiley'], + ['TextColor', 'BGColor', '-', 'Source'], + ['Font', 'FontSize'], + ['Image', ], + ['SpellChecker'] + ], + }, +} +``` + +Don't forget to run the following command: + +``` bash +$ python manage.py collectstatic +``` + +## Version 1.7.1 + +If you used 1.7.0 for a fresh installation, please run the following commands: + +``` shell +$ sudo -u -i bash +$ source /bin/activate +$ cd +$ python manage.py load_initial_data +``` + +## Version 1.7.0 + +This version requires Django \>= 1.10 so you need to make some modifications. + +It also brings internal API changes which are not +backward compatible so installed extensions must be upgraded too. + +First of all, deactivate all installed extensions (edit the `settings.py` file +and comment the corresponding lines in `MODOBOA_APPS`). + +Edit the `urls.py` file of your local +instance and replace its content by the following one: + +``` python +from django.conf.urls import include, url + +urlpatterns = [ + url(r'', include('modoboa.urls')), +] +``` + +Edit the `settings.py` and apply the +following changes: + +* Add `'modoboa.parameters'` to `MODOBOA_APPS`: + +``` python +MODOBOA_APPS = ( + 'modoboa', + 'modoboa.core', + 'modoboa.lib', + 'modoboa.admin', + 'modoboa.relaydomains', + 'modoboa.limits', + 'modoboa.parameters', + # Modoboa extensions here. +) +``` + +* Add `'modoboa.core.middleware.LocalConfigMiddleware'` to + `MIDDLEWARE_CLASSES`: + +``` python +MIDDLEWARE_CLASSES = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'modoboa.core.middleware.LocalConfigMiddleware', + 'modoboa.lib.middleware.AjaxLoginRedirect', + 'modoboa.lib.middleware.CommonExceptionCatcher', + 'modoboa.lib.middleware.RequestCatcherMiddleware', +) +``` + +* Modoboa used to provide a custom authentication backend + (`modoboa.lib.authbackends.SimpleBackend`) but it has been removed. + Replace it as follows: + +``` python +AUTHENTICATION_BACKENDS = ( + # Other backends before... + 'django.contrib.auth.backends.ModelBackend', +) +``` + +* Remove `TEMPLATE_CONTEXT_PROCESSORS` and replace it by: + +``` python +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.template.context_processors.i18n', + 'django.template.context_processors.media', + 'django.template.context_processors.static', + 'django.template.context_processors.tz', + 'django.contrib.messages.context_processors.messages', + 'modoboa.core.context_processors.top_notifications', + ], + 'debug': False, + }, + }, +] +``` + +Run the following commands (load virtualenv if you use one): + +``` bash +$ sudo -u -i bash +$ source /bin/activate +$ cd +$ python manage.py migrate +$ python manage.py collectstatic +``` + +Finally, upgrade your extensions and reactivate them. + +------------------------------------------------------- +Name Version +--------------------------- --------------------------- +modoboa-amavis 1.1.0 + +modoboa-dmarc 1.0.0 + +modoboa-imap-migration 1.1.0 + +modoboa-pdfcredentials 1.1.0 + +modoboa-postfix-autoreply 1.2.0 + +modoboa-radicale 1.1.0 + +modoboa-sievefilters 1.1.0 + +modoboa-stats 1.1.0 + +modoboa-webmail 1.1.0 + +Command line shortcuts: + +``` bash +$ pip install modoboa-amavis==1.1.0 +$ pip install modoboa-dmarc==1.0.0 +$ pip install modoboa-imap-migration==1.1.0 +$ pip install modoboa-pdfcredentials==1.1.0 +$ pip install modoboa-postfix-autoreply==1.2.0 +$ pip install modoboa-radicale==1.1.0 +$ pip install modoboa-sievefilters==1.1.0 +$ pip install modoboa-stats==1.1.0 +$ pip install modoboa-webmail==1.1.0 +``` + +And please make sure you use the latest version of the `django-versionfield2` package: + +``` bash +$ pip install -U django-versionfield2 +``` + +### Notes about quota changes and resellers + +Reseller users now have a quota option in Resources tab. This is the +quota that a reseller can share between all its domains. + +There are two quotas for a domain in the new version: + +1. Quota & +2. Default mailbox quota. + +\[1\]. Quota: quota shared between mailboxes This quota is shared +between all the mailboxes of this domain. This value cannot exceed +reseller\'s quota and hence cannot be 0(unlimited) if reseller has +finite quota. + +\[2\]. Default mailbox quota: default quota applied to mailboxes This +quota is the default quota applied to new mailboxes. This value cannot +exceed Quota\[1\] and hence cannot be 0(unlimited) if Quota\[1\] is +finite. + +## Version 1.6.1 + +First of all, update postfix map files as follows: + +``` bash +$ python manage.py generate_postfix_maps --destdir --force-overwrite +``` + +Then, modify postfix's configuration as follows: + +```txt +smtpd_sender_login_maps = + :/sql-sender-login-mailboxes.cf + :/sql-sender-login-aliases.cf + :/sql-sender-login-mailboxes-extra.cf +``` + +Replace `` and `` by your values. + +Finally, reload postfix. + +This release also deprecates some internal functions. As a result, +several extensions has been updated to maintain the compatibility. If +you enabled the notification service, you\'ll find the list of available +updates directly in your Modoboa console. + +For the others, here is the list: + +------------------------------------------------------- +Name Version +--------------------------- --------------------------- +modoboa-amavis 1.0.10 +modoboa-postfix-autoreply 1.1.7 +modoboa-radicale 1.0.5 +modoboa-stats 1.0.9 + +Command line shortcut: + +``` bash +$ pip install modoboa-amavis==1.0.10 +$ pip install modoboa-postfix-autoreply==1.1.7 +$ pip install modoboa-radicale==1.0.5 +$ pip install modoboa-stats==1.0.9 +``` + +## Version 1.6.0 + +::: warning +You have to upgrade extensions due to [core.User] model +attribute change ([user.group] to [user.role] ). +Otherwise, you will have an internal error after upgrade. In particular: + +* [modoboa-amavisd](https://github.com/modoboa/modoboa-amavis/commit/35df4e48b124e56df930cda8c013af0c1fcaabf3), +* [modoboa-stats](https://github.com/modoboa/modoboa-stats/commit/aa4a39ce65eb306ad6dec30a54eb58945b120274), +* [modoboa-postfix-autoreply](https://github.com/modoboa/modoboa-postfix-autoreply/commit/20f98c8d1c0c0dbd420f47aefcbb0290022414a4) + are concerned. +::: + +An interesting feature brougth by this version is the capability to make +different checks about MX records. + +For example, Modoboa can query main [DNSBL](https://en.wikipedia.org/wiki/DNSBL) providers for every defined domain. + +With this, you will quickly know if one the domains you manage is listed or not. + +To activate it, add the following line to your crontab: + +```txt +*/30 * * * * python /manage.py modo check_mx +```` + +The communication with Modoboa public API has been reworked. Instead of +sending direct synchronous queries (for example to check new versions), +a cron job has been added. To activate it, add the following line to +your crontab: + +```txt +0 * * * * python /manage.py communicate_with_public_api +``` + +Please also note that public API now uses TLS so you must update your configuration as follows: + +```python +MODOBOA_API_URL = 'https://api.modoboa.org/1/' +``` + +Finally, it is now possible to declare additional sender addresses on a per-account basis. + +You need to update your postfix configuration in order to use this functionality. +Just edit the `main.cf` file and change the following parameter: + +```txt +smtpd_sender_login_maps = + :/etc/postfix/sql-sender-login-mailboxes.cf + :/etc/postfix/sql-sender-login-aliases.cf + :/etc/postfix/sql-sender-login-mailboxes-extra.cf +``` + +## Version 1.5.0 + +The API has been greatly improved and a documentation is now available. + +To enable it, add `'rest_framework_swagger'` to the `INSTALLED_APPS` variable in `settings.py` as follows: + +```python +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.sites', + 'django.contrib.staticfiles', + 'reversion', + 'rest_framework.authtoken', + 'rest_framework_swagger', # [!code ++] +) +``` + +Then, add the following content into `settings.py` , just after the `REST_FRAMEWORK` variable: + +```python +SWAGGER_SETTINGS = { # [!code ++] + "is_authenticated": False, # [!code ++] + "api_version": "1.0", # [!code ++] + "exclude_namespaces": [], # [!code ++] + "info": { # [!code ++] + "contact": "contact@modoboa.com", # [!code ++] + "description": ("Modoboa API, requires a valid token."), # [!code ++] + "title": "Modoboa API", # [!code ++] + } # [!code ++] +} # [!code ++] +``` + +You're done. The documentation is now available at the following address: + +```txt +https:// your instance address /docs/api/ +``` + +Finally, if you find a `TEMPLATE_CONTEXT_PROCESSORS` variable in your `settings.py` file, +make sure it looks like this: + +```python +TEMPLATE_CONTEXT_PROCESSORS = global_settings.TEMPLATE_CONTEXT_PROCESSORS + [ + 'modoboa.core.context_processors.top_notifications', +] +``` + +## Version 1.4.0 + +::: warning +Please make sure to use Modoboa 1.3.5 with an up-to-date database before an upgrade to 1.4.0. +::: + +::: warning +Do not follow the regular upgrade procedure for this version. +::: + +Some extension have been moved back into the main repository. The main +reason for that is that using Modoboa without them doesn\'t make sense. + +First of all, you must rename the following applications listed inside +the `MODOBOA_APPS` variable: + +| Old name | New name | +| :-------------------------: | :--------------------: | +| modoboa_admin | modoboa.admin | +| modoboa_admin_limits | modoboa.limits | +| modoboa_admin_relaydomains | modoboa.relaydomains | + +Then, apply the following steps: + +1. Uninstall old extensions: + ```shell + $ pip uninstall modoboa-admin modoboa-admin-limits modoboa-admin-relaydomains + ``` +2. Install all extension updates using pip (check the *Modoboa >Information* page) +3. Manually migrate database: + ```shell + $ cd + $ python manage.py migrate auth + $ python manage.py migrate admin 0001 --fake + $ python manage.py migrate admin + $ python manage.py migrate limits 0001 --fake + $ python manage.py migrate relaydomains 0001 --fake + $ python manage.py migrate + ``` +4. Finally, update static files: + ```shell + $ python manage.py collectstatic + ``` +This version also introduces a REST API. To enable it: + +1. Add `'rest_framework.authtoken'` to the `INSTALLED_APPS` variable +2. Add the following configuration inside `settings.py`: + ```python + # Rest framework settings + + REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework.authentication.TokenAuthentication', + ), + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.IsAuthenticated', + ) + } + ``` +3. Run the following command: + ```shell + $ python manage.py migrate + ``` + +## Version 1.3.5 + +To enhance security, Modoboa now checks the [strength of user passwords](https://github.com/dstufft/django-passwords) . + +To use this feature, add the following configuration into the `settings.py` file: + +```python +# django-passwords + +PASSWORD_MIN_LENGTH = 8 + +PASSWORD_COMPLEXITY = { + "UPPER": 1, + "LOWER": 1, + "DIGITS": 1 +} +``` + +## Version 1.3.2 + +Modoboa now uses the *atomic requests* mode to preserve database consistency +([reference](https://docs.djangoproject.com/en/1.7/topics/db/transactions/#tying-transactions-to-http-requests)). + +To enable it, update the `DATABASES` variable in `settings.py` as follows: + +```python +DATABASES = { + "default": { + # stuff before... + "ATOMIC_REQUESTS": True + }, + "amavis": { + # stuff before... + "ATOMIC_REQUESTS": True + } +} +``` + +## Version 1.3.0 + +This release does not bring awesome new features but it is a necessary bridge to the future of Modoboa. + +All extensions now have their own git repository and the deploy process has been updated to reflect this change. + +Another important update is the use of Django 1.7. + +Besides its new features, the migration system has been reworked and is now more robust than before. + +Before we begin with the procedure, here is a table showing old extension names and their new name: + +Old name New package name New module name +------------------------------------------ ---------------------------- ---------------------------- +modoboa.extensions.admin modoboa-admin modoboa_admin + +modoboa.extensions.limits modoboa-admin-limits modoboa_admin_limits + +modoboa.extensions.postfix_autoreply modoboa-postfix-autoreply modoboa_postfix_autoreply + +modoboa.extensions.postfix_relay_domains modoboa-admin-relaydomains modoboa_admin_relaydomains + +modoboa.extensions.radicale modoboa-radicale modoboa_radicale + +modoboa.extensions.sievefilters modoboa-sievefilters modoboa_sievefilters + +modoboa.extensions.stats modoboa-stats modoboa_stats + +modoboa.extensions.webmail modoboa-webmail modoboa_webmail + +Here are the required steps: + +1. Install the extensions using pip (look at the second column in the table above): + ```shell + $ pip install + ```` +2. Remove `south` from `INSTALLED_APPS` +3. Rename old extension names inside `MODOBOA_APPS` (look at the third column in the table above) +4. Remove `modoboa.lib.middleware.ExtControlMiddleware` from `MIDDLEWARE_CLASSES` +5. Change `DATABASE_ROUTERS` to: + ```python + DATABASE_ROUTERS = ["modoboa_amavis.dbrouter.AmavisRouter"] + ``` +6. Run the following commands: + ```shell + $ cd + $ python manage.py migrate + ``` +7. Reply `yes` to the question +8. Run the following commands: + ```shell + $ python manage.py load_initial_data + $ python manage.py collectstatic + ``` +9. The cleanup job has been renamed in Django, so you have to modify your crontab entry: + ```txt + - 0 0 * * * /manage.py cleanup + + 0 0 * * * /manage.py clearsessions + ``` + +## Version 1.2.0 + +A new notification service let administrators know about new Modoboa versions. + +To activate it, you need to update the `TEMPLATE_CONTEXT_PROCESSORS` variable like this: + +```python +from django.conf import global_settings + +TEMPLATE_CONTEXT_PROCESSORS = global_settings.TEMPLATE_CONTEXT_PROCESSORS + ( + 'modoboa.core.context_processors.top_notifications', +) +``` + +and to define the new `MODOBOA_API_URL` variable: + +```python +MODOBOA_API_URL = 'http://api.modoboa.org/1/' +``` + +The location of external static files has changed. To use them, add a new path to the `STATICFILES_DIRS`: + +```python + # Additional locations of static files + STATICFILES_DIRS = ( + # Put strings here, like "/home/html/static" or "C:/www/django/static". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. + "/bower_components", + ) +``` + +Run the following commands to define the hostname of your instance: + +```shell +$ cd +$ python manage.py set_default_site +``` + +If you plan to use the Radicale extension: + +1. Add `'modoboa.extensions.radicale'` to the `MODOBOA_APPS` variable +2. Run the following commands: + ```shell + $ cd + $ python manage.py syncdb + ``` + +::: warning +You also have to note that the `sitestatic` directory has moved +from `` to `` (it's probably the parent directory). + +You have to adapt your web server configuration to reflect this change. +::: \ No newline at end of file diff --git a/doc-ng/installation/webserver.md b/doc-ng/installation/webserver.md new file mode 100644 index 000000000..acf12a4d4 --- /dev/null +++ b/doc-ng/installation/webserver.md @@ -0,0 +1,347 @@ +# Web server {#webserver} + +::: info +The following instructions are meant to help you get your site up and +running quickly. However it is not possible for the people contributing +documentation to Modoboa to test every single combination of web server, +wsgi server, distribution, etc. So it is possible that **your** +installation of uwsgi or nginx or Apache or what-have-you works +differently. Keep this in mind. +::: + +## Apache2 + +First, make sure that `mod_wsgi` is installed on your server. + +Create a new virtualhost in your Apache configuration and put the +following content inside: + +```txt +WSGIPythonHome /var/www/modoboa/env +WSGIPythonPath /var/www/modoboa/instance + + + ServerName + + DocumentRoot {{ modoboa_instance_path }} + + Alias /media/ {{ modoboa_instance_path }}/media/ + + Options FollowSymLinks + Require all granted + + + Alias /sitestatic/ {{ modoboa_instance_path }}/sitestatic/ + + Options FollowSymLinks + Require all granted + + + WSGIScriptAliasMatch ^/(api|accounts) {{ modoboa_instance_path }}/instance/wsgi.py + WSGIApplicationGroup %{GLOBAL} + + # Pass Authorization header to enable API usage: + WSGIPassAuthorization On + + Alias / {{ modoboa_instance_path }}/frontend + + Options FollowSymLinks + Require all granted + + + LogLevel info + +``` + +This is just one possible configuration. + +Replace values between `<>` with yours. If you don't use a [virtualenv](http://virtualenv.readthedocs.org/en/latest/), +just remove the last part of the `WSGIDaemonProcess` directive. .. note: + +You may need to add: + +``` python [ {{ modoboa_instance_path }}/instance/wsgi.py] +import sys + +sys.path.append('{{ modoboa_instance_path }}') +sys.path.append('{{ modoboa_venv_path }}/bin') +sys.path.append('{{ modoboa_venv_path }}/lib/python3.X/site-packages') +``` + +in :file:` {{ modoboa_instance_path }}/instance/wsgi.py`. + +::: tip +You will certainly need more configuration in order to launch Apache. +::: + +Now, you can go the `dovecot` section to continue the installation. + +## Nginx + +This section covers two different ways of running Modoboa behind +[Nginx](http://nginx.org/) using a WSGI application server. + +Choose the one you prefer between +[Green Unicorn](http://gunicorn.org/) or [uWSGI](https://github.com/unbit/uwsgi). + +In both cases, you\'ll need to download and [install nginx](http://wiki.nginx.org/Install). + +### uWSGI + +The following setup is meant to get you started quickly. You should read +the documentation of both nginx and uwsgi to understand how to optimize +their configuration for your site. + +The Django documentation includes the following warning regarding uwsgi: + +::: warning +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). + +Recommend packages for debian and derivatives: + +```shell +apt install uwsgi uwsgi-plugin-python3 +``` + +Here is a sample nginx configuration: + +``` nginx +upstream modoboa { + server unix:{{ uwsgi_socket_path }} fail_timeout=0; +} + +server { + listen 80; + listen [::]:80; + server_name {{ hostname }}; + rewrite ^ https://$server_name$request_uri? permanent; +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name {{ hostname }}; + root {{ modoboa_instance_path }}; + + ssl_certificate ; + ssl_certificate_key ; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384"; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_verify_depth 3; + ssl_dhparam /etc/nginx/dhparam.pem; + + client_max_body_size 10M; + + access_log /var/log/nginx/modoboa-access.log; + error_log /var/log/nginx/modoboa-error.log; + + location /sitestatic/ { + try_files $uri $uri/ =404; + } + + location /media/ { + try_files $uri $uri/ =404; + } + + location ~ ^/(api|accounts|autodiscover) { + include uwsgi_params; + uwsgi_param UWSGI_SCRIPT instance.wsgi:application; + uwsgi_pass modoboa; + } + + location / { + alias {{ modoboa_instance_path }}/frontend/; + index index.html; + + expires -1; + add_header Pragma "no-cache"; + add_header Cache-Control "no-store, no-cache, must-revalidate, post-check=0, pre-check=0"; + + try_files $uri $uri/ /index.html = 404; + } +} +``` + +'uwsgi_socket_path', `modoboa instance name`, `hostname`, +`modoboa_instance_path` and `ssl...` must be replaced by the value +you used when `you deployed your instance deployment` + +In case you want to use enable auto-configuration functionality, + +create a second virtualhost configuration the hostname should look like `autoconfig.your domain here` + +```txt + server { + listen 80; + listen [::]:80; + server_name autoconfig.; + root {{ modoboa_instance_path }}; + + access_log /var/log/nginx/autoconfig-access.log; + error_log /var/log/nginx/autoconfig-error.log; + + location ~ ^/(mail/config-v1.1.xml|mobileconfig) { + include uwsgi_params; + uwsgi_param UWSGI_SCRIPT instance.wsgi:application; + uwsgi_pass modoboa; + } +} +``` + +`{{ modoboa_instance_path }}` and `your_domain` must be replaced by the +value you used when `you deployed your instance `{.interpreted-text role="ref"}. + +If you do not plan to use SSL then change the listen directive to `listen 80;` and delete each of the following directives: + +```txt + ssl on; + keepalive_timeout 70; + ssl_certificate {{ssl certificate for your site}}; + ssl_certificate_key {{ssl certificate key for your site}}; + uwsgi_param UWSGI_SCHEME https; +``` + +If you do plan to use SSL, you\'ll have to generate a certificate and a +key. [This article](http://wiki.nginx.org/HttpSslModule#Generate_Certificates) +contains information about how to do it. + +Make sure to replace the `...` in the sample configuration with +appropriate values. Here are some explanations for the cases that may +not be completely self-explanatory: + +* `hostname`: This is the URL which will be the root of your Modoboa site at your + domain. For instance, if your Modoboa installation is reachable at + at `https://foo/modoboa` then `hostname` is `/modoboa`. In this + case you probably also have to set the `alias` directives to point + to where Modoboa\'s sitestatic and media directories are because + otherwise nginx won\'t be able to find them. + + If Modoboa is at the root of your domain, then `hostname` is an + empty string and can be deleted from the configuration above. In + this case, you probably do not need the `alias` directives. +* `uwsgi_socket_path`: The location where uwsig is listening. It could be a unix domain + socket or an address:port combination. Ubuntu configures uwsgi so that the port is: + ```txt + unix:/run/uwsgi/app/ app name /socket + ``` + where `app name` is the name of the application. + +Your uwsgi configuration should be: + +``` ini{3,4} +[uwsgi] +# Not needed when using uwsgi from pip +# plugins = python +chdir = +module = .wsgi:application +master = true +harakiri = 60 +processes = 4 +vhost = true +no-default-app = true +buffer-size = 8192 +max-requests = 5000 +single-interpreter = true +``` + +The plugins directive should be turned on if you use a uwsgi +installation that requires it. If uwsgi was installed from pip, it does +not require it. In the configuration above: + +* `modoboa_instance_path`: The directory where `manage.py` resides. + This directory is the parent of `modoboa's settings dir` +* `name`: The name that you passed to `modoboa-admin.py deploy` + when you created your Modoboa instance, usually `instance`. + +Now, you can go the `dovecot` section to continue the installation. + +### Green Unicorn + +Firstly, [Download and install gunicorn](http://gunicorn.org/install.html). + +Then, use the following sample gunicorn configuration +(create a new file named `gunicorn.conf.py` inside Modoboa's root dir): + +```ini gunicorn.conf.py +backlog = 2048 +bind = "unix:/var/run/gunicorn/modoboa.sock" +pidfile = "/var/run/gunicorn/modoboa.pid" +daemon = True +debug = False +workers = 2 +logfile = "/var/log/gunicorn/modoboa.log" +loglevel = "info" +``` + + +To start gunicorn, execute the following commands: + +``` shell +$ cd {{ modoboa dir }} +$ gunicorn -c gunicorn.conf.py {{ APP/INSTANCE Name }}.wsgi:application +``` + +Now the nginx part. Just create a new virtual host and use the following +configuration: + +``` nginx +upstream modoboa { + server unix:/var/run/gunicorn/modoboa.sock fail_timeout=0; +} + +server { + listen 443 ssl; + ssl on; + keepalive_timeout 70; + + server_name {{ host fqdn }}; + root {{ modoboa_instance_path }}; + + access_log /var/log/nginx/{{ host fqdn }}.access.log; + error_log /var/log/nginx/{{ host fqdn }}.error.log; + + ssl_certificate {{ ssl certificate for your site }}; + ssl_certificate_key {{ ssl certificate key for your site }}; + + location /sitestatic/ { + autoindex on; + } + + location /media/ { + autoindex on; + } + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_set_header X-Forwarded-Protocol ssl; + proxy_pass http://modoboa; + } +} +``` + +If you do not plan to use SSL then change the listen directive to `listen 80;` +and delete each of the following directives: + +```txt +ssl on; +keepalive_timeout 70; +ssl_certificate ; +ssl_certificate_key ; +proxy_set_header X-Forwarded-Protocol ssl; +``` + +If you do plan to use SSL, you\'ll have to generate a certificate and a key. + +[This article](http://wiki.nginx.org/HttpSslModule#Generate_Certificates) contains information about how to do it. + +Paste this content to your configuration (replace values between `<>` with yours) and restart nginx. + +Now, you can go the [Dovecot](./dovecot) section to continue the installation. \ No newline at end of file diff --git a/doc-ng/moving.md b/doc-ng/moving.md new file mode 100644 index 000000000..cd47f2a3b --- /dev/null +++ b/doc-ng/moving.md @@ -0,0 +1,169 @@ +# Moving to Modoboa + +You have an existing platform and you\'d like to move to Modoboa, the +following tools could help you. + +## Using IMAP protocol {#imap} + +::: info +[Modoboa](https://modoboa.org/) provides a way to transfer accounts +hosted on a different platform/provider to your personal server using +the IMAP protocol and a tool called +[OfflineIMAP](https://www.offlineimap.org/doc/installation.html). +::: + +It works as follows: + +- You first declare what needs to be migrated, basically the domain(s) + you own and where they can be found. By doing that, you\'ll allow + accounts hosted on the platform/provider you\'re migrating from to log + into your Modoboa server +- The first time an old account (one coming from your previous provider) + successfuly log into Modoboa, a dedicated migration task is created +- On a regular basis (every hour generally), a cron job is responsible + for generating an OfflineIMAP configuration file including + instructions to migrate all declared migrations and lauching + OfflineIMAP to start/continue migrations + +:::: warning +::: title +Warning +::: + +As you probably already understood, you can\'t migrate all your accounts +at the same time since you don\'t know their corresponding password +(most of the time). Every account migration must be started by the +account\'s owner. +:::: + +### Configuration + +Modoboa won\'thandle the installation of OfflineIMAP so you need to do +it on your own. The following +[instructions](https://www.offlineimap.org/doc/quick_start.html) will +help you. + +Once done, *the new admin interface (v2)* will let you: + +- define the neccessary information to `connect to your old + platform/provider` +- define `what is to be migrated` (ie. the accounts) +- customize the behaviour of OfflineIMAP : go to \'Parameters \> Imap + Migration\' from the left menu + +:::: warning +::: title +Warning +::: + +**Auto create domain** option must be enabled to make this feature + +: work. Go to \'Parameters \> Administration \> Mailboxes\' and check + if it\'s the case. +:::: + +To generate an OfflineIMAP configuration file, run the following +command: + +``` bash +> python manage.py generate_offlineimap_config +``` + +The synchronization script must be configured to run periodically on +your new server. Since it will copy mailboxes content to its final +destination, filesystem permissions must be respected. To do that, it +must be executed by the user which owns mailboxes (generally `vmail`). + +Here is a configuration example where the script is executed every +hours. You can copy it inside the `/etc/cron.d/modoboa` file: + +``` shell +PYTHON=/srv/modoboa/env/bin/python +INSTANCE=/srv/modoboa/instance + +0 */1 * * * vmail cd /srv/vmail && $PYTHON $INSTANCE/manage.py generate_offlineimap_config --output .offlineimaprc && /usr/local/bin/offlineimap > /dev/null 2>&1 +``` + +Feel free to adapt it. + +This will sync back-and-forth user mailbox. + +### Providers + +A provider represents an IMAP server that hosts one or several domains +you own. It includes connection information (network address, port, +etc.) and a list of hosted domain. + +For each domain you declare, you must provide its name. Modoboa will +then create a local domain with the exact same name if needed (ie. if it +does not exist yet) the first time it migrates an account. + +Optionaly, you have the possibility to rename a domain (for example: +domain.com -\> domain.net) by providing a value to the \'Local domain\' +field. + +### Migrations + +Migrations are automatically created, you don\'t need to do it. + +The first time an account of your old platform successfuly log into +Modoboa (using old credentials), a local account and a migration task +will be created. + +:::: warning +::: title +Warning +::: + +In case of domain renaming as described in Providers section, users +must use the new email address as username for next connection attempts. +:::: + +SuperAdmins can monitor running migrations and stop/delete them from the +[IMAP Migration] section (left menu). + +## From postfixadmin + +A dedicated command allows you to convert an existing [postfixadmin](http://postfixadmin.sourceforge.net/) +database to a Modoboa one. + +Consult the [documentation](https://github.com/modoboa/modoboa-pfxadmin-migrate) to know more about the process. + +## Using CSV files + +Modoboa allows you to import any object (domain, domain alias, mailbox +and alias) using a simple CSV file encoded using **UTF8**. + +Each line corresponds to a single object and must respect one of the following format: + +```csv +domain; ; ; ; +domainalias; ; ; +relaydomain; ; ; ; ; ; +account; ; ; ; ; ; ; ; ; [, ...] +alias; ; ; ; ... +``` + +Boolean fields accept the following values: `true`, `1`, `yes`, `y` (case insensitive). +Any other value will be evaluated as false. + +::: warning +The order does matter. Objects are created sequencially so a domain must +be created before its mailboxes and aliases and a mailbox must created +before its alias(es). +::: + +To actually import such a file: + +``` bash +$ sudo -u -i bash +$ source /bin/activate +$ cd +$ python manage.py modo import +``` + +Available options can be listed using the following command: + +``` bash +$ python manage.py modo import -h +``` \ No newline at end of file diff --git a/doc-ng/package.json b/doc-ng/package.json new file mode 100644 index 000000000..c614f0a19 --- /dev/null +++ b/doc-ng/package.json @@ -0,0 +1,11 @@ +{ + "devDependencies": { + "vitepress": "2.0.0-alpha.15" + }, + "packageManager": "pnpm@10.22.0+sha512.bf049efe995b28f527fd2b41ae0474ce29186f7edcb3bf545087bd61fbbebb2bf75362d1307fda09c2d288e1e499787ac12d4fcb617a974718a6051f2eee741c", + "scripts": { + "dev": "vitepress dev", + "build": "vitepress build", + "preview": "vitepress preview" + } +} \ No newline at end of file diff --git a/doc-ng/pnpm-lock.yaml b/doc-ng/pnpm-lock.yaml new file mode 100644 index 000000000..32dd05e18 --- /dev/null +++ b/doc-ng/pnpm-lock.yaml @@ -0,0 +1,1837 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + vitepress: + specifier: 2.0.0-alpha.15 + version: 2.0.0-alpha.15(@algolia/client-search@5.46.2)(postcss@8.5.6)(react@19.2.3)(search-insights@2.17.3) + +packages: + + '@ai-sdk/gateway@2.0.24': + resolution: {integrity: sha512-mflk80YF8hj8vrF9e1IHhovGKC1ubX+sY88pesSk3pUiXfH5VPO8dgzNnxjwsqsCZrnkHcztxS5cSl4TzSiEuA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@3.0.20': + resolution: {integrity: sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider@2.0.1': + resolution: {integrity: sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng==} + engines: {node: '>=18'} + + '@ai-sdk/react@2.0.120': + resolution: {integrity: sha512-x7Oa2LDRURc8uRnAdcEfydbHLSXGYjNaFlQrGuxZAMfqhLJQ+7x4K8Z6O5vnLt414mrPaVvgirfRqsP/nsxtnw==} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ~19.0.1 || ~19.1.2 || ^19.2.1 + zod: ^3.25.76 || ^4.1.8 + peerDependenciesMeta: + zod: + optional: true + + '@algolia/abtesting@1.12.2': + resolution: {integrity: sha512-oWknd6wpfNrmRcH0vzed3UPX0i17o4kYLM5OMITyMVM2xLgaRbIafoxL0e8mcrNNb0iORCJA0evnNDKRYth5WQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/autocomplete-core@1.19.2': + resolution: {integrity: sha512-mKv7RyuAzXvwmq+0XRK8HqZXt9iZ5Kkm2huLjgn5JoCPtDy+oh9yxUMfDDaVCw0oyzZ1isdJBc7l9nuCyyR7Nw==} + + '@algolia/autocomplete-plugin-algolia-insights@1.19.2': + resolution: {integrity: sha512-TjxbcC/r4vwmnZaPwrHtkXNeqvlpdyR+oR9Wi2XyfORkiGkLTVhX2j+O9SaCCINbKoDfc+c2PB8NjfOnz7+oKg==} + peerDependencies: + search-insights: '>= 1 < 3' + + '@algolia/autocomplete-shared@1.19.2': + resolution: {integrity: sha512-jEazxZTVD2nLrC+wYlVHQgpBoBB5KPStrJxLzsIFl6Kqd1AlG9sIAGl39V5tECLpIQzB3Qa2T6ZPJ1ChkwMK/w==} + peerDependencies: + '@algolia/client-search': '>= 4.9.1 < 6' + algoliasearch: '>= 4.9.1 < 6' + + '@algolia/client-abtesting@5.46.2': + resolution: {integrity: sha512-oRSUHbylGIuxrlzdPA8FPJuwrLLRavOhAmFGgdAvMcX47XsyM+IOGa9tc7/K5SPvBqn4nhppOCEz7BrzOPWc4A==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-analytics@5.46.2': + resolution: {integrity: sha512-EPBN2Oruw0maWOF4OgGPfioTvd+gmiNwx0HmD9IgmlS+l75DatcBkKOPNJN+0z3wBQWUO5oq602ATxIfmTQ8bA==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-common@5.46.2': + resolution: {integrity: sha512-Hj8gswSJNKZ0oyd0wWissqyasm+wTz1oIsv5ZmLarzOZAp3vFEda8bpDQ8PUhO+DfkbiLyVnAxsPe4cGzWtqkg==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-insights@5.46.2': + resolution: {integrity: sha512-6dBZko2jt8FmQcHCbmNLB0kCV079Mx/DJcySTL3wirgDBUH7xhY1pOuUTLMiGkqM5D8moVZTvTdRKZUJRkrwBA==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-personalization@5.46.2': + resolution: {integrity: sha512-1waE2Uqh/PHNeDXGn/PM/WrmYOBiUGSVxAWqiJIj73jqPqvfzZgzdakHscIVaDl6Cp+j5dwjsZ5LCgaUr6DtmA==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-query-suggestions@5.46.2': + resolution: {integrity: sha512-EgOzTZkyDcNL6DV0V/24+oBJ+hKo0wNgyrOX/mePBM9bc9huHxIY2352sXmoZ648JXXY2x//V1kropF/Spx83w==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-search@5.46.2': + resolution: {integrity: sha512-ZsOJqu4HOG5BlvIFnMU0YKjQ9ZI6r3C31dg2jk5kMWPSdhJpYL9xa5hEe7aieE+707dXeMI4ej3diy6mXdZpgA==} + engines: {node: '>= 14.0.0'} + + '@algolia/ingestion@1.46.2': + resolution: {integrity: sha512-1Uw2OslTWiOFDtt83y0bGiErJYy5MizadV0nHnOoHFWMoDqWW0kQoMFI65pXqRSkVvit5zjXSLik2xMiyQJDWQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/monitoring@1.46.2': + resolution: {integrity: sha512-xk9f+DPtNcddWN6E7n1hyNNsATBCHIqAvVGG2EAGHJc4AFYL18uM/kMTiOKXE/LKDPyy1JhIerrh9oYb7RBrgw==} + engines: {node: '>= 14.0.0'} + + '@algolia/recommend@5.46.2': + resolution: {integrity: sha512-NApbTPj9LxGzNw4dYnZmj2BoXiAc8NmbbH6qBNzQgXklGklt/xldTvu+FACN6ltFsTzoNU6j2mWNlHQTKGC5+Q==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-browser-xhr@5.46.2': + resolution: {integrity: sha512-ekotpCwpSp033DIIrsTpYlGUCF6momkgupRV/FA3m62SreTSZUKjgK6VTNyG7TtYfq9YFm/pnh65bATP/ZWJEg==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-fetch@5.46.2': + resolution: {integrity: sha512-gKE+ZFi/6y7saTr34wS0SqYFDcjHW4Wminv8PDZEi0/mE99+hSrbKgJWxo2ztb5eqGirQTgIh1AMVacGGWM1iw==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-node-http@5.46.2': + resolution: {integrity: sha512-ciPihkletp7ttweJ8Zt+GukSVLp2ANJHU+9ttiSxsJZThXc4Y2yJ8HGVWesW5jN1zrsZsezN71KrMx/iZsOYpg==} + engines: {node: '>= 14.0.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + + '@docsearch/core@4.4.0': + resolution: {integrity: sha512-kiwNo5KEndOnrf5Kq/e5+D9NBMCFgNsDoRpKQJ9o/xnSlheh6b8AXppMuuUVVdAUIhIfQFk/07VLjjk/fYyKmw==} + peerDependencies: + '@types/react': '>= 16.8.0 < 20.0.0' + react: '>= 16.8.0 < 20.0.0' + react-dom: '>= 16.8.0 < 20.0.0' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + react-dom: + optional: true + + '@docsearch/css@4.4.0': + resolution: {integrity: sha512-e9vPgtih6fkawakmYo0Y6V4BKBmDV7Ykudn7ADWXUs5b6pmtBRwDbpSG/WiaUG63G28OkJDEnsMvgIAnZgGwYw==} + + '@docsearch/js@4.4.0': + resolution: {integrity: sha512-vCiKzjYD54bugUIMZA6YzuLDilkD3TNH/kfbvqsnzxiLTMu8F13psD+hdMSEOn7j+dFJOaf49fZ+gwr+rXctMw==} + + '@docsearch/react@4.4.0': + resolution: {integrity: sha512-z12zeg1mV7WD4Ag4pKSuGukETJLaucVFwszDXL/qLaEgRqxEaVacO9SR1qqnCXvZztlvz2rt7cMqryi/7sKfjA==} + peerDependencies: + '@types/react': '>= 16.8.0 < 20.0.0' + react: '>= 16.8.0 < 20.0.0' + react-dom: '>= 16.8.0 < 20.0.0' + search-insights: '>= 1 < 3' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + react-dom: + optional: true + search-insights: + optional: true + + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@iconify-json/simple-icons@1.2.65': + resolution: {integrity: sha512-v/O0UeqrDz6ASuRVE5g2Puo5aWyej4M/CxX6WYDBARgswwxK0mp3VQbGgPFEAAUU9QN02IjTgjMuO021gpWf2w==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@rolldown/pluginutils@1.0.0-beta.53': + resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} + + '@rollup/rollup-android-arm-eabi@4.55.1': + resolution: {integrity: sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.55.1': + resolution: {integrity: sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.55.1': + resolution: {integrity: sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.55.1': + resolution: {integrity: sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.55.1': + resolution: {integrity: sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.55.1': + resolution: {integrity: sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.55.1': + resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.55.1': + resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.55.1': + resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.55.1': + resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.55.1': + resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.55.1': + resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.55.1': + resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.55.1': + resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.55.1': + resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.55.1': + resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.55.1': + resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.55.1': + resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.55.1': + resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.55.1': + resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.55.1': + resolution: {integrity: sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.55.1': + resolution: {integrity: sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.55.1': + resolution: {integrity: sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.55.1': + resolution: {integrity: sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.55.1': + resolution: {integrity: sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==} + cpu: [x64] + os: [win32] + + '@shikijs/core@3.21.0': + resolution: {integrity: sha512-AXSQu/2n1UIQekY8euBJlvFYZIw0PHY63jUzGbrOma4wPxzznJXTXkri+QcHeBNaFxiiOljKxxJkVSoB3PjbyA==} + + '@shikijs/engine-javascript@3.21.0': + resolution: {integrity: sha512-ATwv86xlbmfD9n9gKRiwuPpWgPENAWCLwYCGz9ugTJlsO2kOzhOkvoyV/UD+tJ0uT7YRyD530x6ugNSffmvIiQ==} + + '@shikijs/engine-oniguruma@3.21.0': + resolution: {integrity: sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ==} + + '@shikijs/langs@3.21.0': + resolution: {integrity: sha512-g6mn5m+Y6GBJ4wxmBYqalK9Sp0CFkUqfNzUy2pJglUginz6ZpWbaWjDB4fbQ/8SHzFjYbtU6Ddlp1pc+PPNDVA==} + + '@shikijs/themes@3.21.0': + resolution: {integrity: sha512-BAE4cr9EDiZyYzwIHEk7JTBJ9CzlPuM4PchfcA5ao1dWXb25nv6hYsoDiBq2aZK9E3dlt3WB78uI96UESD+8Mw==} + + '@shikijs/transformers@3.21.0': + resolution: {integrity: sha512-CZwvCWWIiRRiFk9/JKzdEooakAP8mQDtBOQ1TKiCaS2E1bYtyBCOkUzS8akO34/7ufICQ29oeSfkb3tT5KtrhA==} + + '@shikijs/types@3.21.0': + resolution: {integrity: sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@vercel/oidc@3.0.5': + resolution: {integrity: sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw==} + engines: {node: '>= 20'} + + '@vitejs/plugin-vue@6.0.3': + resolution: {integrity: sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + vue: ^3.2.25 + + '@vue/compiler-core@3.5.26': + resolution: {integrity: sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==} + + '@vue/compiler-dom@3.5.26': + resolution: {integrity: sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==} + + '@vue/compiler-sfc@3.5.26': + resolution: {integrity: sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==} + + '@vue/compiler-ssr@3.5.26': + resolution: {integrity: sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==} + + '@vue/devtools-api@8.0.5': + resolution: {integrity: sha512-DgVcW8H/Nral7LgZEecYFFYXnAvGuN9C3L3DtWekAncFBedBczpNW8iHKExfaM559Zm8wQWrwtYZ9lXthEHtDw==} + + '@vue/devtools-kit@8.0.5': + resolution: {integrity: sha512-q2VV6x1U3KJMTQPUlRMyWEKVbcHuxhqJdSr6Jtjz5uAThAIrfJ6WVZdGZm5cuO63ZnSUz0RCsVwiUUb0mDV0Yg==} + + '@vue/devtools-shared@8.0.5': + resolution: {integrity: sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg==} + + '@vue/reactivity@3.5.26': + resolution: {integrity: sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==} + + '@vue/runtime-core@3.5.26': + resolution: {integrity: sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==} + + '@vue/runtime-dom@3.5.26': + resolution: {integrity: sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==} + + '@vue/server-renderer@3.5.26': + resolution: {integrity: sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==} + peerDependencies: + vue: 3.5.26 + + '@vue/shared@3.5.26': + resolution: {integrity: sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==} + + '@vueuse/core@14.1.0': + resolution: {integrity: sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/integrations@14.1.0': + resolution: {integrity: sha512-eNQPdisnO9SvdydTIXnTE7c29yOsJBD/xkwEyQLdhDC/LKbqrFpXHb3uS//7NcIrQO3fWVuvMGp8dbK6mNEMCA==} + peerDependencies: + async-validator: ^4 + axios: ^1 + change-case: ^5 + drauu: ^0.4 + focus-trap: ^7 + fuse.js: ^7 + idb-keyval: ^6 + jwt-decode: ^4 + nprogress: ^0.2 + qrcode: ^1.5 + sortablejs: ^1 + universal-cookie: ^7 || ^8 + vue: ^3.5.0 + peerDependenciesMeta: + async-validator: + optional: true + axios: + optional: true + change-case: + optional: true + drauu: + optional: true + focus-trap: + optional: true + fuse.js: + optional: true + idb-keyval: + optional: true + jwt-decode: + optional: true + nprogress: + optional: true + qrcode: + optional: true + sortablejs: + optional: true + universal-cookie: + optional: true + + '@vueuse/metadata@14.1.0': + resolution: {integrity: sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==} + + '@vueuse/shared@14.1.0': + resolution: {integrity: sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==} + peerDependencies: + vue: ^3.5.0 + + ai@5.0.118: + resolution: {integrity: sha512-sKJHfhJkvAyq5NC3yJJ4R8Z3tn4pSHF760/jInKAtmLwPLWTHfGo293DSO4un8QUAgJOagHd09VSXOXv+STMNQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + algoliasearch@5.46.2: + resolution: {integrity: sha512-qqAXW9QvKf2tTyhpDA4qXv1IfBwD2eduSW6tUEBFIfCeE9gn9HQ9I5+MaKoenRuHrzk5sQoNh1/iof8mY7uD6Q==} + engines: {node: '>= 14.0.0'} + + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + copy-anything@4.0.5: + resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} + engines: {node: '>=18'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + entities@7.0.0: + resolution: {integrity: sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==} + engines: {node: '>=0.12'} + + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + focus-trap@7.7.1: + resolution: {integrity: sha512-Pkp8m55GjxBLnhBoT6OXdMvfRr4TjMAKLvFM566zlIryq5plbhaTmLAJWTGR0EkRwLjEte1lCOG9MxF1ipJrOg==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + htm@3.1.1: + resolution: {integrity: sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + is-what@5.5.0: + resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} + engines: {node: '>=18'} + + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mark.js@8.11.1: + resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} + + marked@16.4.2: + resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} + engines: {node: '>= 20'} + hasBin: true + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + minisearch@7.2.0: + resolution: {integrity: sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + oniguruma-parser@0.12.1: + resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} + + oniguruma-to-es@4.3.4: + resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==} + + perfect-debounce@2.0.0: + resolution: {integrity: sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + + react@19.2.3: + resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} + engines: {node: '>=0.10.0'} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rollup@4.55.1: + resolution: {integrity: sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + search-insights@2.17.3: + resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==} + + shiki@3.21.0: + resolution: {integrity: sha512-N65B/3bqL/TI2crrXr+4UivctrAGEjmsib5rPMMPpFp1xAx/w03v8WZ9RDDFYteXoEgY7qZ4HGgl5KBIu1153w==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + superjson@2.2.6: + resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} + engines: {node: '>=16'} + + swr@2.3.8: + resolution: {integrity: sha512-gaCPRVoMq8WGDcWj9p4YWzCMPHzE0WNl6W8ADIx9c3JBEIdMkJGMzW+uzXvxHMltwcYACr9jP+32H8/hgwMR7w==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + tabbable@6.4.0: + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} + + throttleit@2.1.0: + resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} + engines: {node: '>=18'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitepress@2.0.0-alpha.15: + resolution: {integrity: sha512-jhjSYd10Z6RZiKOa7jy0xMVf5NB5oSc/lS3bD/QoUc6V8PrvQR5JhC9104NEt6+oTGY/ftieVWxY9v7YI+1IjA==} + hasBin: true + peerDependencies: + markdown-it-mathjax3: ^4 + oxc-minify: '*' + postcss: ^8 + peerDependenciesMeta: + markdown-it-mathjax3: + optional: true + oxc-minify: + optional: true + postcss: + optional: true + + vue@3.5.26: + resolution: {integrity: sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + zod@4.3.5: + resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@ai-sdk/gateway@2.0.24(zod@4.3.5)': + dependencies: + '@ai-sdk/provider': 2.0.1 + '@ai-sdk/provider-utils': 3.0.20(zod@4.3.5) + '@vercel/oidc': 3.0.5 + zod: 4.3.5 + + '@ai-sdk/provider-utils@3.0.20(zod@4.3.5)': + dependencies: + '@ai-sdk/provider': 2.0.1 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.6 + zod: 4.3.5 + + '@ai-sdk/provider@2.0.1': + dependencies: + json-schema: 0.4.0 + + '@ai-sdk/react@2.0.120(react@19.2.3)(zod@4.3.5)': + dependencies: + '@ai-sdk/provider-utils': 3.0.20(zod@4.3.5) + ai: 5.0.118(zod@4.3.5) + react: 19.2.3 + swr: 2.3.8(react@19.2.3) + throttleit: 2.1.0 + optionalDependencies: + zod: 4.3.5 + + '@algolia/abtesting@1.12.2': + dependencies: + '@algolia/client-common': 5.46.2 + '@algolia/requester-browser-xhr': 5.46.2 + '@algolia/requester-fetch': 5.46.2 + '@algolia/requester-node-http': 5.46.2 + + '@algolia/autocomplete-core@1.19.2(@algolia/client-search@5.46.2)(algoliasearch@5.46.2)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-plugin-algolia-insights': 1.19.2(@algolia/client-search@5.46.2)(algoliasearch@5.46.2)(search-insights@2.17.3) + '@algolia/autocomplete-shared': 1.19.2(@algolia/client-search@5.46.2)(algoliasearch@5.46.2) + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + - search-insights + + '@algolia/autocomplete-plugin-algolia-insights@1.19.2(@algolia/client-search@5.46.2)(algoliasearch@5.46.2)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-shared': 1.19.2(@algolia/client-search@5.46.2)(algoliasearch@5.46.2) + search-insights: 2.17.3 + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + + '@algolia/autocomplete-shared@1.19.2(@algolia/client-search@5.46.2)(algoliasearch@5.46.2)': + dependencies: + '@algolia/client-search': 5.46.2 + algoliasearch: 5.46.2 + + '@algolia/client-abtesting@5.46.2': + dependencies: + '@algolia/client-common': 5.46.2 + '@algolia/requester-browser-xhr': 5.46.2 + '@algolia/requester-fetch': 5.46.2 + '@algolia/requester-node-http': 5.46.2 + + '@algolia/client-analytics@5.46.2': + dependencies: + '@algolia/client-common': 5.46.2 + '@algolia/requester-browser-xhr': 5.46.2 + '@algolia/requester-fetch': 5.46.2 + '@algolia/requester-node-http': 5.46.2 + + '@algolia/client-common@5.46.2': {} + + '@algolia/client-insights@5.46.2': + dependencies: + '@algolia/client-common': 5.46.2 + '@algolia/requester-browser-xhr': 5.46.2 + '@algolia/requester-fetch': 5.46.2 + '@algolia/requester-node-http': 5.46.2 + + '@algolia/client-personalization@5.46.2': + dependencies: + '@algolia/client-common': 5.46.2 + '@algolia/requester-browser-xhr': 5.46.2 + '@algolia/requester-fetch': 5.46.2 + '@algolia/requester-node-http': 5.46.2 + + '@algolia/client-query-suggestions@5.46.2': + dependencies: + '@algolia/client-common': 5.46.2 + '@algolia/requester-browser-xhr': 5.46.2 + '@algolia/requester-fetch': 5.46.2 + '@algolia/requester-node-http': 5.46.2 + + '@algolia/client-search@5.46.2': + dependencies: + '@algolia/client-common': 5.46.2 + '@algolia/requester-browser-xhr': 5.46.2 + '@algolia/requester-fetch': 5.46.2 + '@algolia/requester-node-http': 5.46.2 + + '@algolia/ingestion@1.46.2': + dependencies: + '@algolia/client-common': 5.46.2 + '@algolia/requester-browser-xhr': 5.46.2 + '@algolia/requester-fetch': 5.46.2 + '@algolia/requester-node-http': 5.46.2 + + '@algolia/monitoring@1.46.2': + dependencies: + '@algolia/client-common': 5.46.2 + '@algolia/requester-browser-xhr': 5.46.2 + '@algolia/requester-fetch': 5.46.2 + '@algolia/requester-node-http': 5.46.2 + + '@algolia/recommend@5.46.2': + dependencies: + '@algolia/client-common': 5.46.2 + '@algolia/requester-browser-xhr': 5.46.2 + '@algolia/requester-fetch': 5.46.2 + '@algolia/requester-node-http': 5.46.2 + + '@algolia/requester-browser-xhr@5.46.2': + dependencies: + '@algolia/client-common': 5.46.2 + + '@algolia/requester-fetch@5.46.2': + dependencies: + '@algolia/client-common': 5.46.2 + + '@algolia/requester-node-http@5.46.2': + dependencies: + '@algolia/client-common': 5.46.2 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@docsearch/core@4.4.0(react@19.2.3)': + optionalDependencies: + react: 19.2.3 + + '@docsearch/css@4.4.0': {} + + '@docsearch/js@4.4.0(@algolia/client-search@5.46.2)(react@19.2.3)(search-insights@2.17.3)': + dependencies: + '@docsearch/react': 4.4.0(@algolia/client-search@5.46.2)(react@19.2.3)(search-insights@2.17.3) + htm: 3.1.1 + transitivePeerDependencies: + - '@algolia/client-search' + - '@types/react' + - react + - react-dom + - search-insights + + '@docsearch/react@4.4.0(@algolia/client-search@5.46.2)(react@19.2.3)(search-insights@2.17.3)': + dependencies: + '@ai-sdk/react': 2.0.120(react@19.2.3)(zod@4.3.5) + '@algolia/autocomplete-core': 1.19.2(@algolia/client-search@5.46.2)(algoliasearch@5.46.2)(search-insights@2.17.3) + '@docsearch/core': 4.4.0(react@19.2.3) + '@docsearch/css': 4.4.0 + ai: 5.0.118(zod@4.3.5) + algoliasearch: 5.46.2 + marked: 16.4.2 + zod: 4.3.5 + optionalDependencies: + react: 19.2.3 + search-insights: 2.17.3 + transitivePeerDependencies: + - '@algolia/client-search' + + '@esbuild/aix-ppc64@0.27.2': + optional: true + + '@esbuild/android-arm64@0.27.2': + optional: true + + '@esbuild/android-arm@0.27.2': + optional: true + + '@esbuild/android-x64@0.27.2': + optional: true + + '@esbuild/darwin-arm64@0.27.2': + optional: true + + '@esbuild/darwin-x64@0.27.2': + optional: true + + '@esbuild/freebsd-arm64@0.27.2': + optional: true + + '@esbuild/freebsd-x64@0.27.2': + optional: true + + '@esbuild/linux-arm64@0.27.2': + optional: true + + '@esbuild/linux-arm@0.27.2': + optional: true + + '@esbuild/linux-ia32@0.27.2': + optional: true + + '@esbuild/linux-loong64@0.27.2': + optional: true + + '@esbuild/linux-mips64el@0.27.2': + optional: true + + '@esbuild/linux-ppc64@0.27.2': + optional: true + + '@esbuild/linux-riscv64@0.27.2': + optional: true + + '@esbuild/linux-s390x@0.27.2': + optional: true + + '@esbuild/linux-x64@0.27.2': + optional: true + + '@esbuild/netbsd-arm64@0.27.2': + optional: true + + '@esbuild/netbsd-x64@0.27.2': + optional: true + + '@esbuild/openbsd-arm64@0.27.2': + optional: true + + '@esbuild/openbsd-x64@0.27.2': + optional: true + + '@esbuild/openharmony-arm64@0.27.2': + optional: true + + '@esbuild/sunos-x64@0.27.2': + optional: true + + '@esbuild/win32-arm64@0.27.2': + optional: true + + '@esbuild/win32-ia32@0.27.2': + optional: true + + '@esbuild/win32-x64@0.27.2': + optional: true + + '@iconify-json/simple-icons@1.2.65': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify/types@2.0.0': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@opentelemetry/api@1.9.0': {} + + '@rolldown/pluginutils@1.0.0-beta.53': {} + + '@rollup/rollup-android-arm-eabi@4.55.1': + optional: true + + '@rollup/rollup-android-arm64@4.55.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.55.1': + optional: true + + '@rollup/rollup-darwin-x64@4.55.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.55.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.55.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.55.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.55.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.55.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.55.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.55.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.55.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.55.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.55.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.55.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.55.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.55.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.55.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.55.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.55.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.55.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.55.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.55.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.55.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.55.1': + optional: true + + '@shikijs/core@3.21.0': + dependencies: + '@shikijs/types': 3.21.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@3.21.0': + dependencies: + '@shikijs/types': 3.21.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.4 + + '@shikijs/engine-oniguruma@3.21.0': + dependencies: + '@shikijs/types': 3.21.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@3.21.0': + dependencies: + '@shikijs/types': 3.21.0 + + '@shikijs/themes@3.21.0': + dependencies: + '@shikijs/types': 3.21.0 + + '@shikijs/transformers@3.21.0': + dependencies: + '@shikijs/core': 3.21.0 + '@shikijs/types': 3.21.0 + + '@shikijs/types@3.21.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + + '@standard-schema/spec@1.1.0': {} + + '@types/estree@1.0.8': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdurl@2.0.0': {} + + '@types/unist@3.0.3': {} + + '@types/web-bluetooth@0.0.21': {} + + '@ungap/structured-clone@1.3.0': {} + + '@vercel/oidc@3.0.5': {} + + '@vitejs/plugin-vue@6.0.3(vite@7.3.1)(vue@3.5.26)': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.53 + vite: 7.3.1 + vue: 3.5.26 + + '@vue/compiler-core@3.5.26': + dependencies: + '@babel/parser': 7.28.5 + '@vue/shared': 3.5.26 + entities: 7.0.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.26': + dependencies: + '@vue/compiler-core': 3.5.26 + '@vue/shared': 3.5.26 + + '@vue/compiler-sfc@3.5.26': + dependencies: + '@babel/parser': 7.28.5 + '@vue/compiler-core': 3.5.26 + '@vue/compiler-dom': 3.5.26 + '@vue/compiler-ssr': 3.5.26 + '@vue/shared': 3.5.26 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.6 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.26': + dependencies: + '@vue/compiler-dom': 3.5.26 + '@vue/shared': 3.5.26 + + '@vue/devtools-api@8.0.5': + dependencies: + '@vue/devtools-kit': 8.0.5 + + '@vue/devtools-kit@8.0.5': + dependencies: + '@vue/devtools-shared': 8.0.5 + birpc: 2.9.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 2.0.0 + speakingurl: 14.0.1 + superjson: 2.2.6 + + '@vue/devtools-shared@8.0.5': + dependencies: + rfdc: 1.4.1 + + '@vue/reactivity@3.5.26': + dependencies: + '@vue/shared': 3.5.26 + + '@vue/runtime-core@3.5.26': + dependencies: + '@vue/reactivity': 3.5.26 + '@vue/shared': 3.5.26 + + '@vue/runtime-dom@3.5.26': + dependencies: + '@vue/reactivity': 3.5.26 + '@vue/runtime-core': 3.5.26 + '@vue/shared': 3.5.26 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.26(vue@3.5.26)': + dependencies: + '@vue/compiler-ssr': 3.5.26 + '@vue/shared': 3.5.26 + vue: 3.5.26 + + '@vue/shared@3.5.26': {} + + '@vueuse/core@14.1.0(vue@3.5.26)': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 14.1.0 + '@vueuse/shared': 14.1.0(vue@3.5.26) + vue: 3.5.26 + + '@vueuse/integrations@14.1.0(focus-trap@7.7.1)(vue@3.5.26)': + dependencies: + '@vueuse/core': 14.1.0(vue@3.5.26) + '@vueuse/shared': 14.1.0(vue@3.5.26) + vue: 3.5.26 + optionalDependencies: + focus-trap: 7.7.1 + + '@vueuse/metadata@14.1.0': {} + + '@vueuse/shared@14.1.0(vue@3.5.26)': + dependencies: + vue: 3.5.26 + + ai@5.0.118(zod@4.3.5): + dependencies: + '@ai-sdk/gateway': 2.0.24(zod@4.3.5) + '@ai-sdk/provider': 2.0.1 + '@ai-sdk/provider-utils': 3.0.20(zod@4.3.5) + '@opentelemetry/api': 1.9.0 + zod: 4.3.5 + + algoliasearch@5.46.2: + dependencies: + '@algolia/abtesting': 1.12.2 + '@algolia/client-abtesting': 5.46.2 + '@algolia/client-analytics': 5.46.2 + '@algolia/client-common': 5.46.2 + '@algolia/client-insights': 5.46.2 + '@algolia/client-personalization': 5.46.2 + '@algolia/client-query-suggestions': 5.46.2 + '@algolia/client-search': 5.46.2 + '@algolia/ingestion': 1.46.2 + '@algolia/monitoring': 1.46.2 + '@algolia/recommend': 5.46.2 + '@algolia/requester-browser-xhr': 5.46.2 + '@algolia/requester-fetch': 5.46.2 + '@algolia/requester-node-http': 5.46.2 + + birpc@2.9.0: {} + + ccount@2.0.1: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + comma-separated-tokens@2.0.3: {} + + copy-anything@4.0.5: + dependencies: + is-what: 5.5.0 + + csstype@3.2.3: {} + + dequal@2.0.3: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + entities@7.0.0: {} + + esbuild@0.27.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 + + estree-walker@2.0.2: {} + + eventsource-parser@3.0.6: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + focus-trap@7.7.1: + dependencies: + tabbable: 6.4.0 + + fsevents@2.3.3: + optional: true + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hookable@5.5.3: {} + + htm@3.1.1: {} + + html-void-elements@3.0.0: {} + + is-what@5.5.0: {} + + json-schema@0.4.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + mark.js@8.11.1: {} + + marked@16.4.2: {} + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-encode@2.0.1: {} + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + minisearch@7.2.0: {} + + mitt@3.0.1: {} + + nanoid@3.3.11: {} + + oniguruma-parser@0.12.1: {} + + oniguruma-to-es@4.3.4: + dependencies: + oniguruma-parser: 0.12.1 + regex: 6.1.0 + regex-recursion: 6.0.2 + + perfect-debounce@2.0.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + property-information@7.1.0: {} + + react@19.2.3: {} + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + + rfdc@1.4.1: {} + + rollup@4.55.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.55.1 + '@rollup/rollup-android-arm64': 4.55.1 + '@rollup/rollup-darwin-arm64': 4.55.1 + '@rollup/rollup-darwin-x64': 4.55.1 + '@rollup/rollup-freebsd-arm64': 4.55.1 + '@rollup/rollup-freebsd-x64': 4.55.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.55.1 + '@rollup/rollup-linux-arm-musleabihf': 4.55.1 + '@rollup/rollup-linux-arm64-gnu': 4.55.1 + '@rollup/rollup-linux-arm64-musl': 4.55.1 + '@rollup/rollup-linux-loong64-gnu': 4.55.1 + '@rollup/rollup-linux-loong64-musl': 4.55.1 + '@rollup/rollup-linux-ppc64-gnu': 4.55.1 + '@rollup/rollup-linux-ppc64-musl': 4.55.1 + '@rollup/rollup-linux-riscv64-gnu': 4.55.1 + '@rollup/rollup-linux-riscv64-musl': 4.55.1 + '@rollup/rollup-linux-s390x-gnu': 4.55.1 + '@rollup/rollup-linux-x64-gnu': 4.55.1 + '@rollup/rollup-linux-x64-musl': 4.55.1 + '@rollup/rollup-openbsd-x64': 4.55.1 + '@rollup/rollup-openharmony-arm64': 4.55.1 + '@rollup/rollup-win32-arm64-msvc': 4.55.1 + '@rollup/rollup-win32-ia32-msvc': 4.55.1 + '@rollup/rollup-win32-x64-gnu': 4.55.1 + '@rollup/rollup-win32-x64-msvc': 4.55.1 + fsevents: 2.3.3 + + search-insights@2.17.3: {} + + shiki@3.21.0: + dependencies: + '@shikijs/core': 3.21.0 + '@shikijs/engine-javascript': 3.21.0 + '@shikijs/engine-oniguruma': 3.21.0 + '@shikijs/langs': 3.21.0 + '@shikijs/themes': 3.21.0 + '@shikijs/types': 3.21.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + source-map-js@1.2.1: {} + + space-separated-tokens@2.0.2: {} + + speakingurl@14.0.1: {} + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + superjson@2.2.6: + dependencies: + copy-anything: 4.0.5 + + swr@2.3.8(react@19.2.3): + dependencies: + dequal: 2.0.3 + react: 19.2.3 + use-sync-external-store: 1.6.0(react@19.2.3) + + tabbable@6.4.0: {} + + throttleit@2.1.0: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + trim-lines@3.0.1: {} + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + use-sync-external-store@1.6.0(react@19.2.3): + dependencies: + react: 19.2.3 + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + vite@7.3.1: + dependencies: + esbuild: 0.27.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.55.1 + tinyglobby: 0.2.15 + optionalDependencies: + fsevents: 2.3.3 + + vitepress@2.0.0-alpha.15(@algolia/client-search@5.46.2)(postcss@8.5.6)(react@19.2.3)(search-insights@2.17.3): + dependencies: + '@docsearch/css': 4.4.0 + '@docsearch/js': 4.4.0(@algolia/client-search@5.46.2)(react@19.2.3)(search-insights@2.17.3) + '@iconify-json/simple-icons': 1.2.65 + '@shikijs/core': 3.21.0 + '@shikijs/transformers': 3.21.0 + '@shikijs/types': 3.21.0 + '@types/markdown-it': 14.1.2 + '@vitejs/plugin-vue': 6.0.3(vite@7.3.1)(vue@3.5.26) + '@vue/devtools-api': 8.0.5 + '@vue/shared': 3.5.26 + '@vueuse/core': 14.1.0(vue@3.5.26) + '@vueuse/integrations': 14.1.0(focus-trap@7.7.1)(vue@3.5.26) + focus-trap: 7.7.1 + mark.js: 8.11.1 + minisearch: 7.2.0 + shiki: 3.21.0 + vite: 7.3.1 + vue: 3.5.26 + optionalDependencies: + postcss: 8.5.6 + transitivePeerDependencies: + - '@algolia/client-search' + - '@types/node' + - '@types/react' + - async-validator + - axios + - change-case + - drauu + - fuse.js + - idb-keyval + - jiti + - jwt-decode + - less + - lightningcss + - nprogress + - qrcode + - react + - react-dom + - sass + - sass-embedded + - search-insights + - sortablejs + - stylus + - sugarss + - terser + - tsx + - typescript + - universal-cookie + - yaml + + vue@3.5.26: + dependencies: + '@vue/compiler-dom': 3.5.26 + '@vue/compiler-sfc': 3.5.26 + '@vue/runtime-dom': 3.5.26 + '@vue/server-renderer': 3.5.26(vue@3.5.26) + '@vue/shared': 3.5.26 + + zod@4.3.5: {} + + zwitch@2.0.4: {} diff --git a/doc-ng/public/admin_dashboard.png b/doc-ng/public/admin_dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..484026c59ec8671ddd5c2e254b341a744f0c9d79 GIT binary patch literal 94291 zcmZ_01ymesvo#!GfWZQU;O;?!ySoGpPH=a3cbCBl?g4^JaCbs*cXxM(f6jgHd;fEC z@~zdg=$Yy6d8BGr)vhYSGH%fj5#Of=aFsM;XxR3DdWIqdj!X%TzfZLWxL#McLW<5Y#}JmmZNSC?%yU zdbV(gqOTI6*&oP@9780ESwbCmPkLT?ZkM^axqo3aAJH8-@MWdcTYIkxHX`q)A1>6I zS&W^6l6B>${NcpYCnbCVe>m2Dk*smG-=439dx=4F))@EA$Gm~dmmspWjmN{&WbI}0 zlz=1?_{U*_1Hs-RVEWsq|NAxu7g&TML<;ea=--FmlAu5kit5Q%Dig_nJn_Hx9>O&Q z&~XKAJ?KC{fQRw>AwWmG<_!JZX!t#y;$L5aFBh<;>=^sH^n$SVB>C^j5@Dj+!Zuh) z)44CFQ~%-iEo(??_V|LG6P-6Ea(@_JW2kR%AHZSUkyH84lam&3i8|lfxpz4EXhJXvuHPF-5OOFX8==L13msLw z4u6mc&x7Z!UaVD!fAzMhd~}Yce@=O^iOt!+B;Y^uOA%O02`zt_X^2SO*r{rtKR0p9Uz<2Ummw z*-QKjpA^bUsD&YienGSOz}l@cO7%;|@jDwP+{vE3XMN~%yTSWuo{RP&-fCMdc^H}V zq2wVvxkTH~?5vSQb0WOpWg;K;G~={mLu8jm=>=K`DU&|SZNz7u-A8LmDs>Q7qf z3t$o=>NMHhO7ji%v&9M}$|HzOgi+Som@LBB!-U-jhh6B8A-VtY8&?oZGhe%;YgeK8+RogO-?Z z`61q8bfmq>syK~|Z9><2x9h;eMP#fdxM>KH%hw`YStD&HQ^fW3f<+ZSv}r)Z;pJ!$ z>FMkcsY|mF`4v5(iT!He>j|U1o6&Be?x{(>)eQL`kR<(62QILyb!`h{`ix?rA`|!Z z1eMgk=RgQw8}IC-!+!yuq(L@9{T#4u7hP9bgJ?Ni=i9UQg*Pt~0ZY|V=+SicCa6(O zN(x(pt&wzLU_h<=Q24d;ZejLX;&uqHQ~%`)$g^cES)XM7jE$r}tcsXyXWoKWp*Uaa zf;yp?upiEujW1K4qu?R-ShRWMp;24U28y%gDyAqZ> z?*lE&$!AMw90WsS&*2j9tV` zUdQ_~#ZMmtWYg;K@vPJ}_&v2{hX#E@Zk?E}yC1_#ZFUnqPd$bHu!~e^XI#yF91#Zz z5t(=Ig&+~Ni51yk41Ye_T}Uv4c$P*;w^`i2Umy5p>-D~EUUN352p(JG!)W^uauEUw zLrleqne`4!e#Ws6;EOi>`fG5fzXK9?vrK!8Tnon|Uyn8U{nS~UOtb&RP`GcTT253+ zCvL7JcDCOhA3^wxU`ZD4uz4LTvD^Ja~ z68YF&^BrT)nCSLrx<0zl-VQwL^bT3c@Lmik6AWUesqI|210~M~*qI^iZ;S56a`~$Q zCtnhkmnTFI4R+H>pjqrn>DKCxF;m*8iL$7Zf4~M9^BGqWs?0}Mn5owCbTj4O?KG8>lg`#QigBXl0AOCa9C0({~C;LRy5&9N;1R-V|2&U z9;vYjZ~Pw*7UpW8ZGR6% z+l~9!9Kdux>Wi3h$lfJy=(YG3^lv>Qi@||V4dAouO!SDqb(AQFa5XLUrUE5={;YSk z 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 @fw$7IdUjO3$x$fL` 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 BH;DiM literal 0 HcmV?d00001 diff --git a/doc-ng/public/user_webmail.png b/doc-ng/public/user_webmail.png new file mode 100644 index 0000000000000000000000000000000000000000..f670759f22c87db5d46134884d6db05495abe2c7 GIT binary patch literal 45446 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 sA +$ (env) $ python manage.py migrate +$ (env) $ python manage.py collectstatic +``` + +You will then see a message similar to: + +``` txt +You have requested to collect static files at the destination +location as specified in your settings: + + /srv/modoboa/instance/sitestatic + +This will overwrite existing files! +Are you sure you want to do this? + +Type 'yes' to continue, or 'no' to cancel: +``` + +You will want to answer `yes` here then simply restart +the `uwsgi` process with `service uwsgi restart` +and you should be up and running again. + +Simply log into your modoboa web panel and verify that +your extensions and webmail box is working. + +## Information + +[Rebuild instructions from](https://help.pythonanywhere.com/pages/RebuildingVirtualenvs/) diff --git a/doc-ng/rest_api.md b/doc-ng/rest_api.md new file mode 100644 index 000000000..b856c2e02 --- /dev/null +++ b/doc-ng/rest_api.md @@ -0,0 +1,35 @@ +# REST API + +To ease the integration with external sources (software or other), Modoboa provides a REST API. + +Every installed instance comes with a ready-to-use API and a documentation. + +**You will find them using the following url patterns:** + +- New API+Docs: *https://\/api/schema-v2/swagger/* +- Old API: *http://\/api/v1/* + +::: info +You need to be logged in for the API docs to show. +::: + +**Examples:** + +* [new example](https://demo.modoboa.org/api/schema-v2/swagger/) + +documentation is available on the official demo. + +Using this API requires an authentication and for now, only a token +based authentication is supported. To get a valid token, log-in to your +instance with a super administrator, go to *Settings \> API* and +activate the API access. Press the Update button and wait until the page +is reloaded, the token will be displayed. + +![image](/api_access_form.png){.align-center} + +To make valid API calls, every requests you send must embed this token +within an Authorization HTTP header like this: + + Authorization: Token + +and the content type of those requests must be `application/json`. \ No newline at end of file diff --git a/doc-ng/upgrade.md b/doc-ng/upgrade.md new file mode 100644 index 000000000..266523e69 --- /dev/null +++ b/doc-ng/upgrade.md @@ -0,0 +1,2241 @@ +# Upgrade + +## Modoboa + +::: tip + +In this doc, `by default` means that you used [modoboa +installer](https://github.com/modoboa/modoboa-installer) to install +modoboa and that your didn\'t change your configuration. +::: + +::: warning +The new version you are going to install may need to modify your +database. Before you start, make sure to backup everything! +::: + +Most of the time, upgrading your installation to a newer Modoboa version +only requires a few actions. In every case, you will need to apply the +specific actions if the version you are installing requires it, and then +apply the general ones. + +In case you use a dedicated user and/or a virtualenv, do not forget to +use them: + +``` bash +> sudo -u -i bash +> source /bin/activate +``` + +::: info +Check the `specific_upgrade_instructions`{.interpreted-text role="ref"} +before running the following commands. +::: + +Then, run the following commands: + +``` bash +> pip install modoboa== +> cd +> python manage.py migrate +> python manage.py collectstatic +> python manage.py check --deploy +> python manage.py load_initial_data +``` + +Then when everything has been updated, restart modoboa\'s services: + +``` bash +# sudo systemctl restart uwsgi +# sudo systemctl restart supervisor +``` + +Finally, restart your web server. + +Sometimes, you might need to upgrade postfix map files too. To do so, +just run the `generate_postfix_maps` command on the same directory than +the one used for installation (`/etc/postfix`{.interpreted-text +role="file"} by default). + +Make sure to use root privileges and run the following command: + +``` bash +> python manage.py generate_postfix_maps --destdir +``` + +Then, reload postfix. + +## Modoboa installer + +It is important to keep the installer updated because it works on +rolling release. To do that, go to modoboa-installer directory then: + +``` bash +> git fetch && git pull +``` + +If some error appears, you may try this: + +``` bash +> git reset --hard origin/master +``` + +## Extensions + +:::: warning +::: title +Warning +::: + +Do not open an issue after an upgrade if you have not updated your +extensions. +:::: + +If a new version is available for an extension you\'re using, it is +recommanded to install it. It may even be required. Upgrading an +extensions is pretty and the procedure is almost the same than the one +used for Modoboa. + +In case you use a dedicated user and/or a virtualenv, do not forget to +use them: + +``` bash +> sudo -u -i bash +> source /bin/activate +``` + +Then, run the following commands: + +``` bash +> pip install == +> cd +> python manage.py migrate +> python manage.py collectstatic +> python manage.py check --deploy +``` + +Finally, restart your web server. + +It is a generic upgrade procedure which will be enough most of the time +but it is generally a good idea to check the associated documentation. + +## Rebuild Virtual Environment + +## Specific instructions {#specific_upgrade_instructions} + +### Version 2.6.1 + +#### Required changes to `settings.py`{.interpreted-text role="file"} + +- Add the following lines to `LOGGING` variable: + + ``` python + 'handlers': { + ... + 'syslog': { + 'class': 'logging.handlers.SysLogHandler', + 'facility': SysLogHandler.LOG_SYSLOG, + 'formatter': 'syslog' + }, + ... + }, + 'loggers': { + ... + 'modoboa.dns': { + 'handlers': ['syslog'], + 'level': 'INFO', + 'propagate': False + }, + ... + } + ``` + +### Version 2.6.0 + +New built-in auto-configuration service. + +#### Required changes to `settings.py`{.interpreted-text role="file"} + +- Add the following line to `MODOBOA_APPS` variable: + + ``` python + 'modoboa.autoconfig' + ``` + +- Add the following hostnames to `ALLOWED_HOSTS` variable: + + ``` python + 'autoconfig.', + 'autodiscover.', + ``` + +- Add the following variable: + + ``` python + EMAIL_CLIENT_CONNECTION_SETTINGS = { + "imap": { + "HOSTNAME": "", + "SOCKET_TYPE": "SSL", + "PORT": 993, + }, + "smtp": {"HOSTNAME": "", "SOCKET_TYPE": "STARTTLS", "PORT": 587}, + } + ``` + +- Remove `rest_framework.authentication.SessionAuthentication` from + `REST_FRAMEWORK` variable: + + ``` python + REST_FRAMEWORK = { + # Stuff before... + "DEFAULT_AUTHENTICATION_CLASSES": ( + "oauth2_provider.contrib.rest_framework.OAuth2Authentication", + "rest_framework.authentication.TokenAuthentication", + ), + # Stuff after... + } + ``` + +### Version 2.5.0 + +Amavis plugin integrated into Modoboa core repository. If you are using +Amavis content filter and the quarantine plugin, the following upgrade +instructions, otherwise proceed with regular upgrade instructions. + +#### Pre update + +- If you are using Postegresql (default option with the installer), run + as a root user: + + ``` bash + > sudo -u postgres -i psql -d "modoboa" -c "UPDATE django_content_type SET app_label='amavis' WHERE app_label='modoboa_amavis'" + > sudo -u postgres -i psql -d "modoboa" -c "UPDATE django_migrations SET app='amavis' WHERE app='modoboa_amavis'" + ``` + +- If you are using Mysql, run as a root user: + + ``` bash + > echo "UPDATE django_content_type SET app_label='amavis' WHERE app_label='modoboa_amavis'" | mysql -u root -p modoboa + > echo "UPDATE django_migrations SET app='amavis' WHERE app='modoboa_amavis'" | mysql -u root -p modoboa + ``` + +#### Required changes to `settings.py`{.interpreted-text role="file"} + +- Rename the following extensions in the `MODOBOA_APPS` variable: + + - `modoboa_amavis` -\> `modoboa.amavis` + +- Add the following variables: + + ``` python + DATABASE_ROUTERS = ["modoboa.amavis.dbrouter.AmavisRouter"] + + AMAVIS_DEFAULT_DATABASE_ENCODING = "LATIN1" # or any value matching your database config + ``` + +- Remove configuration block related to `modoboa_amavis` plugin (these + should be at the end of the file) + +#### Post update + +- Run the following command: + + $ pip uninstall modoboa_amavis + +### Version 2.4.2 + +#### Changes to `settings.py`{.interpreted-text role="file"} + +- Make sure `MODOBOA_APPS` variable contains the following line: + + ``` python + 'modoboa.webmail' + ``` + +### Version 2.4.0 + +:::: warning +::: title +Warning +::: + +The `modoboa-amavis` extension has not been migrated into the core +Modoboa package yet. If you are using it, **you cannot install version +2.4.0 yet**. +:::: + +Legacy web interface has been removed and almost all extensions have +been integrated to the core Modoboa package. + +#### Pre update + +- If you are using Postegresql (default option with the installer), run + as a root user: + + ``` bash + > sudo -u postgres -i psql -d "modoboa" -c "UPDATE django_content_type SET app_label='autoreply' WHERE app_label='postfix_autoreply'" + > sudo -u postgres -i psql -d "modoboa" -c "UPDATE django_migrations SET app='autoreply' WHERE app='postfix_autoreply'" + > sudo -u postgres -i psql -d "modoboa" -c "UPDATE django_content_type SET app_label='contacts' WHERE app_label='modoboa_contacts'" + > sudo -u postgres -i psql -d "modoboa" -c "UPDATE django_migrations SET app='contacts' WHERE app='modoboa_contacts'" + > sudo -u postgres -i psql -d "modoboa" -c "UPDATE django_content_type SET app_label='calendars' WHERE app_label='modoboa_radicale'" + > sudo -u postgres -i psql -d "modoboa" -c "UPDATE django_migrations SET app='calendars' WHERE app='modoboa_radicale'" + ``` + +- If you are using Mysql, run as a root user: + + ``` bash + > echo "UPDATE django_content_type SET app_label='autoreply' WHERE app_label='postfix_autoreply'" | mysql -u root -p modoboa + > echo "UPDATE django_migrations SET app='autoreply' WHERE app='postfix_autoreply'" | mysql -u root -p modoboa + > echo "UPDATE django_content_type SET app_label='contacts' WHERE app_label='modoboa_contacts'" | mysql -u root -p modoboa + > echo "UPDATE django_migrations SET app='contacts' WHERE app='modoboa_contacts'" | mysql -u root -p modoboa + > echo "UPDATE django_content_type SET app_label='calendars' WHERE app_label='modoboa_radicale'" | mysql -u root -p modoboa + > echo "UPDATE django_migrations SET app='calendars' WHERE app='modoboa_radicale'" | mysql -u root -p modoboa + ``` + +:::: warning +::: title +Warning +::: + +MariaDB 10.7+ users might have login issues (see this +[discussion](https://github.com/modoboa/modoboa/discussions/3582)). For +those impacted, run the followed SQL queries: + +``` bash +> echo "ALTER TABLE `oauth2_provider_idtoken` MODIFY `jti` uuid;" | mysql -u root -p modoboa +> echo "ALTER TABLE `oauth2_provider_refreshtoken` MODIFY `token_family` uuid;" | mysql -u root -p modoboa +``` +:::: + +#### Required changes to `settings.py`{.interpreted-text role="file"} + +- Rename the following extensions in the `MODOBOA_APPS` variable: + + - `modoboa.postfix_autoreply` -\> `modoboa.autoreply` + - `modoboa_contacts` -\> `modoboa.contacts` + - `modoboa_radicale` -\> `modoboa.calendars` + - `modoboa_webmail` -\> `modoboa.webmail` + - `modoboa_rspamd` -\> `modoboa.rspamd` + +- Remove configuration blocks related to `modoboa_contacts` and + `modoboa_radicale` plugins (these should be at the end of the file) + +- Remove `AjaxLoginRedirect` from `MIDDLEWARE` variable: + + > ``` python + > MIDDLEWARE = ( + > 'django.contrib.sessions.middleware.SessionMiddleware', + > 'django.middleware.locale.LocaleMiddleware', + > 'x_forwarded_for.middleware.XForwardedForMiddleware', + > 'corsheaders.middleware.CorsMiddleware', + > 'django.middleware.common.CommonMiddleware', + > 'django.middleware.csrf.CsrfViewMiddleware', + > 'django.contrib.auth.middleware.AuthenticationMiddleware', + > 'django.contrib.messages.middleware.MessageMiddleware', + > 'django.middleware.clickjacking.XFrameOptionsMiddleware', + > 'modoboa.core.middleware.LocalConfigMiddleware', + > 'modoboa.lib.middleware.CommonExceptionCatcher', + > 'modoboa.lib.middleware.RequestCatcherMiddleware', + > ) + > ``` + +- Remove `NEW_ADMIN_URL` variable if found + +- Remove `new_admin_url` and `top_notifications` context processors + +- Remove `ckeditor` related apps from `INSTALLED_APPS` variable and + `CKEDITOR_*` settings + +- Remove `STATICFILE_DIRS` variable + +Radicale configuration changes +\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-- + +You must enable oauth2 authentication in Radicale to make the calendar +and contact feature work. Follow this +`guide `{.interpreted-text role="ref"}. + +#### Post update + +- Run the following command: + + $ python manage.py load_initial_data + +- Update your `webserver `{.interpreted-text role="ref"} + configuration + +- Remove `autoreply` service definition from postfix\`s + `master.cf`{.interpreted-text role="file"} file if you were using it + +- Make sure + `/etc/uwsgi/sites-enabled/`{.interpreted-text + role="file"} contains [max-requests=5000]{.title-ref} and that + [process]{.title-ref} is superior or equal to 4. Don\'t forget to + restart uwsgi service after the configuration change. + +- Then proceed with regular upgrade instructions + +### Version 2.3.3 + +:::: warning +::: title +Warning +::: + +The `crypt ({CRYPT})` password scheme has been marked as deprecated and +will be removed from Modoboa starting with version **2.4.0**. If you are +still using this weak scheme, you must change to something safer +**ASAP**. Installations still using this scheme won\'t be able to +install Modoboa 2.4 and upper\... +:::: + +Update your `settings.py`{.interpreted-text role="file"} file with +following content: + +``` python +SILENCED_SYSTEM_CHECKS = [ + "security.W019", # modoboa uses iframes to display e-mails + "ckeditor.W001", # CKEditor 4.22.1 warning +] +``` + +This will silence a [warning sent by +django-ckeditor](https://github.com/modoboa/modoboa/issues/3316). + +### Version 2.3.0 + +:::: warning +::: title +Warning +::: + +For this particular version, it is really important that you apply new +migrations **AFTER** the following instructions. If you don\'t, you\'ll +get into trouble\... +:::: + +#### Pre update + +:::: note +::: title +Note +::: + +To check that you were actually using modoboa-postfix-autoreply you can +check if this command returns something: + +``` bash +cat /srv/modoboa/instance/instance/settings.py | grep "modoboa_postfix_autoreply" +``` +:::: + +If you were using +[modoboa-postfix-autoreply](https://github.com/modoboa/modoboa-postfix-autoreply) +extension, you need to perfome a few commands: + +- If you are using Postegresql (default option with the installer), run + as a root user: + + ``` bash + > sudo -u postgres -i psql -d "modoboa" -c "UPDATE django_content_type SET app_label='postfix_autoreply' WHERE app_label='modoboa_postfix_autoreply'" + > sudo -u postgres -i psql -d "modoboa" -c "UPDATE django_migrations SET app='postfix_autoreply' WHERE app='modoboa_postfix_autoreply'" + ``` + +- If you are using Mysql, run as a root user: + + ``` bash + > echo "UPDATE django_content_type SET app_label='postfix_autoreply' WHERE app_label='modoboa_postfix_autoreply'" | mysql -u root -p modoboa + > echo "UPDATE django_migrations SET app='postfix_autoreply' WHERE app='modoboa_postfix_autoreply'" | mysql -u root -p modoboa + ``` + +#### Required changes to `settings.py`{.interpreted-text role="file"} + +Some changes are required to your `settings.py`{.interpreted-text +role="file"} file. These changes will not be performed by the installer. +The location of this file is `/srv/modoboa/instance/instance` by +default. + +- Append `'import environ'` after `'import os'`: + + ``` python + import os + import environ + ``` + +- Append the following lines after the line starting with `'BASE_DIR'`: + + ``` python + # Build paths inside the project like this: os.path.join(BASE_DIR, ...) + BASE_DIR = os.path.realpath(os.path.dirname(os.path.dirname(__file__))) + env = environ.Env() + environ.Env.read_env(os.path.join(BASE_DIR, '.env')) + ``` + +- Add `oauth2_provider` and `corsheaders` to the `INSTALLED_APPS` list: + + ``` python + INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.sites', + 'django.contrib.staticfiles', + 'reversion', + 'ckeditor', + 'ckeditor_uploader', + 'oauth2_provider', + 'corsheaders', + 'rest_framework', + 'rest_framework.authtoken', + 'drf_spectacular', + 'phonenumber_field', + 'django_otp', + 'django_otp.plugins.otp_totp', + 'django_otp.plugins.otp_static', + 'django_rename_app', + 'django_rq', + ) + ``` + +- The `modoboa-postfix-autoreply` and `modoboa-sievefilters` plugins + have been merged into the core. + + Add `'modoboa.postfix_autoreply'` and `modoboa.sievefilters` to + `MODOBOA_APPS`: + + ``` python + MODOBOA_APPS = ( + 'modoboa', + 'modoboa.core', + 'modoboa.lib', + 'modoboa.admin', + 'modoboa.transport', + 'modoboa.relaydomains', + 'modoboa.limits', + 'modoboa.parameters', + 'modoboa.dnstools', + 'modoboa.policyd', + 'modoboa.maillog', + 'modoboa.dmarc', + 'modoboa.pdfcredentials', + 'modoboa.imap_migration', + 'modoboa.postfix_autoreply', + 'modoboa.sievefilters', + ) + ``` + + :::: warning + ::: title + Warning + ::: + + Remove any reference to `'modoboa_postfix_autoreply'` AND + `modoboa_sievefilters` in this same variable. + :::: + +- Due to the changes to the authentication flow, you need to replace + MIDDLEWARE with the following: + + ``` python + MIDDLEWARE = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', + 'x_forwarded_for.middleware.XForwardedForMiddleware', + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'modoboa.core.middleware.LocalConfigMiddleware', + 'modoboa.lib.middleware.AjaxLoginRedirect', + 'modoboa.lib.middleware.CommonExceptionCatcher', + 'modoboa.lib.middleware.RequestCatcherMiddleware', + ) + ``` + +- Replace `'modoboa.core.drf_authentication.JWTAuthenticationWith2FA'` + by `'oauth2_provider.contrib.rest_framework.OAuth2Authentication'` at + the top of the list of `DEFAULT_AUTHENTICATION_CLASSES` (inside + `REST_FRAMEWORK` section): + + ``` python + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'oauth2_provider.contrib.rest_framework.OAuth2Authentication', + 'rest_framework.authentication.TokenAuthentication', + 'rest_framework.authentication.SessionAuthentication', + ), + ``` + +Save your modifications at this point and run the `post upgrade +commands `{.interpreted-text role="ref"} now. + +Then, apply this last modification to your settings: + +- Run the commands indicated + `in the first section `{.interpreted-text + role="ref"} then add the following content after `MEDIA_ROOT`: + + ``` python + # oAuth2 settings + + OAUTH2_PROVIDER = { + 'OIDC_ENABLED': True, + 'OIDC_RP_INITIATED_LOGOUT_ENABLED': True, + 'OIDC_RP_INITIATED_LOGOUT_ALWAYS_PROMPT': True, + 'OIDC_RSA_PRIVATE_KEY': env.str('OIDC_RSA_PRIVATE_KEY', multiline=True), + 'SCOPES': { + 'openid': 'OpenID Connect scope', + 'read': 'Read scope', + 'write': 'Write scope', + 'introspection': 'Introspect token scope', + }, + 'DEFAULT_SCOPES': ['openid', 'read', 'write'], + } + + # If CORS fail, you might want to try to set it to True + #CORS_ORIGIN_ALLOW_ALL = False + ``` + +#### Docevot configuration changes + +You must enable oauth2 authentication in Dovecot to make the filters +editor work in the new UI. Follow this +`guide `{.interpreted-text role="ref"}. + +#### Post update + +Run this command to be sure to have everything set. + +``` bash +python manage.py modo repair +``` + +You need to set the `buffer-size` to `8192` in uwsgi config. Edit the +value in `/etc/uwsgi/apps-available/automx_instance.ini` to match at +least 8192 or add it if it was not set: + +``` bash +buffer-size = 8192 +``` + +#### SORBS DNS + +> :::: note +> ::: title +> Note +> ::: +> +> The installer will take care of that if you use it to upgrade. +> :::: +> +> [SORBS DNS has been +> deprecated](https://www.mail-archive.com/mailop@mailop.org/msg22064.html). +> If you were using them with postfix, you need to update your +> configuration by removing any reference to it. For example: +> +> # Use some DNSBL +> postscreen_dnsbl_sites = +> zen.spamhaus.org=127.0.0.[2..11]*3 +> bl.spameatingmonkey.net=127.0.0.2*2 +> bl.spamcop.net=127.0.0.2 + +### Version 2.2.3 + +- Reduced the number of call to doveadm + +You need to setup a new worker : + +1. Create a new supervisord config + (`/etc/supervisor/conf.d/modoboa-base-worker.conf` by default) : + +``` ini +[program:modoboa-base-worker] +autostart=true +autorestart=true +command={%python env path%} {% manage.py instance path%} rqworker modoboa +directory={%modoboa home dir%} +user=%{modoboa user} +numprocs=1 +stopsignal=TERM +``` + +`python env path` : Python executable located in your virtual +environment created for modoboa. You will find it here +`/srv/modoboa/venv/bin/python` by default. `manage.py instance path`: +Path to manage.py of your modoboa instance. You will find it here : +`/srv/modoboa/instance/manage.py`by default. `Modoboa home dir`: Home +dir of the user running modooba. You will find it here `/srv/modoboa/` +by default. `modoboa user` : User managing dkim signing (modoboa by +default). + +You can help you with `/etc/supervisor/conf.d/policyd.conf` (by +default). + +Then restart supervisor. +`#> supervisorctl reread && supervisorctl update` on Debian. + +2. Update the RQ_QUEUES section bellow the `#REDIS` section: + +``` python +# RQ +RQ_QUEUES = { + ... + }, + 'modoboa': { + 'HOST': REDIS_HOST, + 'PORT': REDIS_PORT, + 'DB': 0, + }, +} +``` + +3. Add the CACHE section bellow the `# RQ` one: + +``` python +# CACHE +CACHES = { + "default": { + "BACKEND": 'django.core.cache.backends.redis.RedisCache', + "LOCATION": REDIS_URL + } +} +``` + +### Version 2.2.0 + +:::: note +::: title +Note +::: + +If you use the installer to perform the upgrade, you NEED to update it. +See above for instructions. After updating the installer and modoboa +with it, you only need to edit `urls.py` and `settings.py` +:::: + +- Django has been updated to 4.2LTS. Please upgrade all your extensions + alongside modoboa +- Support for Python 3.7 has been dropped, minimum Python version is now + 3.8 +- Support for Postgres 11 has been dropped, minimum Postgres version is + now 12 + +You need to change the first line in `urls.py` and replace +[url]{.title-ref} with [path]{.title-ref} (in +`/srv/modoboa/instance/instance/urls.py` by default): + +``` python +# from django.conf.urls import include, url +from django.urls import include, path + +urlpatterns = [ + path(r'', include('modoboa.urls')), +] +``` + +If you use Postgresql, you need to install pyscopg3+: + +``` bash +pip install psycopg[binary]>=3.1 +``` + +RQ has been added. This should replace the use of cron jobs in the +future. For now, only `manage_dkim_keys` has been migrated. This will +make the dkim key generation asynchronous but the task will be started +as soon as the generation is required. + +Follow these instructions to perform the update in case you used +supervisord for the installation (this apply if you used +`modoboa-installer`): + +1. Edit settings.py (`/srv/modoboa/instance/instance/settings.py` by + default) and add: + +``` python +INSTALLED_APPS = ( + ..., + 'django_rq', +) +``` + +and add the RQ section bellow the `#REDIS` section: + +``` python +# RQ +RQ_QUEUES = { + 'dkim': { + 'HOST': REDIS_HOST, + 'PORT': REDIS_PORT, + 'DB': 0, + }, +} +``` + +Then by default you will need to restart `uwsgi` service +(`systemctl restart uwsgi` on Debian). + +2. Create a new supervisord config + (`/etc/supervisor/conf.d/modoboa-worker.conf` by default) : + +``` ini +[program:modoboa-worker] +autostart=true +autorestart=true +command={%python env path%} {% manage.py instance path%} rqworker dkim +directory={%modoboa home dir%} +user=%{dkim manager user} +numprocs=1 +stopsignal=TERM +``` + +`python env path` : Python executable located in your virtual +environment created for modoboa. You will find it here +`/srv/modoboa/venv/bin/python` by default. `manage.py instance path`: +Path to manage.py of your modoboa instance. You will find it here : +`/srv/modoboa/instance/manage.py`by default. `Modoboa home dir`: Home +dir of the user running modooba. You will find it here `/srv/modoboa/` +by default. `dkim manager user` : User managing dkim signing (opendkim +by default). + +You can help you with `/etc/supervisor/conf.d/policyd.conf` (by +default). + +Then restart supervisor. +`#> supervisorctl reread && supervisorctl update` on Debian. + +Admins now have the option to setup a send-only mailbox. + +Send-only mailboxes do not have access to IMAP. You need to change your +dovecot configuration to enable it. + +:::: note +::: title +Note +::: + +modoboa-webmail may not work with send-only user for now. +:::: + +If you use `Postgres`: Change `user_query` and `password_query` in +`/etc/dovecot/dovecot-sql.conf.ext`: + + user_query = SELECT '%{home_dir}/%%d/%%n' AS home, %mailboxes_owner_uid as uid, %mailboxes_owner_gid as gid, '*:bytes=' || mb.quota || 'M' AS quota_rule FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN core_user u ON u.id=mb.user_id WHERE (mb.is_send_only IS NOT TRUE OR '%s' NOT IN ('imap', 'pop3', 'lmtp')) AND mb.address='%%n' AND dom.name='%%d' + + password_query = SELECT email AS user, password, '%{home_dir}/%%d/%%n' AS userdb_home, %mailboxes_owner_uid AS userdb_uid, %mailboxes_owner_gid AS userdb_gid, CONCAT('*:bytes=', mb.quota, 'M') AS userdb_quota_rule FROM core_user u INNER JOIN admin_mailbox mb ON u.id=mb.user_id INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE (mb.is_send_only IS NOT TRUE OR '%s' NOT IN ('imap', 'pop3')) AND email='%%u' AND is_active AND dom.enabled + +You basically simply need to add +`(mb.is_send_only IS NOT TRUE OR '%s' NOT IN ('imap', 'pop3', 'lmtp')) AND` +for `user_query` after `WHERE` and +`(mb.is_send_only IS NOT TRUE OR '%s' NOT IN ('imap', 'pop3')) AND` for +`password_query` after `WHERE`. + +If you use `MySQL`: Change `user_query` and `password_query` in +`/etc/dovecot/dovecot-sql-master.ext`: + + user_query = SELECT '%{home_dir}/%%d/%%n' AS home, %mailboxes_owner_uid as uid, %mailboxes_owner_gid as gid, CONCAT('*:bytes=', mb.quota, 'M') AS quota_rule FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN core_user u ON u.id=mb.user_id WHERE (mb.is_send_only=0 OR '%s' NOT IN ('imap', 'pop3', 'lmtp')) AND mb.address='%%n' AND dom.name='%%d' + + password_query = SELECT email AS user, password, '%{home_dir}/%%d/%%n' AS userdb_home, %mailboxes_owner_uid AS userdb_uid, %mailboxes_owner_gid AS userdb_gid, CONCAT('*:bytes=', mb.quota, 'M') AS userdb_quota_rule FROM core_user u INNER JOIN admin_mailbox mb ON u.id=mb.user_id INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE (mb.is_send_only=0 OR '%s' NOT IN ('imap', 'pop3')) AND u.email='%%u' AND u.is_active=1 AND dom.enabled=1 + +You basically simply need to add +`(mb.is_send_only=0 OR '%s' NOT IN ('imap', 'pop3', 'lmtp')) AND` for +`user_query` after `WHERE` and +`(mb.is_send_only=0 OR '%s' NOT IN ('imap', 'pop3'))` for +`password_query` after `WHERE`. + +### Version 2.1.0 + +:::: warning +::: title +Warning +::: + +For this particular version, it is really important that you apply new +migrations **AFTER** the following instructions. If you don\'t, you\'ll +get into trouble\... +:::: + +The `modoboa-dmarc`, `modoboa-pdfcredentials` and +`modoboa-imapmigration` plugins have been merged into the core. + +Add `'django_rename_app,'` To `INSTALLED_APPS`: + +``` python +INSTALLED_APPS = ( +..., +'django_rename_app', +) +``` + +Add `'modoboa.dmarc'`, `'modoboa.pdfcredentials'` and +`'modoboa.imap_migration'` to `MODOBOA_APPS`: + +``` python +MODOBOA_APPS = ( + 'modoboa', + 'modoboa.core', + 'modoboa.lib', + 'modoboa.admin', + 'modoboa.transport', + 'modoboa.relaydomains', + 'modoboa.limits', + 'modoboa.parameters', + 'modoboa.dnstools', + 'modoboa.policyd', + 'modoboa.maillog', + 'modoboa.dmarc', + 'modoboa.pdfcredentials', + 'modoboa.imap_migration', +) +``` + +And remove any reference to `'modoboa_dmarc'`, +`'modoboa_pdfcredentials'` or `'modoboa_imap_migration'` in this same +variable. + +You need to add `'modoboa.imap_migration.auth_backends.IMAPBackend',` at +the end of `AUTHENTICATION_BACKENDS`: + +``` python +AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.ModelBackend', + 'modoboa.imap_migration.auth_backends.IMAPBackend', +) +``` + +:::: note +::: title +Note +::: + +It won\'t affect login if you don\'t use `imap_migration` feature. It is +therefor highly recommended to add this authentication backend. +:::: + +After upgrading modoboa, run the following commands from your virtual +environment: + +``` bash +> python manage.py rename_app modoboa_dmarc dmarc +> python manage.py rename_app modoboa_imap_migration imap_migration +``` + +### Version 2.0.4 + +The following modifications must be applied to the +`settings.py`{.interpreted-text role="file"} file: + +- Add `'modoboa.core.context_processors.new_admin_url',` at the end of + [context_processors]{.title-ref} list: + +``` python +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.template.context_processors.i18n', + 'django.template.context_processors.media', + 'django.template.context_processors.static', + 'django.template.context_processors.tz', + 'django.contrib.messages.context_processors.messages', + 'modoboa.core.context_processors.top_notifications', + 'modoboa.core.context_processors.new_admin_url', + ], + 'debug': False, + }, + }, +] +``` + +- Add the following variable: + +``` python +PID_FILE_STORAGE_PATH = '/var/run' +``` + +- Update `LOGGING` variable to define an address for `syslog-auth` + handler: + +``` python +'syslog-auth': { + 'class': 'logging.handlers.SysLogHandler', + 'facility': SysLogHandler.LOG_AUTH, + 'formatter': 'syslog', + 'address': '/dev/log' +}, +``` + +- + + You now have the possibility to customize the url of the new-admin interface. + + : To do so please head up to + `the custom configuration chapter `{.interpreted-text + role="ref"} (advanced user). + +- Add `DEFAULT_THROTTLE_RATES` to `REST_FRAMEWORK`: + +``` python +REST_FRAMEWORK = { + 'DEFAULT_THROTTLE_RATES': { + 'user': '300/minute', + 'ddos': '5/second', + 'ddos_lesser': '200/minute', + 'login': '10/minute', + 'password_recovery_request': '12/hour', + 'password_recovery_totp_check': '25/hour', + 'password_recovery_apply': '25/hour' + }, + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'modoboa.core.drf_authentication.JWTAuthenticationWith2FA', + 'rest_framework.authentication.TokenAuthentication', + 'rest_framework.authentication.SessionAuthentication', + ), + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', + 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning', +} +``` + +- + + You can edit the `DEFAULT_THROTTLE_RATES` to whatever value suits you. + + : - [user]{.title-ref} is for every endpoint, it is per user or per + ip if not logged. + - [ddos]{.title-ref} is per api endpoint and per user or per ip if + not logged. + - [ddos_lesser]{.title-ref} is for per api endpoint and per user + or per ip if not logged. This is for api endpoint that are + lighter. + - [login]{.title-ref} the number of time an ip can attempt to log. + The counter will reset on login success. + - [password\_]{.title-ref} is for the recovery, it is divided per + step in the recovery process. + +### Version 2.0.3 + +Update the new following setting in the `settings.py`{.interpreted-text +role="file"} file: + +``` python +DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' +``` + +### Version 2.0.0 + +Add `drf_spectacular` and `phonenumber_field` to `INSTALLED_APPS` in the +`settings.py`{.interpreted-text role="file"} file, as follows: + +``` python +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.sites', + 'django.contrib.staticfiles', + 'reversion', + 'ckeditor', + 'ckeditor_uploader', + 'rest_framework', + 'rest_framework.authtoken', + 'drf_spectacular', + 'phonenumber_field', + 'django_otp', + 'django_otp.plugins.otp_totp', + 'django_otp.plugins.otp_static', +) +``` + +Modify the `REST_FRAMEWORK` setting as follows: + +``` python +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'modoboa.core.drf_authentication.JWTAuthenticationWith2FA', + 'rest_framework.authentication.TokenAuthentication', + 'rest_framework.authentication.SessionAuthentication', + ), + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', + 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning', +} +``` + +Add the new following settings: + +``` python +SPECTACULAR_SETTINGS = { + 'SCHEMA_PATH_PREFIX': r'/api/v[0-9]', + 'TITLE': 'Modoboa API', + 'VERSION': None, + 'SERVE_PERMISSIONS': ['rest_framework.permissions.IsAuthenticated'], +} + +PHONENUMBER_DB_FORMAT = 'INTERNATIONAL' + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +``` + +#### Migration issue for Postgres/OpenDKIM users + +The [migration will probably +fail](https://github.com/modoboa/modoboa/issues/2508) because of the +additional view created for OpenDKIM. + +To make it work, you first need to drop the view: + +``` bash +$ sudo su - postgres +$ psql +psql (12.10 (Ubuntu 12.10-0ubuntu0.20.04.1)) +Type "help" for help. + +postgres=# \c modoboa +You are now connected to database "modoboa" as user "postgres". +modoboa-# \d+ dkim + View "public.dkim" + Column | Type | Collation | Nullable | Default | Storage | Description +------------------+------------------------+-----------+----------+---------+----------+------------- + id | integer | | | | plain | + domain_name | character varying(100) | | | | extended | + private_key_path | character varying(254) | | | | extended | + selector | character varying(30) | | | | extended | +View definition: + SELECT admin_domain.id, + admin_domain.name AS domain_name, + admin_domain.dkim_private_key_path AS private_key_path, + admin_domain.dkim_key_selector AS selector + FROM admin_domain + WHERE admin_domain.enable_dkim; + +modoboa=# drop view dkim; +DROP VIEW +``` + +Then, run the migration as usual: + +``` bash +python manage.py migrate +``` + +Finally, recreate the view: + +``` bash +modoboa=# CREATE OR REPLACE VIEW dkim AS +modoboa-# SELECT admin_domain.id, +modoboa-# admin_domain.name AS domain_name, +modoboa-# admin_domain.dkim_private_key_path AS private_key_path, +modoboa-# admin_domain.dkim_key_selector AS selector +modoboa-# FROM admin_domain +modoboa-# WHERE admin_domain.enable_dkim; +CREATE VIEW +modoboa=# grant select on dkim to opendkim; +GRANT +``` + +#### New admin interface + +This new release brings a new admin interface written with Vue.js +framework. It is a work in progress and all features are not yet +implemented - i.e. extensions integration - but you could give it a try. +It uses a new API version but the old one is still available. + +You will need to copy the frontend files in the folder you specified in +your web server configuration. If you used the installer, the folder +should be `/srv/modoboa/instance/frontend`: + +``` bash +mkdir /srv/modoboa/instance/frontend +cp -r /srv/modoboa/env/lib/pythonX.X/site-packages/modoboa/frontend_dist/* /srv/modoboa/instance/frontend +``` + +Then, edit the +`/srv/modoboa/instance/frontend/config.json`{.interpreted-text +role="file"} and update the `API_BASE_URL` setting according to the +hostname of your server: + +``` json +{ + "API_BASE_URL": "https:///api/v2" +} +``` + +Finally, update the configuration of your web server to serve the +frontend files. For NGINX, you should add the following in the `server` +block: + +``` nginx +location ^~ /new-admin { + alias /srv/modoboa/instance/frontend/; + index index.html; + + expires -1; + add_header Pragma "no-cache"; + add_header Cache-Control "no-store, no-cache, must-revalidate, post-check=0, pre-check=0"; + + try_files $uri $uri/ /index.html = 404; +} +``` + +### Version 1.17.0 + +Modoboa now supports Two-Factor authentication using TOTP applications. + +To enable it, install the following new requirement in your virtualenv: + +``` bash +(env)> pip install django-otp qrcode +``` + +Then, open the `settings.py`{.interpreted-text role="file"} file and add +`django_otp` packages to `INSTALLED_APPS`: + +``` python +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.sites', + 'django.contrib.staticfiles', + 'reversion', + 'ckeditor', + 'ckeditor_uploader', + 'rest_framework', + 'rest_framework.authtoken', + 'django_otp', + 'django_otp.plugins.otp_totp', + 'django_otp.plugins.otp_static', +) +``` + +Add new middlewares to `MIDDLEWARE`. It has to be after the +SessionMiddleware and before the CommonMiddleware: + +``` python +MIDDLEWARE = ( + 'x_forwarded_for.middleware.XForwardedForMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django_otp.middleware.OTPMiddleware', + 'modoboa.core.middleware.TwoFAMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'modoboa.core.middleware.LocalConfigMiddleware', + 'modoboa.lib.middleware.AjaxLoginRedirect', + 'modoboa.lib.middleware.CommonExceptionCatcher', + 'modoboa.lib.middleware.RequestCatcherMiddleware', +) +``` + +And add the following new setting: + +``` python +# 2FA + +OTP_TOTP_ISSUER = "" +``` + +### Version 1.16.0 + +A new `policy daemon `{.interpreted-text role="ref"} has +been added. + +Make sure to have a Redis instance running on your server. + +Add `'modoboa.policyd'` to `MODOBOA_APPS`: + +``` python +MODOBOA_APPS = ( + 'modoboa', + 'modoboa.core', + 'modoboa.lib', + 'modoboa.admin', + 'modoboa.transport', + 'modoboa.relaydomains', + 'modoboa.limits', + 'modoboa.parameters', + 'modoboa.dnstools', + 'modoboa.policyd' +) +``` + +Add the following settings to your `settings.py` file: + +``` python +REDIS_HOST = '' +REDIS_PORT = 6379 +REDIS_QUOTA_DB = 0 +REDIS_URL = 'redis://{}:{}/{}'.format(REDIS_HOST, REDIS_PORT, REDIS_QUOTA_DB) +``` + +Or, if Redis listen on unix socket and you are not using the Modoboa +installer: + +``` python +REDIS_HOST = '/path/to/redis/socket' +REDIS_PORT = 6379 +REDIS_QUOTA_DB = 0 +REDIS_URL = 'unix://{}?db={}'.format(REDIS_HOST, REDIS_QUOTA_DB) +``` + +Once done, you can start the policy daemon using the following commands: + +``` bash +> python manage.py policy_daemon +``` + +Don\'t forget to configure `_policyd_config `{.interpreted-text +role="ref"} if you want to use this feature. + +The `modoboa-stats` plugin has been merged into the core. + +Add `'modoboa.maillog'` to `MODOBOA_APPS`: + +``` python +MODOBOA_APPS = ( + 'modoboa', + 'modoboa.core', + 'modoboa.lib', + 'modoboa.admin', + 'modoboa.transport', + 'modoboa.relaydomains', + 'modoboa.limits', + 'modoboa.parameters', + 'modoboa.dnstools', + 'modoboa.policyd', + 'modoboa.maillog', +) +``` + +And remove any reference to `modoboa_stats` in this same variable. + +### Version 1.15.0 + +This version drops Python 2 support so don\'t forget to update all the +extensions you use. + +:::: warning +::: title +Warning +::: + +If you upgrade an existing python 2 installation, you will need to +create a new Python 3 virtual environment. You can remove the existing +virtual environment and replace it by the new one so you won\'t have to +modify your configuration files. +:::: + +Add the following new setting: + +``` python +DISABLE_DASHBOARD_EXTERNAL_QUERIES = False +``` + +Reload uwsgi/gunicorn/apache depending on your setup. + +Finally, Make sure to use root privileges and run the following command: + +``` bash +> python manage.py generate_postfix_maps --destdir +``` + +Then, reload postfix. + +### Version 1.14.0 + +This release introduces an optional LDAP synchronization process. If you +want to use it, please follow the +`dedicated procedure `{.interpreted-text role="ref"}. + +### Version 1.13.1 + +Upgrade postfix maps files as follows: + +``` bash +> python manage.py generate_postfix_maps --destdir --force-overwrite +``` + +### Version 1.13.0 + +Add `'modoboa.dnstools'` to `MODOBOA_APPS`: + +``` python +MODOBOA_APPS = ( + 'modoboa', + 'modoboa.core', + 'modoboa.lib', + 'modoboa.admin', + 'modoboa.transport', + 'modoboa.relaydomains', + 'modoboa.limits', + 'modoboa.parameters', + 'modoboa.dnstools', +) +``` + +Add the following new settings: + +``` python +CSRF_COOKIE_SECURE = True +SESSION_COOKIE_SECURE = True +``` + +### modoboa-postfix-autoreply 1.5.0 + +Edit the `/etc/postfix/main.cf`{.interpreted-text role="file"} file and +remove the `sql-autoreplies-transport.cf` map from the `transport_maps` +if present. Remove the corresponding `proxy_read_maps` entry if +relevant. + +Reload postfix. + +### Version 1.10.0 + +:::: warning +::: title +Warning +::: + +Upgrade installed extensions BEFORE running `check` or `migrate` +commands. +:::: + +Upgrade all your installed plugins to the following versions: + +:::: warning +::: title +Warning +::: + +If you use the amavis plugin, make sure to include its configuration as +follows into `settings.py`{.interpreted-text role="file"}: + +``` python +from modoboa_amavis import settings as modoboa_amavis_settings +modoboa_amavis_settings.apply(globals()) +``` +:::: + + ------------------------------------------------------- + Name Version + --------------------------- --------------------------- + modoboa-amavis 1.2.0 + + modoboa-contacts 0.5.0 + + modoboa-dmarc 1.1.0 + + modoboa-imap-migration 1.2.0 + + modoboa-pdfcredentials 1.3.0 + + modoboa-postfix-autoreply 1.4.0 + + modoboa-radicale 1.2.0 + + modoboa-sievefilters 1.4.0 + + modoboa-stats 1.4.0 + + modoboa-webmail 1.4.0 + ------------------------------------------------------- + +Edit the `settings.py`{.interpreted-text role="file"} file and apply the +following modifications. + +Add `'modoboa.transport'` to `MODOBOA_APPS`: + +``` python +MODOBOA_APPS = ( + 'modoboa', + 'modoboa.core', + 'modoboa.lib', + 'modoboa.admin', + 'modoboa.transport', + 'modoboa.relaydomains', + 'modoboa.limits', + 'modoboa.parameters', +) +``` + +Replace the following line: + +``` python +MIDDLEWARE_CLASSES = ( +``` + +by: + +``` python +MIDDLEWARE = ( +``` + +Update postfix map files as follows: + +``` bash +> rm -f /modoboa-postfix-maps.chk +> python manage.py generate_postfix_maps --force --destdir +``` + +Then, modify postfix\'s configuration as follows: + + smtpd_sender_login_maps = + :/sql-sender-login-map.cf + + transport_maps = + :/sql-transport.cf + :/sql-spliteddomains-transport.cf + # other map files... + +Replace `` and `` by your values. + +If `transport_maps` contains `sql-relaydomains-transport.cf`, remove it. + +:::: warning +::: title +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: + + # Generate DKIM keys (they will belong to the user running this job) + * * * * * root $PYTHON $INSTANCE/manage.py modo manage_dkim_keys + +### Version 1.9.0 + +If you want to manage inactive accounts, look at +`inactive_accounts`{.interpreted-text role="ref"}. + +### Version 1.8.3 + +Edit the `settings.py`{.interpreted-text role="file"} file and replace +the following line: + +``` python +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) +``` + +by: + +``` python +BASE_DIR = os.path.realpath(os.path.dirname(os.path.dirname(__file__))) +``` + +### Version 1.8.0 + +Modoboa now relies on [Django\'s builtin password validation +system](https://docs.djangoproject.com/en/1.10/topics/auth/passwords/#module-django.contrib.auth.password_validation) +to validate user passwords, instead of `django-passwords`. + +Remove `django-passwords` from your system: + +``` bash +> sudo -u -i bash +> source /bin/activate +> pip uninstall django-passwords +``` + +Edit the `settings.py`{.interpreted-text role="file"} file and remove +the following content: + +``` python +# django-passwords + +PASSWORD_MIN_LENGTH = 8 + +PASSWORD_COMPLEXITY = { + "UPPER": 1, + "LOWER": 1, + "DIGITS": 1 +} +``` + +Add the following lines: + +``` python +# Password validation rules +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, + { + 'NAME': 'modoboa.core.password_validation.ComplexityValidator', + 'OPTIONS': { + 'upper': 1, + 'lower': 1, + 'digits': 1, + 'specials': 0 + } + }, +] +``` + +### Version 1.7.2 + +API documentation has evolved (because of the upgrade to Django Rest +Framework 3.6) and CKeditor is now embedded by default (thanks to the +`django-ckeditor` package). Some configuration changes are required. + +Edit your `settings.py`{.interpreted-text role="file"} file and apply +the following modifications: + +- Update the `INSTALLED_APPS` variable as follows: + +``` python +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.sites', + 'django.contrib.staticfiles', + 'reversion', + 'ckeditor', + 'ckeditor_uploader', + 'rest_framework', + 'rest_framework.authtoken', +) +``` + +- Update the `REST_FRAMEWORK` variable as follows: + +``` python +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework.authentication.TokenAuthentication', + 'rest_framework.authentication.SessionAuthentication', + ), +} +``` + +- Remove the `SWAGGER_SETTINGS` variable +- Add the following content + +``` python +# CKeditor + +CKEDITOR_UPLOAD_PATH = "uploads/" + +CKEDITOR_IMAGE_BACKEND = "pillow" + +CKEDITOR_RESTRICT_BY_USER = True + +CKEDITOR_BROWSE_SHOW_DIRS = True + +CKEDITOR_ALLOW_NONIMAGE_FILES = False + +CKEDITOR_CONFIGS = { + 'default': { + 'allowedContent': True, + 'toolbar': 'Modoboa', + 'width': None, + 'toolbar_Modoboa': [ + ['Bold', 'Italic', 'Underline'], + ['JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock'], + ['BidiLtr', 'BidiRtl', 'Language'], + ['NumberedList', 'BulletedList', '-', 'Outdent', 'Indent'], + ['Undo', 'Redo'], + ['Link', 'Unlink', 'Anchor', '-', 'Smiley'], + ['TextColor', 'BGColor', '-', 'Source'], + ['Font', 'FontSize'], + ['Image', ], + ['SpellChecker'] + ], + }, +} +``` + +Don\'t forget to run the following command: + +``` bash +> python manage.py collectstatic +``` + +### Version 1.7.1 + +If you used 1.7.0 for a fresh installation, please run the following +commands: + +``` bash +> sudo -u -i bash +> source /bin/activate +> cd +> python manage.py load_initial_data +``` + +### Version 1.7.0 + +This version requires Django \>= 1.10 so you need to make some +modifications. It also brings internal API changes which are not +backward compatible so installed extensions must be upgraded too. + +First of all, deactivate all installed extensions (edit the +`settings.py`{.interpreted-text role="file"} file and comment the +corresponding lines in `MODOBOA_APPS`). + +Edit the `urls.py`{.interpreted-text role="file"} file of your local +instance and replace its content by the following one: + +``` python +from django.conf.urls import include, url + +urlpatterns = [ + url(r'', include('modoboa.urls')), +] +``` + +Edit the `settings.py`{.interpreted-text role="file"} and apply the +following changes: + +- Add `'modoboa.parameters'` to `MODOBOA_APPS`: + +``` python +MODOBOA_APPS = ( + 'modoboa', + 'modoboa.core', + 'modoboa.lib', + 'modoboa.admin', + 'modoboa.relaydomains', + 'modoboa.limits', + 'modoboa.parameters', + # Modoboa extensions here. +) +``` + +- Add `'modoboa.core.middleware.LocalConfigMiddleware'` to + `MIDDLEWARE_CLASSES`: + +``` python +MIDDLEWARE_CLASSES = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'modoboa.core.middleware.LocalConfigMiddleware', + 'modoboa.lib.middleware.AjaxLoginRedirect', + 'modoboa.lib.middleware.CommonExceptionCatcher', + 'modoboa.lib.middleware.RequestCatcherMiddleware', +) +``` + +- Modoboa used to provide a custom authentication backend + (`modoboa.lib.authbackends.SimpleBackend`) but it has been removed. + Replace it as follows: + +``` python +AUTHENTICATION_BACKENDS = ( + # Other backends before... + 'django.contrib.auth.backends.ModelBackend', +) +``` + +- Remove `TEMPLATE_CONTEXT_PROCESSORS` and replace it by: + +``` python +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.template.context_processors.i18n', + 'django.template.context_processors.media', + 'django.template.context_processors.static', + 'django.template.context_processors.tz', + 'django.contrib.messages.context_processors.messages', + 'modoboa.core.context_processors.top_notifications', + ], + 'debug': False, + }, + }, +] +``` + +Run the following commands (load virtualenv if you use one): + +``` bash +> sudo -u -i bash +> source /bin/activate +> cd +> python manage.py migrate +> python manage.py collectstatic +``` + +Finally, upgrade your extensions and reactivate them. + + ------------------------------------------------------- + Name Version + --------------------------- --------------------------- + modoboa-amavis 1.1.0 + + modoboa-dmarc 1.0.0 + + modoboa-imap-migration 1.1.0 + + modoboa-pdfcredentials 1.1.0 + + modoboa-postfix-autoreply 1.2.0 + + modoboa-radicale 1.1.0 + + modoboa-sievefilters 1.1.0 + + modoboa-stats 1.1.0 + + modoboa-webmail 1.1.0 + ------------------------------------------------------- + +Command line shortcuts: + +``` bash +$ pip install modoboa-amavis==1.1.0 +$ pip install modoboa-dmarc==1.0.0 +$ pip install modoboa-imap-migration==1.1.0 +$ pip install modoboa-pdfcredentials==1.1.0 +$ pip install modoboa-postfix-autoreply==1.2.0 +$ pip install modoboa-radicale==1.1.0 +$ pip install modoboa-sievefilters==1.1.0 +$ pip install modoboa-stats==1.1.0 +$ pip install modoboa-webmail==1.1.0 +``` + +And please make sure you use the latest version of the +`django-versionfield2` package: + +``` bash +$ pip install -U django-versionfield2 +``` + +#### Notes about quota changes and resellers + +Reseller users now have a quota option in Resources tab. This is the +quota that a reseller can share between all its domains. + +There are two quotas for a domain in the new version: + +1. Quota & +2. Default mailbox quota. + +\[1\]. Quota: quota shared between mailboxes This quota is shared +between all the mailboxes of this domain. This value cannot exceed +reseller\'s quota and hence cannot be 0(unlimited) if reseller has +finite quota. + +\[2\]. Default mailbox quota: default quota applied to mailboxes This +quota is the default quota applied to new mailboxes. This value cannot +exceed Quota\[1\] and hence cannot be 0(unlimited) if Quota\[1\] is +finite. + +### Version 1.6.1 + +First of all, update postfix map files as follows: + +``` bash +> python manage.py generate_postfix_maps --destdir --force-overwrite +``` + +Then, modify postfix\'s configuration as follows: + + smtpd_sender_login_maps = + :/sql-sender-login-mailboxes.cf + :/sql-sender-login-aliases.cf + :/sql-sender-login-mailboxes-extra.cf + +Replace `` and `` by your values. + +Finally, reload postfix. + +This release also deprecates some internal functions. As a result, +several extensions has been updated to maintain the compatibility. If +you enabled the notification service, you\'ll find the list of available +updates directly in your Modoboa console. + +For the others, here is the list: + + ------------------------------------------------------- + Name Version + --------------------------- --------------------------- + modoboa-amavis 1.0.10 + + modoboa-postfix-autoreply 1.1.7 + + modoboa-radicale 1.0.5 + + modoboa-stats 1.0.9 + ------------------------------------------------------- + +Command line shortcut: + +``` bash +$ pip install modoboa-amavis==1.0.10 +$ pip install modoboa-postfix-autoreply==1.1.7 +$ pip install modoboa-radicale==1.0.5 +$ pip install modoboa-stats==1.0.9 +``` + +### Version 1.6.0 + +:::: warning +::: title +Warning +::: + +You have to upgrade extensions due to [core.User]{.title-ref} model +attribute change ([user.group]{.title-ref} to [user.role]{.title-ref}). +Otherwise, you will have an internal error after upgrade. In particular: +[modoboa-amavisd](https://github.com/modoboa/modoboa-amavis/commit/35df4e48b124e56df930cda8c013af0c1fcaabf3), +[modoboa-stats](https://github.com/modoboa/modoboa-stats/commit/aa4a39ce65eb306ad6dec30a54eb58945b120274), +[modoboa-postfix-autoreply](https://github.com/modoboa/modoboa-postfix-autoreply/commit/20f98c8d1c0c0dbd420f47aefcbb0290022414a4) +are concerned. +:::: + +An interesting feature brougth by this version is the capability to make +different checks about MX records. For example, Modoboa can query main +[DNSBL](https://en.wikipedia.org/wiki/DNSBL) providers for every defined +domain. With this, you will quickly know if one the domains you manage +is listed or not. To activate it, add the following line to your +crontab: + + */30 * * * * python /manage.py modo check_mx + +The communication with Modoboa public API has been reworked. Instead of +sending direct synchronous queries (for example to check new versions), +a cron job has been added. To activate it, add the following line to +your crontab: + + 0 * * * * python /manage.py communicate_with_public_api + +Please also note that public API now uses TLS so you must update your +configuration as follows: + + MODOBOA_API_URL = 'https://api.modoboa.org/1/' + +Finally, it is now possible to declare additional sender addresses on a +per-account basis. You need to update your postfix configuration in +order to use this functionality. Just edit the +`main.cf`{.interpreted-text role="file"} file and change the following +parameter: + + smtpd_sender_login_maps = + :/etc/postfix/sql-sender-login-mailboxes.cf + :/etc/postfix/sql-sender-login-aliases.cf + :/etc/postfix/sql-sender-login-mailboxes-extra.cf + +### Version 1.5.0 + +The API has been greatly improved and a documentation is now available. +To enable it, add `'rest_framework_swagger'` to the `INSTALLED_APPS` +variable in `settings.py`{.interpreted-text role="file"} as follows: + + INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.sites', + 'django.contrib.staticfiles', + 'reversion', + 'rest_framework.authtoken', + 'rest_framework_swagger', + ) + +Then, add the following content into `settings.py`{.interpreted-text +role="file"}, just after the `REST_FRAMEWORK` variable: + + SWAGGER_SETTINGS = { + "is_authenticated": False, + "api_version": "1.0", + "exclude_namespaces": [], + "info": { + "contact": "contact@modoboa.com", + "description": ("Modoboa API, requires a valid token."), + "title": "Modoboa API", + } + } + +You\'re done. The documentation is now available at the following +address: + +> \/docs/api/ + +Finally, if you find a `TEMPLATE_CONTEXT_PROCESSORS` variable in your +`settings.py`{.interpreted-text role="file"} file, make sure it looks +like this: + + TEMPLATE_CONTEXT_PROCESSORS = global_settings.TEMPLATE_CONTEXT_PROCESSORS + [ + 'modoboa.core.context_processors.top_notifications', + ] + +### Version 1.4.0 + +:::: warning +::: title +Warning +::: + +Please make sure to use Modoboa 1.3.5 with an up-to-date database before +an upgrade to 1.4.0. +:::: + +:::: warning +::: title +Warning +::: + +Do not follow the regular upgrade procedure for this version. +:::: + +Some extension have been moved back into the main repository. The main +reason for that is that using Modoboa without them doesn\'t make sense. + +First of all, you must rename the following applications listed inside +the `MODOBOA_APPS` variable: + + --------------------------------------------------- + Old name New name + ---------------------------- ---------------------- + modoboa_admin modoboa.admin + + modoboa_admin_limits modoboa.limits + + modoboa_admin_relaydomains modoboa.relaydomains + --------------------------------------------------- + +Then, apply the following steps: + +1. Uninstall old extensions: + + $ pip uninstall modoboa-admin modoboa-admin-limits modoboa-admin-relaydomains + +2. Install all extension updates using pip (check the *Modoboa \> + Information* page) + +3. Manually migrate database: + + $ cd + $ python manage.py migrate auth + $ python manage.py migrate admin 0001 --fake + $ python manage.py migrate admin + $ python manage.py migrate limits 0001 --fake + $ python manage.py migrate relaydomains 0001 --fake + $ python manage.py migrate + +4. Finally, update static files: + + $ python manage.py collectstatic + +This version also introduces a REST API. To enable it: + +1. Add `'rest_framework.authtoken'` to the `INSTALLED_APPS` variable + +2. Add the following configuration inside `settings.py`: + + # Rest framework settings + + REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework.authentication.TokenAuthentication', + ), + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.IsAuthenticated', + ) + } + +3. Run the following command: + + $ python manage.py migrate + +### Version 1.3.5 + +To enhance security, Modoboa now checks the [strength of user passwords +\\_]{.title-ref}. + +To use this feature, add the following configuration into the +`settings.py` file: + + # django-passwords + + PASSWORD_MIN_LENGTH = 8 + + PASSWORD_COMPLEXITY = { + "UPPER": 1, + "LOWER": 1, + "DIGITS": 1 + } + +### Version 1.3.2 + +Modoboa now uses the *atomic requests* mode to preserve database +consistency +([reference](https://docs.djangoproject.com/en/1.7/topics/db/transactions/#tying-transactions-to-http-requests)). + +To enable it, update the `DATABASES` variable in `settings.py` as +follows: + + DATABASES = { + "default": { + # stuff before... + "ATOMIC_REQUESTS": True + }, + "amavis": { + # stuff before... + "ATOMIC_REQUESTS": True + } + } + +### Version 1.3.0 + +This release does not bring awesome new features but it is a necessary +bridge to the future of Modoboa. All extensions now have their own git +repository and the deploy process has been updated to reflect this +change. + +Another important update is the use of Django 1.7. Besides its new +features, the migration system has been reworked and is now more robust +than before. + +Before we begin with the procedure, here is a table showing old +extension names and their new name: + + ---------------------------------------------------------------------------------------------------- + Old name New package name New module name + ------------------------------------------ ---------------------------- ---------------------------- + modoboa.extensions.admin modoboa-admin modoboa_admin + + modoboa.extensions.limits modoboa-admin-limits modoboa_admin_limits + + modoboa.extensions.postfix_autoreply modoboa-postfix-autoreply modoboa_postfix_autoreply + + modoboa.extensions.postfix_relay_domains modoboa-admin-relaydomains modoboa_admin_relaydomains + + modoboa.extensions.radicale modoboa-radicale modoboa_radicale + + modoboa.extensions.sievefilters modoboa-sievefilters modoboa_sievefilters + + modoboa.extensions.stats modoboa-stats modoboa_stats + + modoboa.extensions.webmail modoboa-webmail modoboa_webmail + ---------------------------------------------------------------------------------------------------- + +Here are the required steps: + +1. Install the extensions using pip (look at the second column in the + table above): + + $ pip install + +2. Remove `south` from `INSTALLED_APPS` + +3. Rename old extension names inside `MODOBOA_APPS` (look at the third + column in the table above) + +4. Remove `modoboa.lib.middleware.ExtControlMiddleware` from + `MIDDLEWARE_CLASSES` + +5. Change `DATABASE_ROUTERS` to: + + DATABASE_ROUTERS = ["modoboa_amavis.dbrouter.AmavisRouter"] + +6. Run the following commands: + + $ cd + $ python manage.py migrate + +7. Reply `yes` to the question + +8. Run the following commands: + + $ python manage.py load_initial_data + $ python manage.py collectstatic + +9. The cleanup job has been renamed in Django, so you have to modify + your crontab entry: + + - 0 0 * * * /manage.py cleanup + + 0 0 * * * /manage.py clearsessions + +### Version 1.2.0 + +A new notification service let administrators know about new Modoboa +versions. To activate it, you need to update the +`TEMPLATE_CONTEXT_PROCESSORS` variable like this: + + from django.conf import global_settings + + TEMPLATE_CONTEXT_PROCESSORS = global_settings.TEMPLATE_CONTEXT_PROCESSORS + ( + 'modoboa.core.context_processors.top_notifications', + ) + +and to define the new `MODOBOA_API_URL` variable: + + MODOBOA_API_URL = 'http://api.modoboa.org/1/' + +The location of external static files has changed. To use them, add a +new path to the `STATICFILES_DIRS`: + + # Additional locations of static files + STATICFILES_DIRS = ( + # Put strings here, like "/home/html/static" or "C:/www/django/static". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. + "/bower_components", + ) + +Run the following commands to define the hostname of your instance: + + $ cd + $ python manage.py set_default_site + +If you plan to use the Radicale extension: + +1. Add `'modoboa.extensions.radicale'` to the `MODOBOA_APPS` variable + +2. Run the following commands: + + $ cd + $ python manage.py syncdb + +:::: warning +::: title +Warning +::: + +You also have to note that the `sitestatic`{.interpreted-text +role="file"} directory has moved from `` to +`` (it\'s probably the parent directory). You have +to adapt your web server configuration to reflect this change. +:::: \ No newline at end of file From 7344d8fdddf4a781cac6a3512d7f8db2bfad75dd Mon Sep 17 00:00:00 2001 From: Christophe Chauvet Date: Thu, 8 Jan 2026 20:17:58 +0100 Subject: [PATCH 2/8] ci: fix build path --- .github/workflows/documentation.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 3b35c5559..784dcb476 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -27,8 +27,8 @@ jobs: cache: true - name: Install dependencies - run: pnpm install + run: cd doc-ng && pnpm install - name: Build documentation - run: pnpm run build + run: cd doc-ng && pnpm run build \ No newline at end of file From 88ff877ad32d7d5c835cbd403890d30e45d414ca Mon Sep 17 00:00:00 2001 From: Christophe Chauvet Date: Fri, 9 Jan 2026 11:06:34 +0100 Subject: [PATCH 3/8] chore: fix gitignore to exlude only folder on vitepress --- .gitignore | 3 +- doc-ng/.vitepress/config.mjs | 107 +++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 doc-ng/.vitepress/config.mjs diff --git a/.gitignore b/.gitignore index 0708bfd31..3b3c04c36 100644 --- a/.gitignore +++ b/.gitignore @@ -24,7 +24,8 @@ node_modules /frontend/webpack-stats.json /frontend/src/translations.json /frontend/.__mf__temp/ -.vitepress/ +doc-ng/.vitepress/cache/ +doc-ng/.vitepress/dist/ # TOX stuff test_project/modoboa.db diff --git a/doc-ng/.vitepress/config.mjs b/doc-ng/.vitepress/config.mjs new file mode 100644 index 000000000..94fe5832b --- /dev/null +++ b/doc-ng/.vitepress/config.mjs @@ -0,0 +1,107 @@ +import { defineConfig } from 'vitepress' + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + // Use Canonical URL, but only the path and with no trailing / + // End result is like: `/en/latest` + // https://docs.readthedocs.com/platform/stable/intro/vitepress.html#using-the-proper-base-path + base: process.env.READTHEDOCS_CANONICAL_URL ? new URL(process.env.READTHEDOCS_CANONICAL_URL).pathname.replace(/\/$/, "") : "", + title: "Modoboa Documentation ", + titleTemplate: ':title', + description: "Modoboa documentation", + srcExclude: ['public/**/*.md'], + ignoreDeadLinks: [ + /^https?:\/\/localhost:*/, + ], + metaChunk: true, + themeConfig: { + // https://vitepress.dev/reference/default-theme-config + nav: [ + { text: 'Home', link: '/' }, + { text: 'Installation', items: [ + { text: 'Requirements', link: '/installation/requirements'}, + { text: 'Manual Installation', link: '/installation/manual'}, + { text: 'Modoboa Installer', link: '/installation/installer' }, + { text: 'Upgrade Instance', link: '/installation/upgrade' }, + { text: 'Configuration', link: '/configuration' }, + ]}, + { text: 'Contributing', items: [ + { text: 'Getting Start', link: '/contributing/getting_start'}, + { text: 'Write Plugin', link: '/contributing/plugin_api'}, + { text: 'Translate', link: '/contributing/translation'}, + { text: 'Contributor', link: '/contributors'}, + ]} + ], + + sidebar: { + "/installation/": [{ + text: 'Manual', + items: [ + { text: 'Modoboa', link: '/installation/modoboa' }, + { text: 'WebServer', link: '/installation/webserver' }, + { text: 'Dovecot', link: '/installation/dovecot' }, + { text: 'Postfix', link: '/installation/postfix' }, + { text: 'OpenDKIM', link: '/installation/opendkim' }, + { text: 'Radicale', link: '/installation/radicale' }, + { text: 'Amavis', link: '/installation/amavis' }, + ] + }, + { + text: 'Automatic', + items: [ + { text: 'Modoboa Installer', link: '/installation/installer' }, + ] + }], + "/contributing/": [ + { text: 'Getting Start', link: '/contributing/getting_start'}, + { text: 'Write Plugin', link: '/contributing/plugin_api'}, + { text: 'Translate', link: '/contributing/translate'}, + ] + } , + socialLinks: [ + { icon: 'github', link: 'https://github.com/modoboa/modoboa' }, + { icon: 'discord', link: 'https://discord.gg/WuQ3v3PXGR'}, + { icon: 'twitter', link: 'https://modoboa.org/'}, + { icon: { + svg: ` + Transifex + + ` + }, link: 'https://app.transifex.com/tonio/modoboa/dashboard/' }, + { icon: 'linkedin', link: 'https://www.linkedin.com/company/102575109'}, + ], + footer: { + message: 'Build with Vitepress', + copyright: `Copyright © 2010-${new Date().getUTCFullYear()} Antoine Nguyen` + }, + outline: { + label: "Summmary" + }, + docFooter: { + prev: false, + next: false + }, + search: { + provider: 'local', + }, + lastUpdated: { + text: 'Update at', + formatOptions: { + dateStyle: 'full', + timeStyle: 'short' + } + } + }, + sitemap: { + hostname: 'https://modoboa.readthedocs.io/en/latest' + }, + markdown: { + lineNumbers: true, + image: { + lazyLoading: true + } + }, + cleanUrls: true, + outDir: './dist', + lastUpdated: true, +}) From a0c4bc8241fe94f2872a67990fb4c07b94ab5970 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 13 Jan 2026 17:06:43 +0100 Subject: [PATCH 4/8] Small updates --- doc-ng/.vitepress/config.mjs | 128 +++++++++++++++------------- doc-ng/contributing/plugin_api.md | 75 ++-------------- doc-ng/contributing/translation.md | 15 +++- doc-ng/contributors.md | 1 + doc-ng/installation/requirements.md | 10 +-- 5 files changed, 96 insertions(+), 133 deletions(-) diff --git a/doc-ng/.vitepress/config.mjs b/doc-ng/.vitepress/config.mjs index 94fe5832b..9d1208704 100644 --- a/doc-ng/.vitepress/config.mjs +++ b/doc-ng/.vitepress/config.mjs @@ -5,81 +5,91 @@ export default defineConfig({ // Use Canonical URL, but only the path and with no trailing / // End result is like: `/en/latest` // https://docs.readthedocs.com/platform/stable/intro/vitepress.html#using-the-proper-base-path - base: process.env.READTHEDOCS_CANONICAL_URL ? new URL(process.env.READTHEDOCS_CANONICAL_URL).pathname.replace(/\/$/, "") : "", - title: "Modoboa Documentation ", + base: process.env.READTHEDOCS_CANONICAL_URL + ? new URL(process.env.READTHEDOCS_CANONICAL_URL).pathname.replace(/\/$/, '') + : '', + title: 'Modoboa Documentation ', titleTemplate: ':title', - description: "Modoboa documentation", + description: 'Modoboa documentation', srcExclude: ['public/**/*.md'], - ignoreDeadLinks: [ - /^https?:\/\/localhost:*/, - ], + ignoreDeadLinks: [/^https?:\/\/localhost:*/], metaChunk: true, themeConfig: { // https://vitepress.dev/reference/default-theme-config nav: [ { text: 'Home', link: '/' }, - { text: 'Installation', items: [ - { text: 'Requirements', link: '/installation/requirements'}, - { text: 'Manual Installation', link: '/installation/manual'}, - { text: 'Modoboa Installer', link: '/installation/installer' }, - { text: 'Upgrade Instance', link: '/installation/upgrade' }, - { text: 'Configuration', link: '/configuration' }, - ]}, - { text: 'Contributing', items: [ - { text: 'Getting Start', link: '/contributing/getting_start'}, - { text: 'Write Plugin', link: '/contributing/plugin_api'}, - { text: 'Translate', link: '/contributing/translation'}, - { text: 'Contributor', link: '/contributors'}, - ]} + { + text: 'Installation', + items: [ + { text: 'Requirements', link: '/installation/requirements' }, + { text: 'Manual Installation', link: '/installation/manual' }, + { text: 'Modoboa Installer', link: '/installation/installer' }, + { text: 'Upgrade Instance', link: '/installation/upgrade' }, + { text: 'Configuration', link: '/configuration' }, + ], + }, + { + text: 'Contributing', + items: [ + { text: 'Getting Started', link: '/contributing/getting_start' }, + { text: 'Write a Plugin', link: '/contributing/plugin_api' }, + { text: 'Translate', link: '/contributing/translation' }, + { text: 'Contributors', link: '/contributors' }, + ], + }, ], sidebar: { - "/installation/": [{ - text: 'Manual', - items: [ - { text: 'Modoboa', link: '/installation/modoboa' }, - { text: 'WebServer', link: '/installation/webserver' }, - { text: 'Dovecot', link: '/installation/dovecot' }, - { text: 'Postfix', link: '/installation/postfix' }, - { text: 'OpenDKIM', link: '/installation/opendkim' }, - { text: 'Radicale', link: '/installation/radicale' }, - { text: 'Amavis', link: '/installation/amavis' }, - ] - }, - { - text: 'Automatic', - items: [ - { text: 'Modoboa Installer', link: '/installation/installer' }, - ] - }], - "/contributing/": [ - { text: 'Getting Start', link: '/contributing/getting_start'}, - { text: 'Write Plugin', link: '/contributing/plugin_api'}, - { text: 'Translate', link: '/contributing/translate'}, - ] - } , + '/installation/': [ + { + text: 'Manual', + items: [ + { text: 'Modoboa', link: '/installation/modoboa' }, + { text: 'WebServer', link: '/installation/webserver' }, + { text: 'Dovecot', link: '/installation/dovecot' }, + { text: 'Postfix', link: '/installation/postfix' }, + { text: 'OpenDKIM', link: '/installation/opendkim' }, + { text: 'Radicale', link: '/installation/radicale' }, + { text: 'Amavis', link: '/installation/amavis' }, + ], + }, + { + text: 'Automatic', + items: [ + { text: 'Modoboa Installer', link: '/installation/installer' }, + ], + }, + ], + '/contributing/': [ + { text: 'Getting Started', link: '/contributing/getting_start' }, + { text: 'Write a Plugin', link: '/contributing/plugin_api' }, + { text: 'Translate', link: '/contributing/translate' }, + ], + }, socialLinks: [ { icon: 'github', link: 'https://github.com/modoboa/modoboa' }, - { icon: 'discord', link: 'https://discord.gg/WuQ3v3PXGR'}, - { icon: 'twitter', link: 'https://modoboa.org/'}, - { icon: { - svg: ` + { icon: 'discord', link: 'https://discord.gg/WuQ3v3PXGR' }, + { + icon: { + svg: ` Transifex - ` - }, link: 'https://app.transifex.com/tonio/modoboa/dashboard/' }, - { icon: 'linkedin', link: 'https://www.linkedin.com/company/102575109'}, + `, + }, + link: 'https://app.transifex.com/tonio/modoboa/dashboard/', + }, + { icon: 'linkedin', link: 'https://www.linkedin.com/company/102575109' }, ], footer: { message: 'Build with Vitepress', - copyright: `Copyright © 2010-${new Date().getUTCFullYear()} Antoine Nguyen` + copyright: `Copyright © 2010-${new Date().getUTCFullYear()} Antoine Nguyen`, }, outline: { - label: "Summmary" + label: 'Summmary', }, docFooter: { prev: false, - next: false + next: false, }, search: { provider: 'local', @@ -88,18 +98,18 @@ export default defineConfig({ text: 'Update at', formatOptions: { dateStyle: 'full', - timeStyle: 'short' - } - } + timeStyle: 'short', + }, + }, }, sitemap: { - hostname: 'https://modoboa.readthedocs.io/en/latest' + hostname: 'https://modoboa.readthedocs.io/en/latest', }, markdown: { lineNumbers: true, image: { - lazyLoading: true - } + lazyLoading: true, + }, }, cleanUrls: true, outDir: './dist', diff --git a/doc-ng/contributing/plugin_api.md b/doc-ng/contributing/plugin_api.md index 5114ac3c9..791dbaa8e 100644 --- a/doc-ng/contributing/plugin_api.md +++ b/doc-ng/contributing/plugin_api.md @@ -2,7 +2,7 @@ ## Introduction -Modoboa offers a plugin API to expand its capabilities. +Modoboa offers a plugin API to expand its capabilities. The current implementation provides the following possibilities: @@ -12,7 +12,7 @@ The current implementation provides the following possibilities: ::: tip Plugins are nothing more than Django applications with an extra piece of -code that integrates them into Modoboa. +code that integrates them into Modoboa. ::: The `modo_extension.py` file will contain a complete description of the plugin: @@ -20,7 +20,7 @@ The `modo_extension.py` file will contain a complete description of the plugin: - Admin and user parameters - Custom menu entries -The communication between both applications is provided by +The communication between both applications is provided by [Django signals](https://docs.djangoproject.com/en/5.2/topics/signals/). The following subsections describe the plugin architecture and explain @@ -56,7 +56,7 @@ class MyExtension(ModoExtension): """This method is called when Modoboa loads available and activated plugins. Declare parameters and register events here. - """ + """ pass def load_initial_data(self): @@ -66,8 +66,8 @@ class MyExtension(ModoExtension): exts_pool.register_extension(MyExtension) ``` -Once done, simply add your extension's module name to the `MODOBOA_APPS` -variable located inside `settings.py`. +Once done, simply add your extension's module name to the `MODOBOA_APPS` +variable located inside `settings.py`. Finally, run the following commands: @@ -88,7 +88,7 @@ A plugin can declare its own parameters. There are two levels available: #### Playing with parameters -Parameters are defined using [Django forms](https://docs.djangoproject.com/en/1.9/topics/forms/) +Parameters are defined using [Django forms](https://docs.djangoproject.com/en/1.9/topics/forms/) and Modoboa adds two special forms you can inherit depending on the level of parameter(s) you want to add: * `modoboa.parameters.forms.AdminParametersForm`: for general parameters @@ -106,7 +106,7 @@ Replace: ### Custom role permissions -Modoboa uses Django's internal permission system. +Modoboa uses Django's internal permission system. Administrative roles are nothing more than groups (`Group` instances). @@ -132,62 +132,3 @@ def extra_role_permissions(sender, role, **kwargs): """Add permissions to the Resellers group.""" return constants.PERMISSIONS.get(role, []) ``` - -### Extending admin forms - -The forms used to edit objects (account, domain, etc.) through the admin panel are composed of tabs. - -You can extend them (ie. add new tabs) in a pretty easy way thanks to custom signals. - -#### Account - -To add a new tab to the account edition form, define new listeners -(handlers) for the following signals: - -* `modoboa.admin.signals.extra_account_forms` -* `modoboa.admin.signals.get_account_form_instances` -* `modoboa.admin.signals.check_extra_account_form` (optional) - -Example: - -``` python -from django.dispatch import receiver -from modoboa.admin import signals as admin_signals - - -@receiver(admin_signals.extra_account_forms) -def extra_account_form(sender, user, account, **kwargs): - return [ - {"id": "tabid", "title": "Title", "cls": MyFormClass} - ] - -@receiver(admin_signals.get_account_form_instances) -def fill_my_tab(sender, user, account, **kwargs): - return {"id": my_instance} -``` - -#### Domain - -To add a new tab to the domain edition form, define new listeners -(handlers) for the following signals: - -* `modoboa.admin.signals.extra_domain_forms` -* `modoboa.admin.signals.get_domain_form_instances` - -Example: - -``` python -from django.dispatch import receiver -from modoboa.admin import signals as admin_signals - - -@receiver(admin_signals.extra_domain_forms) -def extra_account_form(sender, user, domain, **kwargs): - return [ - {"id": "tabid", "title": "Title", "cls": MyFormClass} - ] - -@receiver(admin_signals.get_domain_form_instances) -def fill_my_tab(sender, user, domain, **kwargs): - return {"id": my_instance} -``` diff --git a/doc-ng/contributing/translation.md b/doc-ng/contributing/translation.md index 8bf6ce1f3..000f24f77 100644 --- a/doc-ng/contributing/translation.md +++ b/doc-ng/contributing/translation.md @@ -7,6 +7,17 @@ The project is using Transifex. To start with translation, you will need to create an account and join [modoboa\'s team](https://app.transifex.com/tonio/modoboa/dashboard/) +## API + +Modoboa API code relies on Django's translation system. + +To update translation files after a change to the code, execute the following commands: + +```shell +$ cd modoboa +$ django-admin makemessages -a +``` + ## Frontend When editing the frontend, you may need to update localization files for @@ -16,5 +27,5 @@ To do so, after finishing editing, go to the frontend directory and run the following command: ```shell -$ make makemessages -``` \ No newline at end of file +$ yarn gettext:extract +``` diff --git a/doc-ng/contributors.md b/doc-ng/contributors.md index 3f87980b4..4ae6e3f9c 100644 --- a/doc-ng/contributors.md +++ b/doc-ng/contributors.md @@ -1,5 +1,6 @@ # Contributors +- [6clones](https://www.6clones.fr/) - [Antidot](http://antidot.com) - [Bearstech](http://bearstech.com) - [CAP-REL](https://cap-rel.fr/) diff --git a/doc-ng/installation/requirements.md b/doc-ng/installation/requirements.md index b8a26c5da..8e127fa81 100644 --- a/doc-ng/installation/requirements.md +++ b/doc-ng/installation/requirements.md @@ -2,11 +2,11 @@ You will need a Server to perform well with at least: -- CPU: 2 -- RAM: 2GB +- CPU: 4 +- RAM: 4GB - Disk: 10GB -- Python 3.8+ -- Postgres 12+ or MariaDB 10.4+ or MySQL 8+ or sqlite3 or LDAP +- Python 3.10+ +- Postgres 14+ or MariaDB 10.5+ or MySQL 8.0.11+ or sqlite3 ::: warning @@ -23,4 +23,4 @@ RAM. You have 2 possibilities to install Modoboa * [Manual Installation](manual) -* [Automatic Install](installer) \ No newline at end of file +* [Automatic Install](installer) From cfa7930de2010c57d44d161d2d8d75a629afc69a Mon Sep 17 00:00:00 2001 From: Christophe Chauvet Date: Tue, 13 Jan 2026 17:41:21 +0100 Subject: [PATCH 5/8] refactor: reduce title on top left --- doc-ng/.vitepress/config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc-ng/.vitepress/config.mjs b/doc-ng/.vitepress/config.mjs index 9d1208704..45a6a0ad3 100644 --- a/doc-ng/.vitepress/config.mjs +++ b/doc-ng/.vitepress/config.mjs @@ -8,7 +8,7 @@ export default defineConfig({ base: process.env.READTHEDOCS_CANONICAL_URL ? new URL(process.env.READTHEDOCS_CANONICAL_URL).pathname.replace(/\/$/, '') : '', - title: 'Modoboa Documentation ', + title: 'Modoboa', titleTemplate: ':title', description: 'Modoboa documentation', srcExclude: ['public/**/*.md'], From dee70b5f05f14a3ebf73724dc4970fed1987ba17 Mon Sep 17 00:00:00 2001 From: Christophe Chauvet Date: Tue, 13 Jan 2026 18:46:06 +0100 Subject: [PATCH 6/8] chore(docs): add frontmatter for each pages --- doc-ng/.vitepress/config.mjs | 51 + doc-ng/contributing/getting_start.md | 9 + doc-ng/contributing/plugin_api.md | 9 + doc-ng/contributing/translation.md | 13 +- doc-ng/contributors.md | 9 + doc-ng/index.md | 2 + doc-ng/installation/amavis.md | 9 + doc-ng/installation/dovecot.md | 9 + doc-ng/installation/installer.md | 10 +- doc-ng/installation/manual.md | 9 + doc-ng/installation/modoboa.md | 9 + doc-ng/installation/opendkim.md | 9 + doc-ng/installation/postfix.md | 9 + doc-ng/installation/radicale.md | 9 + doc-ng/installation/requirements.md | 9 + doc-ng/installation/upgrade.md | 9 + doc-ng/installation/webserver.md | 9 + doc-ng/moving.md | 10 + doc-ng/rebuild_venv.md | 9 + doc-ng/rest_api.md | 15 +- doc-ng/upgrade.md | 2241 -------------------------- 21 files changed, 221 insertions(+), 2247 deletions(-) delete mode 100644 doc-ng/upgrade.md diff --git a/doc-ng/.vitepress/config.mjs b/doc-ng/.vitepress/config.mjs index 45a6a0ad3..55bd278ec 100644 --- a/doc-ng/.vitepress/config.mjs +++ b/doc-ng/.vitepress/config.mjs @@ -34,6 +34,7 @@ export default defineConfig({ { text: 'Getting Started', link: '/contributing/getting_start' }, { text: 'Write a Plugin', link: '/contributing/plugin_api' }, { text: 'Translate', link: '/contributing/translation' }, + { text: 'REST API', link: '/rest_api' }, { text: 'Contributors', link: '/contributors' }, ], }, @@ -114,4 +115,54 @@ export default defineConfig({ cleanUrls: true, outDir: './dist', lastUpdated: true, + transformPageData(pageData) { + // console.log(pageData); + + const baseUrl = process.env.READTHEDOCS_CANONICAL_URL + ? new URL(process.env.READTHEDOCS_CANONICAL_URL).pathname.replace(/\/$/, '') + : '' + + const canonicalUrl = baseUrl + '/' + pageData.relativePath.replace(/\.md$/, "").replace(/^index$/, ""); + + let updtDate = new Date(); + // If no commit for a page (dev in progress), lastUpdated == NaN + // Ensure lastUpdated is defined and a number before passing it to isNaN/new Date + if (typeof pageData.lastUpdated === 'number' && !isNaN(pageData.lastUpdated)) { + updtDate = new Date(pageData.lastUpdated); + } + + pageData.frontmatter.head ??= []; + pageData.frontmatter.head.push([ + "link", + { rel: "canonical", href: canonicalUrl }, + ]); + pageData.frontmatter.head.push([ + "meta", + { property: "og:title", content: pageData.frontmatter.title }, + ]); + pageData.frontmatter.head.push([ + "meta", + { property: "og:description", content: pageData.frontmatter.description }, + ]); + pageData.frontmatter.head.push([ + "meta", + { property: "og:type", content: 'article' }, + ]); + pageData.frontmatter.head.push([ + "meta", + { property: "og:url", content: canonicalUrl }, + ]); + + // TODO: Uncomment when modoboa image is available on the public folder + // pageData.frontmatter.head.push([ + // "meta", + // { property: "og:image", content: 'https://xxxx/modoboa.png' }, + // ]); + + pageData.frontmatter.head.push([ + "meta", + { property: "og:locale", content: 'en_GB' }, + ]); + + } }) diff --git a/doc-ng/contributing/getting_start.md b/doc-ng/contributing/getting_start.md index bf8da39a1..e0ffb1cf4 100644 --- a/doc-ng/contributing/getting_start.md +++ b/doc-ng/contributing/getting_start.md @@ -1,3 +1,12 @@ +--- +title: Getting stated with modoboa +description: Discover how to develop and extend modoboa +head: + - - meta + - name: 'keywords' + content: 'modoboa, getting started, backend, frontend, docker' +--- + # Getting started You would like to work on Modoboa but you don\'t know where to start? diff --git a/doc-ng/contributing/plugin_api.md b/doc-ng/contributing/plugin_api.md index 791dbaa8e..52d1256dc 100644 --- a/doc-ng/contributing/plugin_api.md +++ b/doc-ng/contributing/plugin_api.md @@ -1,3 +1,12 @@ +--- +title: plugin development guide for modoboa +description: Discover how to develop a plugin for modoboa to extend functionnalities +head: + - - meta + - name: 'keywords' + content: 'modoboa, plugin, extension, api, navigation, Django signals' +--- + # Create a new plugin ## Introduction diff --git a/doc-ng/contributing/translation.md b/doc-ng/contributing/translation.md index 000f24f77..42e6d68ce 100644 --- a/doc-ng/contributing/translation.md +++ b/doc-ng/contributing/translation.md @@ -1,11 +1,20 @@ +--- +title: translation guide for modoboa +description: How to help to translate modoboa for your natural language +head: + - - meta + - name: 'keywords' + content: 'modoboa, i18n, translation, transifex, makemessages, backend, frontend' +--- + # Translation Modoboa needs you to translate the project ! Feel free to join the translation team. The project is using Transifex. To start with translation, you will need -to create an account and join [modoboa\'s -team](https://app.transifex.com/tonio/modoboa/dashboard/) +to create an account and join +[modoboa's team](https://app.transifex.com/tonio/modoboa/dashboard/) ## API diff --git a/doc-ng/contributors.md b/doc-ng/contributors.md index 4ae6e3f9c..8ec727c83 100644 --- a/doc-ng/contributors.md +++ b/doc-ng/contributors.md @@ -1,3 +1,12 @@ +--- +title: "Contributors list" +description: "List of the contributor who participate to the modoboa's project" +head: + - - meta + - name: 'keywords' + content: 'modoboa, contributor, project' +--- + # Contributors - [6clones](https://www.6clones.fr/) diff --git a/doc-ng/index.md b/doc-ng/index.md index 5e03f6958..671ede471 100644 --- a/doc-ng/index.md +++ b/doc-ng/index.md @@ -1,6 +1,8 @@ --- # https://vitepress.dev/reference/default-theme-home-page layout: home +title: "Modoboa, the Mail hosting and management platform" +description: "Web User Interface designed to work with Postfix and Dovecot" hero: name: "Modoboa" diff --git a/doc-ng/installation/amavis.md b/doc-ng/installation/amavis.md index 4cf53b4c4..2876ba84d 100644 --- a/doc-ng/installation/amavis.md +++ b/doc-ng/installation/amavis.md @@ -1,3 +1,12 @@ +--- +title: manual step to install amavis component on modoboa +description: This guide describe how to install amavis on the modoboa platform +head: + - - meta + - name: 'keywords' + content: 'modoboa, installation, amavis, mysql, postgresql, quarantine' +--- + # Amavis ## Quick Amavis setup diff --git a/doc-ng/installation/dovecot.md b/doc-ng/installation/dovecot.md index 2a724bea4..684e7765a 100644 --- a/doc-ng/installation/dovecot.md +++ b/doc-ng/installation/dovecot.md @@ -1,3 +1,12 @@ +--- +title: manual step to install dovecot component on modoboa +description: This guide describe how to install dovecot on the modoboa platform +head: + - - meta + - name: 'keywords' + content: 'modoboa, installation, dovecot, mysql, postgresql, imapd' +--- + # Dovecot ## introduction diff --git a/doc-ng/installation/installer.md b/doc-ng/installation/installer.md index d7007de09..32f0de028 100644 --- a/doc-ng/installation/installer.md +++ b/doc-ng/installation/installer.md @@ -1,5 +1,13 @@ -# Automatic Installer +--- +title: Guide to install modoboa with the automatic installer +description: This guide describe how to install modoboa with the automatic installer +head: + - - meta + - name: 'keywords' + content: 'modoboa, installation, webserver, nginx, apache, wsgi' +--- +# Automatic Installer ## Introduction diff --git a/doc-ng/installation/manual.md b/doc-ng/installation/manual.md index e0ae00e30..3a531fde0 100644 --- a/doc-ng/installation/manual.md +++ b/doc-ng/installation/manual.md @@ -1,3 +1,12 @@ +--- +title: manual step to made modoboa installation +description: This guide describe all steps to install modoboa +head: + - - meta + - name: 'keywords' + content: 'modoboa, installation, postfix, dovecot, opendkim, amavis, radicale, nginx' +--- + # Manual installation For those who need a manual installation or who just want to setup a specific part, diff --git a/doc-ng/installation/modoboa.md b/doc-ng/installation/modoboa.md index 58f59f705..eeaac2919 100644 --- a/doc-ng/installation/modoboa.md +++ b/doc-ng/installation/modoboa.md @@ -1,3 +1,12 @@ +--- +title: manual step to install core component on modoboa +description: This guide describe how to install core on the modoboa platform +head: + - - meta + - name: 'keywords' + content: 'modoboa, installation, core, mysql, postgresql, cron' +--- + # Modoboa This section describes the installation of the web interface (a [Django](https://www.djangoproject.com/) project). diff --git a/doc-ng/installation/opendkim.md b/doc-ng/installation/opendkim.md index bd914a810..7c1218cd0 100644 --- a/doc-ng/installation/opendkim.md +++ b/doc-ng/installation/opendkim.md @@ -1,3 +1,12 @@ +--- +title: manual step to install opendkim component on modoboa +description: This guide describe how to install opendkim on the modoboa platform +head: + - - meta + - name: 'keywords' + content: 'modoboa, installation, opendkim, mysql, postgresql, dkim' +--- + # OpenDKIM Modoboa can generate [DKIM](https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail) keys diff --git a/doc-ng/installation/postfix.md b/doc-ng/installation/postfix.md index 2cdf41067..f8523af92 100644 --- a/doc-ng/installation/postfix.md +++ b/doc-ng/installation/postfix.md @@ -1,3 +1,12 @@ +--- +title: manual step to install postfix component on modoboa +description: This guide describe how to install postfix on the modoboa platform +head: + - - meta + - name: 'keywords' + content: 'modoboa, installation, postfix, mysql, postgresql, smtp, alias, relay, virtual' +--- + # Postfix This section gives an example about building a simple virtual hosting diff --git a/doc-ng/installation/radicale.md b/doc-ng/installation/radicale.md index e89d4f756..be864bf2a 100644 --- a/doc-ng/installation/radicale.md +++ b/doc-ng/installation/radicale.md @@ -1,3 +1,12 @@ +--- +title: manual step to install radicale component on modoboa +description: This guide describe how to install radicale on the modoboa platform +head: + - - meta + - name: 'keywords' + content: 'modoboa, installation, radicale, oauth2' +--- + # Radicale This page is displaying how to install and use radicale when either diff --git a/doc-ng/installation/requirements.md b/doc-ng/installation/requirements.md index 8e127fa81..6a1c3e4ed 100644 --- a/doc-ng/installation/requirements.md +++ b/doc-ng/installation/requirements.md @@ -1,3 +1,12 @@ +--- +title: Requirements to install Modoboa on a server +description: Minimal requirements to install Modoboa on a server +head: + - - meta + - name: 'keywords' + content: 'modoboa, installation, requirements, cpu, ram, disk, python' +--- + # Requirements You will need a Server to perform well with at least: diff --git a/doc-ng/installation/upgrade.md b/doc-ng/installation/upgrade.md index 95f03c1ec..00cfd9e24 100644 --- a/doc-ng/installation/upgrade.md +++ b/doc-ng/installation/upgrade.md @@ -1,3 +1,12 @@ +--- +title: Guide to update a Modoboa instance to the last version +description: This guide perform how to upgrade your current Modoboa instance to the last version available +head: + - - meta + - name: 'keywords' + content: 'modoboa, installation, upgrade, procedure' +--- + # Upgrade ## Modoboa diff --git a/doc-ng/installation/webserver.md b/doc-ng/installation/webserver.md index acf12a4d4..5e9fdab5e 100644 --- a/doc-ng/installation/webserver.md +++ b/doc-ng/installation/webserver.md @@ -1,3 +1,12 @@ +--- +title: manual step to install webserver component on modoboa +description: This guide describe how to install webserver on the modoboa platform +head: + - - meta + - name: 'keywords' + content: 'modoboa, installation, webserver, nginx, apache, wsgi' +--- + # Web server {#webserver} ::: info diff --git a/doc-ng/moving.md b/doc-ng/moving.md index cd47f2a3b..da59f7d9f 100644 --- a/doc-ng/moving.md +++ b/doc-ng/moving.md @@ -1,3 +1,13 @@ +--- +title: Migrate to modoboa +description: Migrate from actual provider to modoboa +head: + - - meta + - name: 'keywords' + content: 'modoboa, imap, migration, constraint, offlineimap' +--- + + # Moving to Modoboa You have an existing platform and you\'d like to move to Modoboa, the diff --git a/doc-ng/rebuild_venv.md b/doc-ng/rebuild_venv.md index 8b879fca6..71b10397d 100644 --- a/doc-ng/rebuild_venv.md +++ b/doc-ng/rebuild_venv.md @@ -1,3 +1,12 @@ +--- +title: How to rebuild your virtual environment +description: Procedure to repair your broken environment +head: + - - meta + - name: 'keywords' + content: 'modoboa, virtualenv, broken, ubuntu' +--- + # Rebuild Virtualenv Sometimes when upgrading your Operating System (eg from Ubuntu 17.04 to diff --git a/doc-ng/rest_api.md b/doc-ng/rest_api.md index b856c2e02..9314ccbdc 100644 --- a/doc-ng/rest_api.md +++ b/doc-ng/rest_api.md @@ -1,3 +1,12 @@ +--- +title: REST API to control your Modoboa instance. +description: Guide to describe the use of the REST API to manage your modoboa instance +head: + - - meta + - name: 'keywords' + content: 'modoboa, rest api, swagger, authorization, token' +--- + # REST API To ease the integration with external sources (software or other), Modoboa provides a REST API. @@ -9,19 +18,19 @@ Every installed instance comes with a ready-to-use API and a documentation. - New API+Docs: *https://\/api/schema-v2/swagger/* - Old API: *http://\/api/v1/* -::: info +::: warning You need to be logged in for the API docs to show. ::: **Examples:** -* [new example](https://demo.modoboa.org/api/schema-v2/swagger/) +* [Demo Instance](https://demo.modoboa.org/api/schema-v2/swagger/ "Demo Instance Swagger API") documentation is available on the official demo. Using this API requires an authentication and for now, only a token based authentication is supported. To get a valid token, log-in to your -instance with a super administrator, go to *Settings \> API* and +instance with a super administrator, go to `Settings > API` and activate the API access. Press the Update button and wait until the page is reloaded, the token will be displayed. diff --git a/doc-ng/upgrade.md b/doc-ng/upgrade.md deleted file mode 100644 index 266523e69..000000000 --- a/doc-ng/upgrade.md +++ /dev/null @@ -1,2241 +0,0 @@ -# Upgrade - -## Modoboa - -::: tip - -In this doc, `by default` means that you used [modoboa -installer](https://github.com/modoboa/modoboa-installer) to install -modoboa and that your didn\'t change your configuration. -::: - -::: warning -The new version you are going to install may need to modify your -database. Before you start, make sure to backup everything! -::: - -Most of the time, upgrading your installation to a newer Modoboa version -only requires a few actions. In every case, you will need to apply the -specific actions if the version you are installing requires it, and then -apply the general ones. - -In case you use a dedicated user and/or a virtualenv, do not forget to -use them: - -``` bash -> sudo -u -i bash -> source /bin/activate -``` - -::: info -Check the `specific_upgrade_instructions`{.interpreted-text role="ref"} -before running the following commands. -::: - -Then, run the following commands: - -``` bash -> pip install modoboa== -> cd -> python manage.py migrate -> python manage.py collectstatic -> python manage.py check --deploy -> python manage.py load_initial_data -``` - -Then when everything has been updated, restart modoboa\'s services: - -``` bash -# sudo systemctl restart uwsgi -# sudo systemctl restart supervisor -``` - -Finally, restart your web server. - -Sometimes, you might need to upgrade postfix map files too. To do so, -just run the `generate_postfix_maps` command on the same directory than -the one used for installation (`/etc/postfix`{.interpreted-text -role="file"} by default). - -Make sure to use root privileges and run the following command: - -``` bash -> python manage.py generate_postfix_maps --destdir -``` - -Then, reload postfix. - -## Modoboa installer - -It is important to keep the installer updated because it works on -rolling release. To do that, go to modoboa-installer directory then: - -``` bash -> git fetch && git pull -``` - -If some error appears, you may try this: - -``` bash -> git reset --hard origin/master -``` - -## Extensions - -:::: warning -::: title -Warning -::: - -Do not open an issue after an upgrade if you have not updated your -extensions. -:::: - -If a new version is available for an extension you\'re using, it is -recommanded to install it. It may even be required. Upgrading an -extensions is pretty and the procedure is almost the same than the one -used for Modoboa. - -In case you use a dedicated user and/or a virtualenv, do not forget to -use them: - -``` bash -> sudo -u -i bash -> source /bin/activate -``` - -Then, run the following commands: - -``` bash -> pip install == -> cd -> python manage.py migrate -> python manage.py collectstatic -> python manage.py check --deploy -``` - -Finally, restart your web server. - -It is a generic upgrade procedure which will be enough most of the time -but it is generally a good idea to check the associated documentation. - -## Rebuild Virtual Environment - -## Specific instructions {#specific_upgrade_instructions} - -### Version 2.6.1 - -#### Required changes to `settings.py`{.interpreted-text role="file"} - -- Add the following lines to `LOGGING` variable: - - ``` python - 'handlers': { - ... - 'syslog': { - 'class': 'logging.handlers.SysLogHandler', - 'facility': SysLogHandler.LOG_SYSLOG, - 'formatter': 'syslog' - }, - ... - }, - 'loggers': { - ... - 'modoboa.dns': { - 'handlers': ['syslog'], - 'level': 'INFO', - 'propagate': False - }, - ... - } - ``` - -### Version 2.6.0 - -New built-in auto-configuration service. - -#### Required changes to `settings.py`{.interpreted-text role="file"} - -- Add the following line to `MODOBOA_APPS` variable: - - ``` python - 'modoboa.autoconfig' - ``` - -- Add the following hostnames to `ALLOWED_HOSTS` variable: - - ``` python - 'autoconfig.', - 'autodiscover.', - ``` - -- Add the following variable: - - ``` python - EMAIL_CLIENT_CONNECTION_SETTINGS = { - "imap": { - "HOSTNAME": "", - "SOCKET_TYPE": "SSL", - "PORT": 993, - }, - "smtp": {"HOSTNAME": "", "SOCKET_TYPE": "STARTTLS", "PORT": 587}, - } - ``` - -- Remove `rest_framework.authentication.SessionAuthentication` from - `REST_FRAMEWORK` variable: - - ``` python - REST_FRAMEWORK = { - # Stuff before... - "DEFAULT_AUTHENTICATION_CLASSES": ( - "oauth2_provider.contrib.rest_framework.OAuth2Authentication", - "rest_framework.authentication.TokenAuthentication", - ), - # Stuff after... - } - ``` - -### Version 2.5.0 - -Amavis plugin integrated into Modoboa core repository. If you are using -Amavis content filter and the quarantine plugin, the following upgrade -instructions, otherwise proceed with regular upgrade instructions. - -#### Pre update - -- If you are using Postegresql (default option with the installer), run - as a root user: - - ``` bash - > sudo -u postgres -i psql -d "modoboa" -c "UPDATE django_content_type SET app_label='amavis' WHERE app_label='modoboa_amavis'" - > sudo -u postgres -i psql -d "modoboa" -c "UPDATE django_migrations SET app='amavis' WHERE app='modoboa_amavis'" - ``` - -- If you are using Mysql, run as a root user: - - ``` bash - > echo "UPDATE django_content_type SET app_label='amavis' WHERE app_label='modoboa_amavis'" | mysql -u root -p modoboa - > echo "UPDATE django_migrations SET app='amavis' WHERE app='modoboa_amavis'" | mysql -u root -p modoboa - ``` - -#### Required changes to `settings.py`{.interpreted-text role="file"} - -- Rename the following extensions in the `MODOBOA_APPS` variable: - - - `modoboa_amavis` -\> `modoboa.amavis` - -- Add the following variables: - - ``` python - DATABASE_ROUTERS = ["modoboa.amavis.dbrouter.AmavisRouter"] - - AMAVIS_DEFAULT_DATABASE_ENCODING = "LATIN1" # or any value matching your database config - ``` - -- Remove configuration block related to `modoboa_amavis` plugin (these - should be at the end of the file) - -#### Post update - -- Run the following command: - - $ pip uninstall modoboa_amavis - -### Version 2.4.2 - -#### Changes to `settings.py`{.interpreted-text role="file"} - -- Make sure `MODOBOA_APPS` variable contains the following line: - - ``` python - 'modoboa.webmail' - ``` - -### Version 2.4.0 - -:::: warning -::: title -Warning -::: - -The `modoboa-amavis` extension has not been migrated into the core -Modoboa package yet. If you are using it, **you cannot install version -2.4.0 yet**. -:::: - -Legacy web interface has been removed and almost all extensions have -been integrated to the core Modoboa package. - -#### Pre update - -- If you are using Postegresql (default option with the installer), run - as a root user: - - ``` bash - > sudo -u postgres -i psql -d "modoboa" -c "UPDATE django_content_type SET app_label='autoreply' WHERE app_label='postfix_autoreply'" - > sudo -u postgres -i psql -d "modoboa" -c "UPDATE django_migrations SET app='autoreply' WHERE app='postfix_autoreply'" - > sudo -u postgres -i psql -d "modoboa" -c "UPDATE django_content_type SET app_label='contacts' WHERE app_label='modoboa_contacts'" - > sudo -u postgres -i psql -d "modoboa" -c "UPDATE django_migrations SET app='contacts' WHERE app='modoboa_contacts'" - > sudo -u postgres -i psql -d "modoboa" -c "UPDATE django_content_type SET app_label='calendars' WHERE app_label='modoboa_radicale'" - > sudo -u postgres -i psql -d "modoboa" -c "UPDATE django_migrations SET app='calendars' WHERE app='modoboa_radicale'" - ``` - -- If you are using Mysql, run as a root user: - - ``` bash - > echo "UPDATE django_content_type SET app_label='autoreply' WHERE app_label='postfix_autoreply'" | mysql -u root -p modoboa - > echo "UPDATE django_migrations SET app='autoreply' WHERE app='postfix_autoreply'" | mysql -u root -p modoboa - > echo "UPDATE django_content_type SET app_label='contacts' WHERE app_label='modoboa_contacts'" | mysql -u root -p modoboa - > echo "UPDATE django_migrations SET app='contacts' WHERE app='modoboa_contacts'" | mysql -u root -p modoboa - > echo "UPDATE django_content_type SET app_label='calendars' WHERE app_label='modoboa_radicale'" | mysql -u root -p modoboa - > echo "UPDATE django_migrations SET app='calendars' WHERE app='modoboa_radicale'" | mysql -u root -p modoboa - ``` - -:::: warning -::: title -Warning -::: - -MariaDB 10.7+ users might have login issues (see this -[discussion](https://github.com/modoboa/modoboa/discussions/3582)). For -those impacted, run the followed SQL queries: - -``` bash -> echo "ALTER TABLE `oauth2_provider_idtoken` MODIFY `jti` uuid;" | mysql -u root -p modoboa -> echo "ALTER TABLE `oauth2_provider_refreshtoken` MODIFY `token_family` uuid;" | mysql -u root -p modoboa -``` -:::: - -#### Required changes to `settings.py`{.interpreted-text role="file"} - -- Rename the following extensions in the `MODOBOA_APPS` variable: - - - `modoboa.postfix_autoreply` -\> `modoboa.autoreply` - - `modoboa_contacts` -\> `modoboa.contacts` - - `modoboa_radicale` -\> `modoboa.calendars` - - `modoboa_webmail` -\> `modoboa.webmail` - - `modoboa_rspamd` -\> `modoboa.rspamd` - -- Remove configuration blocks related to `modoboa_contacts` and - `modoboa_radicale` plugins (these should be at the end of the file) - -- Remove `AjaxLoginRedirect` from `MIDDLEWARE` variable: - - > ``` python - > MIDDLEWARE = ( - > 'django.contrib.sessions.middleware.SessionMiddleware', - > 'django.middleware.locale.LocaleMiddleware', - > 'x_forwarded_for.middleware.XForwardedForMiddleware', - > 'corsheaders.middleware.CorsMiddleware', - > 'django.middleware.common.CommonMiddleware', - > 'django.middleware.csrf.CsrfViewMiddleware', - > 'django.contrib.auth.middleware.AuthenticationMiddleware', - > 'django.contrib.messages.middleware.MessageMiddleware', - > 'django.middleware.clickjacking.XFrameOptionsMiddleware', - > 'modoboa.core.middleware.LocalConfigMiddleware', - > 'modoboa.lib.middleware.CommonExceptionCatcher', - > 'modoboa.lib.middleware.RequestCatcherMiddleware', - > ) - > ``` - -- Remove `NEW_ADMIN_URL` variable if found - -- Remove `new_admin_url` and `top_notifications` context processors - -- Remove `ckeditor` related apps from `INSTALLED_APPS` variable and - `CKEDITOR_*` settings - -- Remove `STATICFILE_DIRS` variable - -Radicale configuration changes -\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-- - -You must enable oauth2 authentication in Radicale to make the calendar -and contact feature work. Follow this -`guide `{.interpreted-text role="ref"}. - -#### Post update - -- Run the following command: - - $ python manage.py load_initial_data - -- Update your `webserver `{.interpreted-text role="ref"} - configuration - -- Remove `autoreply` service definition from postfix\`s - `master.cf`{.interpreted-text role="file"} file if you were using it - -- Make sure - `/etc/uwsgi/sites-enabled/`{.interpreted-text - role="file"} contains [max-requests=5000]{.title-ref} and that - [process]{.title-ref} is superior or equal to 4. Don\'t forget to - restart uwsgi service after the configuration change. - -- Then proceed with regular upgrade instructions - -### Version 2.3.3 - -:::: warning -::: title -Warning -::: - -The `crypt ({CRYPT})` password scheme has been marked as deprecated and -will be removed from Modoboa starting with version **2.4.0**. If you are -still using this weak scheme, you must change to something safer -**ASAP**. Installations still using this scheme won\'t be able to -install Modoboa 2.4 and upper\... -:::: - -Update your `settings.py`{.interpreted-text role="file"} file with -following content: - -``` python -SILENCED_SYSTEM_CHECKS = [ - "security.W019", # modoboa uses iframes to display e-mails - "ckeditor.W001", # CKEditor 4.22.1 warning -] -``` - -This will silence a [warning sent by -django-ckeditor](https://github.com/modoboa/modoboa/issues/3316). - -### Version 2.3.0 - -:::: warning -::: title -Warning -::: - -For this particular version, it is really important that you apply new -migrations **AFTER** the following instructions. If you don\'t, you\'ll -get into trouble\... -:::: - -#### Pre update - -:::: note -::: title -Note -::: - -To check that you were actually using modoboa-postfix-autoreply you can -check if this command returns something: - -``` bash -cat /srv/modoboa/instance/instance/settings.py | grep "modoboa_postfix_autoreply" -``` -:::: - -If you were using -[modoboa-postfix-autoreply](https://github.com/modoboa/modoboa-postfix-autoreply) -extension, you need to perfome a few commands: - -- If you are using Postegresql (default option with the installer), run - as a root user: - - ``` bash - > sudo -u postgres -i psql -d "modoboa" -c "UPDATE django_content_type SET app_label='postfix_autoreply' WHERE app_label='modoboa_postfix_autoreply'" - > sudo -u postgres -i psql -d "modoboa" -c "UPDATE django_migrations SET app='postfix_autoreply' WHERE app='modoboa_postfix_autoreply'" - ``` - -- If you are using Mysql, run as a root user: - - ``` bash - > echo "UPDATE django_content_type SET app_label='postfix_autoreply' WHERE app_label='modoboa_postfix_autoreply'" | mysql -u root -p modoboa - > echo "UPDATE django_migrations SET app='postfix_autoreply' WHERE app='modoboa_postfix_autoreply'" | mysql -u root -p modoboa - ``` - -#### Required changes to `settings.py`{.interpreted-text role="file"} - -Some changes are required to your `settings.py`{.interpreted-text -role="file"} file. These changes will not be performed by the installer. -The location of this file is `/srv/modoboa/instance/instance` by -default. - -- Append `'import environ'` after `'import os'`: - - ``` python - import os - import environ - ``` - -- Append the following lines after the line starting with `'BASE_DIR'`: - - ``` python - # Build paths inside the project like this: os.path.join(BASE_DIR, ...) - BASE_DIR = os.path.realpath(os.path.dirname(os.path.dirname(__file__))) - env = environ.Env() - environ.Env.read_env(os.path.join(BASE_DIR, '.env')) - ``` - -- Add `oauth2_provider` and `corsheaders` to the `INSTALLED_APPS` list: - - ``` python - INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.sites', - 'django.contrib.staticfiles', - 'reversion', - 'ckeditor', - 'ckeditor_uploader', - 'oauth2_provider', - 'corsheaders', - 'rest_framework', - 'rest_framework.authtoken', - 'drf_spectacular', - 'phonenumber_field', - 'django_otp', - 'django_otp.plugins.otp_totp', - 'django_otp.plugins.otp_static', - 'django_rename_app', - 'django_rq', - ) - ``` - -- The `modoboa-postfix-autoreply` and `modoboa-sievefilters` plugins - have been merged into the core. - - Add `'modoboa.postfix_autoreply'` and `modoboa.sievefilters` to - `MODOBOA_APPS`: - - ``` python - MODOBOA_APPS = ( - 'modoboa', - 'modoboa.core', - 'modoboa.lib', - 'modoboa.admin', - 'modoboa.transport', - 'modoboa.relaydomains', - 'modoboa.limits', - 'modoboa.parameters', - 'modoboa.dnstools', - 'modoboa.policyd', - 'modoboa.maillog', - 'modoboa.dmarc', - 'modoboa.pdfcredentials', - 'modoboa.imap_migration', - 'modoboa.postfix_autoreply', - 'modoboa.sievefilters', - ) - ``` - - :::: warning - ::: title - Warning - ::: - - Remove any reference to `'modoboa_postfix_autoreply'` AND - `modoboa_sievefilters` in this same variable. - :::: - -- Due to the changes to the authentication flow, you need to replace - MIDDLEWARE with the following: - - ``` python - MIDDLEWARE = ( - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.locale.LocaleMiddleware', - 'x_forwarded_for.middleware.XForwardedForMiddleware', - 'corsheaders.middleware.CorsMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'modoboa.core.middleware.LocalConfigMiddleware', - 'modoboa.lib.middleware.AjaxLoginRedirect', - 'modoboa.lib.middleware.CommonExceptionCatcher', - 'modoboa.lib.middleware.RequestCatcherMiddleware', - ) - ``` - -- Replace `'modoboa.core.drf_authentication.JWTAuthenticationWith2FA'` - by `'oauth2_provider.contrib.rest_framework.OAuth2Authentication'` at - the top of the list of `DEFAULT_AUTHENTICATION_CLASSES` (inside - `REST_FRAMEWORK` section): - - ``` python - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'oauth2_provider.contrib.rest_framework.OAuth2Authentication', - 'rest_framework.authentication.TokenAuthentication', - 'rest_framework.authentication.SessionAuthentication', - ), - ``` - -Save your modifications at this point and run the `post upgrade -commands `{.interpreted-text role="ref"} now. - -Then, apply this last modification to your settings: - -- Run the commands indicated - `in the first section `{.interpreted-text - role="ref"} then add the following content after `MEDIA_ROOT`: - - ``` python - # oAuth2 settings - - OAUTH2_PROVIDER = { - 'OIDC_ENABLED': True, - 'OIDC_RP_INITIATED_LOGOUT_ENABLED': True, - 'OIDC_RP_INITIATED_LOGOUT_ALWAYS_PROMPT': True, - 'OIDC_RSA_PRIVATE_KEY': env.str('OIDC_RSA_PRIVATE_KEY', multiline=True), - 'SCOPES': { - 'openid': 'OpenID Connect scope', - 'read': 'Read scope', - 'write': 'Write scope', - 'introspection': 'Introspect token scope', - }, - 'DEFAULT_SCOPES': ['openid', 'read', 'write'], - } - - # If CORS fail, you might want to try to set it to True - #CORS_ORIGIN_ALLOW_ALL = False - ``` - -#### Docevot configuration changes - -You must enable oauth2 authentication in Dovecot to make the filters -editor work in the new UI. Follow this -`guide `{.interpreted-text role="ref"}. - -#### Post update - -Run this command to be sure to have everything set. - -``` bash -python manage.py modo repair -``` - -You need to set the `buffer-size` to `8192` in uwsgi config. Edit the -value in `/etc/uwsgi/apps-available/automx_instance.ini` to match at -least 8192 or add it if it was not set: - -``` bash -buffer-size = 8192 -``` - -#### SORBS DNS - -> :::: note -> ::: title -> Note -> ::: -> -> The installer will take care of that if you use it to upgrade. -> :::: -> -> [SORBS DNS has been -> deprecated](https://www.mail-archive.com/mailop@mailop.org/msg22064.html). -> If you were using them with postfix, you need to update your -> configuration by removing any reference to it. For example: -> -> # Use some DNSBL -> postscreen_dnsbl_sites = -> zen.spamhaus.org=127.0.0.[2..11]*3 -> bl.spameatingmonkey.net=127.0.0.2*2 -> bl.spamcop.net=127.0.0.2 - -### Version 2.2.3 - -- Reduced the number of call to doveadm - -You need to setup a new worker : - -1. Create a new supervisord config - (`/etc/supervisor/conf.d/modoboa-base-worker.conf` by default) : - -``` ini -[program:modoboa-base-worker] -autostart=true -autorestart=true -command={%python env path%} {% manage.py instance path%} rqworker modoboa -directory={%modoboa home dir%} -user=%{modoboa user} -numprocs=1 -stopsignal=TERM -``` - -`python env path` : Python executable located in your virtual -environment created for modoboa. You will find it here -`/srv/modoboa/venv/bin/python` by default. `manage.py instance path`: -Path to manage.py of your modoboa instance. You will find it here : -`/srv/modoboa/instance/manage.py`by default. `Modoboa home dir`: Home -dir of the user running modooba. You will find it here `/srv/modoboa/` -by default. `modoboa user` : User managing dkim signing (modoboa by -default). - -You can help you with `/etc/supervisor/conf.d/policyd.conf` (by -default). - -Then restart supervisor. -`#> supervisorctl reread && supervisorctl update` on Debian. - -2. Update the RQ_QUEUES section bellow the `#REDIS` section: - -``` python -# RQ -RQ_QUEUES = { - ... - }, - 'modoboa': { - 'HOST': REDIS_HOST, - 'PORT': REDIS_PORT, - 'DB': 0, - }, -} -``` - -3. Add the CACHE section bellow the `# RQ` one: - -``` python -# CACHE -CACHES = { - "default": { - "BACKEND": 'django.core.cache.backends.redis.RedisCache', - "LOCATION": REDIS_URL - } -} -``` - -### Version 2.2.0 - -:::: note -::: title -Note -::: - -If you use the installer to perform the upgrade, you NEED to update it. -See above for instructions. After updating the installer and modoboa -with it, you only need to edit `urls.py` and `settings.py` -:::: - -- Django has been updated to 4.2LTS. Please upgrade all your extensions - alongside modoboa -- Support for Python 3.7 has been dropped, minimum Python version is now - 3.8 -- Support for Postgres 11 has been dropped, minimum Postgres version is - now 12 - -You need to change the first line in `urls.py` and replace -[url]{.title-ref} with [path]{.title-ref} (in -`/srv/modoboa/instance/instance/urls.py` by default): - -``` python -# from django.conf.urls import include, url -from django.urls import include, path - -urlpatterns = [ - path(r'', include('modoboa.urls')), -] -``` - -If you use Postgresql, you need to install pyscopg3+: - -``` bash -pip install psycopg[binary]>=3.1 -``` - -RQ has been added. This should replace the use of cron jobs in the -future. For now, only `manage_dkim_keys` has been migrated. This will -make the dkim key generation asynchronous but the task will be started -as soon as the generation is required. - -Follow these instructions to perform the update in case you used -supervisord for the installation (this apply if you used -`modoboa-installer`): - -1. Edit settings.py (`/srv/modoboa/instance/instance/settings.py` by - default) and add: - -``` python -INSTALLED_APPS = ( - ..., - 'django_rq', -) -``` - -and add the RQ section bellow the `#REDIS` section: - -``` python -# RQ -RQ_QUEUES = { - 'dkim': { - 'HOST': REDIS_HOST, - 'PORT': REDIS_PORT, - 'DB': 0, - }, -} -``` - -Then by default you will need to restart `uwsgi` service -(`systemctl restart uwsgi` on Debian). - -2. Create a new supervisord config - (`/etc/supervisor/conf.d/modoboa-worker.conf` by default) : - -``` ini -[program:modoboa-worker] -autostart=true -autorestart=true -command={%python env path%} {% manage.py instance path%} rqworker dkim -directory={%modoboa home dir%} -user=%{dkim manager user} -numprocs=1 -stopsignal=TERM -``` - -`python env path` : Python executable located in your virtual -environment created for modoboa. You will find it here -`/srv/modoboa/venv/bin/python` by default. `manage.py instance path`: -Path to manage.py of your modoboa instance. You will find it here : -`/srv/modoboa/instance/manage.py`by default. `Modoboa home dir`: Home -dir of the user running modooba. You will find it here `/srv/modoboa/` -by default. `dkim manager user` : User managing dkim signing (opendkim -by default). - -You can help you with `/etc/supervisor/conf.d/policyd.conf` (by -default). - -Then restart supervisor. -`#> supervisorctl reread && supervisorctl update` on Debian. - -Admins now have the option to setup a send-only mailbox. - -Send-only mailboxes do not have access to IMAP. You need to change your -dovecot configuration to enable it. - -:::: note -::: title -Note -::: - -modoboa-webmail may not work with send-only user for now. -:::: - -If you use `Postgres`: Change `user_query` and `password_query` in -`/etc/dovecot/dovecot-sql.conf.ext`: - - user_query = SELECT '%{home_dir}/%%d/%%n' AS home, %mailboxes_owner_uid as uid, %mailboxes_owner_gid as gid, '*:bytes=' || mb.quota || 'M' AS quota_rule FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN core_user u ON u.id=mb.user_id WHERE (mb.is_send_only IS NOT TRUE OR '%s' NOT IN ('imap', 'pop3', 'lmtp')) AND mb.address='%%n' AND dom.name='%%d' - - password_query = SELECT email AS user, password, '%{home_dir}/%%d/%%n' AS userdb_home, %mailboxes_owner_uid AS userdb_uid, %mailboxes_owner_gid AS userdb_gid, CONCAT('*:bytes=', mb.quota, 'M') AS userdb_quota_rule FROM core_user u INNER JOIN admin_mailbox mb ON u.id=mb.user_id INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE (mb.is_send_only IS NOT TRUE OR '%s' NOT IN ('imap', 'pop3')) AND email='%%u' AND is_active AND dom.enabled - -You basically simply need to add -`(mb.is_send_only IS NOT TRUE OR '%s' NOT IN ('imap', 'pop3', 'lmtp')) AND` -for `user_query` after `WHERE` and -`(mb.is_send_only IS NOT TRUE OR '%s' NOT IN ('imap', 'pop3')) AND` for -`password_query` after `WHERE`. - -If you use `MySQL`: Change `user_query` and `password_query` in -`/etc/dovecot/dovecot-sql-master.ext`: - - user_query = SELECT '%{home_dir}/%%d/%%n' AS home, %mailboxes_owner_uid as uid, %mailboxes_owner_gid as gid, CONCAT('*:bytes=', mb.quota, 'M') AS quota_rule FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN core_user u ON u.id=mb.user_id WHERE (mb.is_send_only=0 OR '%s' NOT IN ('imap', 'pop3', 'lmtp')) AND mb.address='%%n' AND dom.name='%%d' - - password_query = SELECT email AS user, password, '%{home_dir}/%%d/%%n' AS userdb_home, %mailboxes_owner_uid AS userdb_uid, %mailboxes_owner_gid AS userdb_gid, CONCAT('*:bytes=', mb.quota, 'M') AS userdb_quota_rule FROM core_user u INNER JOIN admin_mailbox mb ON u.id=mb.user_id INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE (mb.is_send_only=0 OR '%s' NOT IN ('imap', 'pop3')) AND u.email='%%u' AND u.is_active=1 AND dom.enabled=1 - -You basically simply need to add -`(mb.is_send_only=0 OR '%s' NOT IN ('imap', 'pop3', 'lmtp')) AND` for -`user_query` after `WHERE` and -`(mb.is_send_only=0 OR '%s' NOT IN ('imap', 'pop3'))` for -`password_query` after `WHERE`. - -### Version 2.1.0 - -:::: warning -::: title -Warning -::: - -For this particular version, it is really important that you apply new -migrations **AFTER** the following instructions. If you don\'t, you\'ll -get into trouble\... -:::: - -The `modoboa-dmarc`, `modoboa-pdfcredentials` and -`modoboa-imapmigration` plugins have been merged into the core. - -Add `'django_rename_app,'` To `INSTALLED_APPS`: - -``` python -INSTALLED_APPS = ( -..., -'django_rename_app', -) -``` - -Add `'modoboa.dmarc'`, `'modoboa.pdfcredentials'` and -`'modoboa.imap_migration'` to `MODOBOA_APPS`: - -``` python -MODOBOA_APPS = ( - 'modoboa', - 'modoboa.core', - 'modoboa.lib', - 'modoboa.admin', - 'modoboa.transport', - 'modoboa.relaydomains', - 'modoboa.limits', - 'modoboa.parameters', - 'modoboa.dnstools', - 'modoboa.policyd', - 'modoboa.maillog', - 'modoboa.dmarc', - 'modoboa.pdfcredentials', - 'modoboa.imap_migration', -) -``` - -And remove any reference to `'modoboa_dmarc'`, -`'modoboa_pdfcredentials'` or `'modoboa_imap_migration'` in this same -variable. - -You need to add `'modoboa.imap_migration.auth_backends.IMAPBackend',` at -the end of `AUTHENTICATION_BACKENDS`: - -``` python -AUTHENTICATION_BACKENDS = ( - 'django.contrib.auth.backends.ModelBackend', - 'modoboa.imap_migration.auth_backends.IMAPBackend', -) -``` - -:::: note -::: title -Note -::: - -It won\'t affect login if you don\'t use `imap_migration` feature. It is -therefor highly recommended to add this authentication backend. -:::: - -After upgrading modoboa, run the following commands from your virtual -environment: - -``` bash -> python manage.py rename_app modoboa_dmarc dmarc -> python manage.py rename_app modoboa_imap_migration imap_migration -``` - -### Version 2.0.4 - -The following modifications must be applied to the -`settings.py`{.interpreted-text role="file"} file: - -- Add `'modoboa.core.context_processors.new_admin_url',` at the end of - [context_processors]{.title-ref} list: - -``` python -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.template.context_processors.i18n', - 'django.template.context_processors.media', - 'django.template.context_processors.static', - 'django.template.context_processors.tz', - 'django.contrib.messages.context_processors.messages', - 'modoboa.core.context_processors.top_notifications', - 'modoboa.core.context_processors.new_admin_url', - ], - 'debug': False, - }, - }, -] -``` - -- Add the following variable: - -``` python -PID_FILE_STORAGE_PATH = '/var/run' -``` - -- Update `LOGGING` variable to define an address for `syslog-auth` - handler: - -``` python -'syslog-auth': { - 'class': 'logging.handlers.SysLogHandler', - 'facility': SysLogHandler.LOG_AUTH, - 'formatter': 'syslog', - 'address': '/dev/log' -}, -``` - -- - - You now have the possibility to customize the url of the new-admin interface. - - : To do so please head up to - `the custom configuration chapter `{.interpreted-text - role="ref"} (advanced user). - -- Add `DEFAULT_THROTTLE_RATES` to `REST_FRAMEWORK`: - -``` python -REST_FRAMEWORK = { - 'DEFAULT_THROTTLE_RATES': { - 'user': '300/minute', - 'ddos': '5/second', - 'ddos_lesser': '200/minute', - 'login': '10/minute', - 'password_recovery_request': '12/hour', - 'password_recovery_totp_check': '25/hour', - 'password_recovery_apply': '25/hour' - }, - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'modoboa.core.drf_authentication.JWTAuthenticationWith2FA', - 'rest_framework.authentication.TokenAuthentication', - 'rest_framework.authentication.SessionAuthentication', - ), - 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', - 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning', -} -``` - -- - - You can edit the `DEFAULT_THROTTLE_RATES` to whatever value suits you. - - : - [user]{.title-ref} is for every endpoint, it is per user or per - ip if not logged. - - [ddos]{.title-ref} is per api endpoint and per user or per ip if - not logged. - - [ddos_lesser]{.title-ref} is for per api endpoint and per user - or per ip if not logged. This is for api endpoint that are - lighter. - - [login]{.title-ref} the number of time an ip can attempt to log. - The counter will reset on login success. - - [password\_]{.title-ref} is for the recovery, it is divided per - step in the recovery process. - -### Version 2.0.3 - -Update the new following setting in the `settings.py`{.interpreted-text -role="file"} file: - -``` python -DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' -``` - -### Version 2.0.0 - -Add `drf_spectacular` and `phonenumber_field` to `INSTALLED_APPS` in the -`settings.py`{.interpreted-text role="file"} file, as follows: - -``` python -INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.sites', - 'django.contrib.staticfiles', - 'reversion', - 'ckeditor', - 'ckeditor_uploader', - 'rest_framework', - 'rest_framework.authtoken', - 'drf_spectacular', - 'phonenumber_field', - 'django_otp', - 'django_otp.plugins.otp_totp', - 'django_otp.plugins.otp_static', -) -``` - -Modify the `REST_FRAMEWORK` setting as follows: - -``` python -REST_FRAMEWORK = { - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'modoboa.core.drf_authentication.JWTAuthenticationWith2FA', - 'rest_framework.authentication.TokenAuthentication', - 'rest_framework.authentication.SessionAuthentication', - ), - 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', - 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning', -} -``` - -Add the new following settings: - -``` python -SPECTACULAR_SETTINGS = { - 'SCHEMA_PATH_PREFIX': r'/api/v[0-9]', - 'TITLE': 'Modoboa API', - 'VERSION': None, - 'SERVE_PERMISSIONS': ['rest_framework.permissions.IsAuthenticated'], -} - -PHONENUMBER_DB_FORMAT = 'INTERNATIONAL' - -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -``` - -#### Migration issue for Postgres/OpenDKIM users - -The [migration will probably -fail](https://github.com/modoboa/modoboa/issues/2508) because of the -additional view created for OpenDKIM. - -To make it work, you first need to drop the view: - -``` bash -$ sudo su - postgres -$ psql -psql (12.10 (Ubuntu 12.10-0ubuntu0.20.04.1)) -Type "help" for help. - -postgres=# \c modoboa -You are now connected to database "modoboa" as user "postgres". -modoboa-# \d+ dkim - View "public.dkim" - Column | Type | Collation | Nullable | Default | Storage | Description -------------------+------------------------+-----------+----------+---------+----------+------------- - id | integer | | | | plain | - domain_name | character varying(100) | | | | extended | - private_key_path | character varying(254) | | | | extended | - selector | character varying(30) | | | | extended | -View definition: - SELECT admin_domain.id, - admin_domain.name AS domain_name, - admin_domain.dkim_private_key_path AS private_key_path, - admin_domain.dkim_key_selector AS selector - FROM admin_domain - WHERE admin_domain.enable_dkim; - -modoboa=# drop view dkim; -DROP VIEW -``` - -Then, run the migration as usual: - -``` bash -python manage.py migrate -``` - -Finally, recreate the view: - -``` bash -modoboa=# CREATE OR REPLACE VIEW dkim AS -modoboa-# SELECT admin_domain.id, -modoboa-# admin_domain.name AS domain_name, -modoboa-# admin_domain.dkim_private_key_path AS private_key_path, -modoboa-# admin_domain.dkim_key_selector AS selector -modoboa-# FROM admin_domain -modoboa-# WHERE admin_domain.enable_dkim; -CREATE VIEW -modoboa=# grant select on dkim to opendkim; -GRANT -``` - -#### New admin interface - -This new release brings a new admin interface written with Vue.js -framework. It is a work in progress and all features are not yet -implemented - i.e. extensions integration - but you could give it a try. -It uses a new API version but the old one is still available. - -You will need to copy the frontend files in the folder you specified in -your web server configuration. If you used the installer, the folder -should be `/srv/modoboa/instance/frontend`: - -``` bash -mkdir /srv/modoboa/instance/frontend -cp -r /srv/modoboa/env/lib/pythonX.X/site-packages/modoboa/frontend_dist/* /srv/modoboa/instance/frontend -``` - -Then, edit the -`/srv/modoboa/instance/frontend/config.json`{.interpreted-text -role="file"} and update the `API_BASE_URL` setting according to the -hostname of your server: - -``` json -{ - "API_BASE_URL": "https:///api/v2" -} -``` - -Finally, update the configuration of your web server to serve the -frontend files. For NGINX, you should add the following in the `server` -block: - -``` nginx -location ^~ /new-admin { - alias /srv/modoboa/instance/frontend/; - index index.html; - - expires -1; - add_header Pragma "no-cache"; - add_header Cache-Control "no-store, no-cache, must-revalidate, post-check=0, pre-check=0"; - - try_files $uri $uri/ /index.html = 404; -} -``` - -### Version 1.17.0 - -Modoboa now supports Two-Factor authentication using TOTP applications. - -To enable it, install the following new requirement in your virtualenv: - -``` bash -(env)> pip install django-otp qrcode -``` - -Then, open the `settings.py`{.interpreted-text role="file"} file and add -`django_otp` packages to `INSTALLED_APPS`: - -``` python -INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.sites', - 'django.contrib.staticfiles', - 'reversion', - 'ckeditor', - 'ckeditor_uploader', - 'rest_framework', - 'rest_framework.authtoken', - 'django_otp', - 'django_otp.plugins.otp_totp', - 'django_otp.plugins.otp_static', -) -``` - -Add new middlewares to `MIDDLEWARE`. It has to be after the -SessionMiddleware and before the CommonMiddleware: - -``` python -MIDDLEWARE = ( - 'x_forwarded_for.middleware.XForwardedForMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.locale.LocaleMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django_otp.middleware.OTPMiddleware', - 'modoboa.core.middleware.TwoFAMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'modoboa.core.middleware.LocalConfigMiddleware', - 'modoboa.lib.middleware.AjaxLoginRedirect', - 'modoboa.lib.middleware.CommonExceptionCatcher', - 'modoboa.lib.middleware.RequestCatcherMiddleware', -) -``` - -And add the following new setting: - -``` python -# 2FA - -OTP_TOTP_ISSUER = "" -``` - -### Version 1.16.0 - -A new `policy daemon `{.interpreted-text role="ref"} has -been added. - -Make sure to have a Redis instance running on your server. - -Add `'modoboa.policyd'` to `MODOBOA_APPS`: - -``` python -MODOBOA_APPS = ( - 'modoboa', - 'modoboa.core', - 'modoboa.lib', - 'modoboa.admin', - 'modoboa.transport', - 'modoboa.relaydomains', - 'modoboa.limits', - 'modoboa.parameters', - 'modoboa.dnstools', - 'modoboa.policyd' -) -``` - -Add the following settings to your `settings.py` file: - -``` python -REDIS_HOST = '' -REDIS_PORT = 6379 -REDIS_QUOTA_DB = 0 -REDIS_URL = 'redis://{}:{}/{}'.format(REDIS_HOST, REDIS_PORT, REDIS_QUOTA_DB) -``` - -Or, if Redis listen on unix socket and you are not using the Modoboa -installer: - -``` python -REDIS_HOST = '/path/to/redis/socket' -REDIS_PORT = 6379 -REDIS_QUOTA_DB = 0 -REDIS_URL = 'unix://{}?db={}'.format(REDIS_HOST, REDIS_QUOTA_DB) -``` - -Once done, you can start the policy daemon using the following commands: - -``` bash -> python manage.py policy_daemon -``` - -Don\'t forget to configure `_policyd_config `{.interpreted-text -role="ref"} if you want to use this feature. - -The `modoboa-stats` plugin has been merged into the core. - -Add `'modoboa.maillog'` to `MODOBOA_APPS`: - -``` python -MODOBOA_APPS = ( - 'modoboa', - 'modoboa.core', - 'modoboa.lib', - 'modoboa.admin', - 'modoboa.transport', - 'modoboa.relaydomains', - 'modoboa.limits', - 'modoboa.parameters', - 'modoboa.dnstools', - 'modoboa.policyd', - 'modoboa.maillog', -) -``` - -And remove any reference to `modoboa_stats` in this same variable. - -### Version 1.15.0 - -This version drops Python 2 support so don\'t forget to update all the -extensions you use. - -:::: warning -::: title -Warning -::: - -If you upgrade an existing python 2 installation, you will need to -create a new Python 3 virtual environment. You can remove the existing -virtual environment and replace it by the new one so you won\'t have to -modify your configuration files. -:::: - -Add the following new setting: - -``` python -DISABLE_DASHBOARD_EXTERNAL_QUERIES = False -``` - -Reload uwsgi/gunicorn/apache depending on your setup. - -Finally, Make sure to use root privileges and run the following command: - -``` bash -> python manage.py generate_postfix_maps --destdir -``` - -Then, reload postfix. - -### Version 1.14.0 - -This release introduces an optional LDAP synchronization process. If you -want to use it, please follow the -`dedicated procedure `{.interpreted-text role="ref"}. - -### Version 1.13.1 - -Upgrade postfix maps files as follows: - -``` bash -> python manage.py generate_postfix_maps --destdir --force-overwrite -``` - -### Version 1.13.0 - -Add `'modoboa.dnstools'` to `MODOBOA_APPS`: - -``` python -MODOBOA_APPS = ( - 'modoboa', - 'modoboa.core', - 'modoboa.lib', - 'modoboa.admin', - 'modoboa.transport', - 'modoboa.relaydomains', - 'modoboa.limits', - 'modoboa.parameters', - 'modoboa.dnstools', -) -``` - -Add the following new settings: - -``` python -CSRF_COOKIE_SECURE = True -SESSION_COOKIE_SECURE = True -``` - -### modoboa-postfix-autoreply 1.5.0 - -Edit the `/etc/postfix/main.cf`{.interpreted-text role="file"} file and -remove the `sql-autoreplies-transport.cf` map from the `transport_maps` -if present. Remove the corresponding `proxy_read_maps` entry if -relevant. - -Reload postfix. - -### Version 1.10.0 - -:::: warning -::: title -Warning -::: - -Upgrade installed extensions BEFORE running `check` or `migrate` -commands. -:::: - -Upgrade all your installed plugins to the following versions: - -:::: warning -::: title -Warning -::: - -If you use the amavis plugin, make sure to include its configuration as -follows into `settings.py`{.interpreted-text role="file"}: - -``` python -from modoboa_amavis import settings as modoboa_amavis_settings -modoboa_amavis_settings.apply(globals()) -``` -:::: - - ------------------------------------------------------- - Name Version - --------------------------- --------------------------- - modoboa-amavis 1.2.0 - - modoboa-contacts 0.5.0 - - modoboa-dmarc 1.1.0 - - modoboa-imap-migration 1.2.0 - - modoboa-pdfcredentials 1.3.0 - - modoboa-postfix-autoreply 1.4.0 - - modoboa-radicale 1.2.0 - - modoboa-sievefilters 1.4.0 - - modoboa-stats 1.4.0 - - modoboa-webmail 1.4.0 - ------------------------------------------------------- - -Edit the `settings.py`{.interpreted-text role="file"} file and apply the -following modifications. - -Add `'modoboa.transport'` to `MODOBOA_APPS`: - -``` python -MODOBOA_APPS = ( - 'modoboa', - 'modoboa.core', - 'modoboa.lib', - 'modoboa.admin', - 'modoboa.transport', - 'modoboa.relaydomains', - 'modoboa.limits', - 'modoboa.parameters', -) -``` - -Replace the following line: - -``` python -MIDDLEWARE_CLASSES = ( -``` - -by: - -``` python -MIDDLEWARE = ( -``` - -Update postfix map files as follows: - -``` bash -> rm -f /modoboa-postfix-maps.chk -> python manage.py generate_postfix_maps --force --destdir -``` - -Then, modify postfix\'s configuration as follows: - - smtpd_sender_login_maps = - :/sql-sender-login-map.cf - - transport_maps = - :/sql-transport.cf - :/sql-spliteddomains-transport.cf - # other map files... - -Replace `` and `` by your values. - -If `transport_maps` contains `sql-relaydomains-transport.cf`, remove it. - -:::: warning -::: title -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: - - # Generate DKIM keys (they will belong to the user running this job) - * * * * * root $PYTHON $INSTANCE/manage.py modo manage_dkim_keys - -### Version 1.9.0 - -If you want to manage inactive accounts, look at -`inactive_accounts`{.interpreted-text role="ref"}. - -### Version 1.8.3 - -Edit the `settings.py`{.interpreted-text role="file"} file and replace -the following line: - -``` python -BASE_DIR = os.path.dirname(os.path.dirname(__file__)) -``` - -by: - -``` python -BASE_DIR = os.path.realpath(os.path.dirname(os.path.dirname(__file__))) -``` - -### Version 1.8.0 - -Modoboa now relies on [Django\'s builtin password validation -system](https://docs.djangoproject.com/en/1.10/topics/auth/passwords/#module-django.contrib.auth.password_validation) -to validate user passwords, instead of `django-passwords`. - -Remove `django-passwords` from your system: - -``` bash -> sudo -u -i bash -> source /bin/activate -> pip uninstall django-passwords -``` - -Edit the `settings.py`{.interpreted-text role="file"} file and remove -the following content: - -``` python -# django-passwords - -PASSWORD_MIN_LENGTH = 8 - -PASSWORD_COMPLEXITY = { - "UPPER": 1, - "LOWER": 1, - "DIGITS": 1 -} -``` - -Add the following lines: - -``` python -# Password validation rules -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, - { - 'NAME': 'modoboa.core.password_validation.ComplexityValidator', - 'OPTIONS': { - 'upper': 1, - 'lower': 1, - 'digits': 1, - 'specials': 0 - } - }, -] -``` - -### Version 1.7.2 - -API documentation has evolved (because of the upgrade to Django Rest -Framework 3.6) and CKeditor is now embedded by default (thanks to the -`django-ckeditor` package). Some configuration changes are required. - -Edit your `settings.py`{.interpreted-text role="file"} file and apply -the following modifications: - -- Update the `INSTALLED_APPS` variable as follows: - -``` python -INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.sites', - 'django.contrib.staticfiles', - 'reversion', - 'ckeditor', - 'ckeditor_uploader', - 'rest_framework', - 'rest_framework.authtoken', -) -``` - -- Update the `REST_FRAMEWORK` variable as follows: - -``` python -REST_FRAMEWORK = { - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'rest_framework.authentication.TokenAuthentication', - 'rest_framework.authentication.SessionAuthentication', - ), -} -``` - -- Remove the `SWAGGER_SETTINGS` variable -- Add the following content - -``` python -# CKeditor - -CKEDITOR_UPLOAD_PATH = "uploads/" - -CKEDITOR_IMAGE_BACKEND = "pillow" - -CKEDITOR_RESTRICT_BY_USER = True - -CKEDITOR_BROWSE_SHOW_DIRS = True - -CKEDITOR_ALLOW_NONIMAGE_FILES = False - -CKEDITOR_CONFIGS = { - 'default': { - 'allowedContent': True, - 'toolbar': 'Modoboa', - 'width': None, - 'toolbar_Modoboa': [ - ['Bold', 'Italic', 'Underline'], - ['JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock'], - ['BidiLtr', 'BidiRtl', 'Language'], - ['NumberedList', 'BulletedList', '-', 'Outdent', 'Indent'], - ['Undo', 'Redo'], - ['Link', 'Unlink', 'Anchor', '-', 'Smiley'], - ['TextColor', 'BGColor', '-', 'Source'], - ['Font', 'FontSize'], - ['Image', ], - ['SpellChecker'] - ], - }, -} -``` - -Don\'t forget to run the following command: - -``` bash -> python manage.py collectstatic -``` - -### Version 1.7.1 - -If you used 1.7.0 for a fresh installation, please run the following -commands: - -``` bash -> sudo -u -i bash -> source /bin/activate -> cd -> python manage.py load_initial_data -``` - -### Version 1.7.0 - -This version requires Django \>= 1.10 so you need to make some -modifications. It also brings internal API changes which are not -backward compatible so installed extensions must be upgraded too. - -First of all, deactivate all installed extensions (edit the -`settings.py`{.interpreted-text role="file"} file and comment the -corresponding lines in `MODOBOA_APPS`). - -Edit the `urls.py`{.interpreted-text role="file"} file of your local -instance and replace its content by the following one: - -``` python -from django.conf.urls import include, url - -urlpatterns = [ - url(r'', include('modoboa.urls')), -] -``` - -Edit the `settings.py`{.interpreted-text role="file"} and apply the -following changes: - -- Add `'modoboa.parameters'` to `MODOBOA_APPS`: - -``` python -MODOBOA_APPS = ( - 'modoboa', - 'modoboa.core', - 'modoboa.lib', - 'modoboa.admin', - 'modoboa.relaydomains', - 'modoboa.limits', - 'modoboa.parameters', - # Modoboa extensions here. -) -``` - -- Add `'modoboa.core.middleware.LocalConfigMiddleware'` to - `MIDDLEWARE_CLASSES`: - -``` python -MIDDLEWARE_CLASSES = ( - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.locale.LocaleMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'modoboa.core.middleware.LocalConfigMiddleware', - 'modoboa.lib.middleware.AjaxLoginRedirect', - 'modoboa.lib.middleware.CommonExceptionCatcher', - 'modoboa.lib.middleware.RequestCatcherMiddleware', -) -``` - -- Modoboa used to provide a custom authentication backend - (`modoboa.lib.authbackends.SimpleBackend`) but it has been removed. - Replace it as follows: - -``` python -AUTHENTICATION_BACKENDS = ( - # Other backends before... - 'django.contrib.auth.backends.ModelBackend', -) -``` - -- Remove `TEMPLATE_CONTEXT_PROCESSORS` and replace it by: - -``` python -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.template.context_processors.i18n', - 'django.template.context_processors.media', - 'django.template.context_processors.static', - 'django.template.context_processors.tz', - 'django.contrib.messages.context_processors.messages', - 'modoboa.core.context_processors.top_notifications', - ], - 'debug': False, - }, - }, -] -``` - -Run the following commands (load virtualenv if you use one): - -``` bash -> sudo -u -i bash -> source /bin/activate -> cd -> python manage.py migrate -> python manage.py collectstatic -``` - -Finally, upgrade your extensions and reactivate them. - - ------------------------------------------------------- - Name Version - --------------------------- --------------------------- - modoboa-amavis 1.1.0 - - modoboa-dmarc 1.0.0 - - modoboa-imap-migration 1.1.0 - - modoboa-pdfcredentials 1.1.0 - - modoboa-postfix-autoreply 1.2.0 - - modoboa-radicale 1.1.0 - - modoboa-sievefilters 1.1.0 - - modoboa-stats 1.1.0 - - modoboa-webmail 1.1.0 - ------------------------------------------------------- - -Command line shortcuts: - -``` bash -$ pip install modoboa-amavis==1.1.0 -$ pip install modoboa-dmarc==1.0.0 -$ pip install modoboa-imap-migration==1.1.0 -$ pip install modoboa-pdfcredentials==1.1.0 -$ pip install modoboa-postfix-autoreply==1.2.0 -$ pip install modoboa-radicale==1.1.0 -$ pip install modoboa-sievefilters==1.1.0 -$ pip install modoboa-stats==1.1.0 -$ pip install modoboa-webmail==1.1.0 -``` - -And please make sure you use the latest version of the -`django-versionfield2` package: - -``` bash -$ pip install -U django-versionfield2 -``` - -#### Notes about quota changes and resellers - -Reseller users now have a quota option in Resources tab. This is the -quota that a reseller can share between all its domains. - -There are two quotas for a domain in the new version: - -1. Quota & -2. Default mailbox quota. - -\[1\]. Quota: quota shared between mailboxes This quota is shared -between all the mailboxes of this domain. This value cannot exceed -reseller\'s quota and hence cannot be 0(unlimited) if reseller has -finite quota. - -\[2\]. Default mailbox quota: default quota applied to mailboxes This -quota is the default quota applied to new mailboxes. This value cannot -exceed Quota\[1\] and hence cannot be 0(unlimited) if Quota\[1\] is -finite. - -### Version 1.6.1 - -First of all, update postfix map files as follows: - -``` bash -> python manage.py generate_postfix_maps --destdir --force-overwrite -``` - -Then, modify postfix\'s configuration as follows: - - smtpd_sender_login_maps = - :/sql-sender-login-mailboxes.cf - :/sql-sender-login-aliases.cf - :/sql-sender-login-mailboxes-extra.cf - -Replace `` and `` by your values. - -Finally, reload postfix. - -This release also deprecates some internal functions. As a result, -several extensions has been updated to maintain the compatibility. If -you enabled the notification service, you\'ll find the list of available -updates directly in your Modoboa console. - -For the others, here is the list: - - ------------------------------------------------------- - Name Version - --------------------------- --------------------------- - modoboa-amavis 1.0.10 - - modoboa-postfix-autoreply 1.1.7 - - modoboa-radicale 1.0.5 - - modoboa-stats 1.0.9 - ------------------------------------------------------- - -Command line shortcut: - -``` bash -$ pip install modoboa-amavis==1.0.10 -$ pip install modoboa-postfix-autoreply==1.1.7 -$ pip install modoboa-radicale==1.0.5 -$ pip install modoboa-stats==1.0.9 -``` - -### Version 1.6.0 - -:::: warning -::: title -Warning -::: - -You have to upgrade extensions due to [core.User]{.title-ref} model -attribute change ([user.group]{.title-ref} to [user.role]{.title-ref}). -Otherwise, you will have an internal error after upgrade. In particular: -[modoboa-amavisd](https://github.com/modoboa/modoboa-amavis/commit/35df4e48b124e56df930cda8c013af0c1fcaabf3), -[modoboa-stats](https://github.com/modoboa/modoboa-stats/commit/aa4a39ce65eb306ad6dec30a54eb58945b120274), -[modoboa-postfix-autoreply](https://github.com/modoboa/modoboa-postfix-autoreply/commit/20f98c8d1c0c0dbd420f47aefcbb0290022414a4) -are concerned. -:::: - -An interesting feature brougth by this version is the capability to make -different checks about MX records. For example, Modoboa can query main -[DNSBL](https://en.wikipedia.org/wiki/DNSBL) providers for every defined -domain. With this, you will quickly know if one the domains you manage -is listed or not. To activate it, add the following line to your -crontab: - - */30 * * * * python /manage.py modo check_mx - -The communication with Modoboa public API has been reworked. Instead of -sending direct synchronous queries (for example to check new versions), -a cron job has been added. To activate it, add the following line to -your crontab: - - 0 * * * * python /manage.py communicate_with_public_api - -Please also note that public API now uses TLS so you must update your -configuration as follows: - - MODOBOA_API_URL = 'https://api.modoboa.org/1/' - -Finally, it is now possible to declare additional sender addresses on a -per-account basis. You need to update your postfix configuration in -order to use this functionality. Just edit the -`main.cf`{.interpreted-text role="file"} file and change the following -parameter: - - smtpd_sender_login_maps = - :/etc/postfix/sql-sender-login-mailboxes.cf - :/etc/postfix/sql-sender-login-aliases.cf - :/etc/postfix/sql-sender-login-mailboxes-extra.cf - -### Version 1.5.0 - -The API has been greatly improved and a documentation is now available. -To enable it, add `'rest_framework_swagger'` to the `INSTALLED_APPS` -variable in `settings.py`{.interpreted-text role="file"} as follows: - - INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.sites', - 'django.contrib.staticfiles', - 'reversion', - 'rest_framework.authtoken', - 'rest_framework_swagger', - ) - -Then, add the following content into `settings.py`{.interpreted-text -role="file"}, just after the `REST_FRAMEWORK` variable: - - SWAGGER_SETTINGS = { - "is_authenticated": False, - "api_version": "1.0", - "exclude_namespaces": [], - "info": { - "contact": "contact@modoboa.com", - "description": ("Modoboa API, requires a valid token."), - "title": "Modoboa API", - } - } - -You\'re done. The documentation is now available at the following -address: - -> \/docs/api/ - -Finally, if you find a `TEMPLATE_CONTEXT_PROCESSORS` variable in your -`settings.py`{.interpreted-text role="file"} file, make sure it looks -like this: - - TEMPLATE_CONTEXT_PROCESSORS = global_settings.TEMPLATE_CONTEXT_PROCESSORS + [ - 'modoboa.core.context_processors.top_notifications', - ] - -### Version 1.4.0 - -:::: warning -::: title -Warning -::: - -Please make sure to use Modoboa 1.3.5 with an up-to-date database before -an upgrade to 1.4.0. -:::: - -:::: warning -::: title -Warning -::: - -Do not follow the regular upgrade procedure for this version. -:::: - -Some extension have been moved back into the main repository. The main -reason for that is that using Modoboa without them doesn\'t make sense. - -First of all, you must rename the following applications listed inside -the `MODOBOA_APPS` variable: - - --------------------------------------------------- - Old name New name - ---------------------------- ---------------------- - modoboa_admin modoboa.admin - - modoboa_admin_limits modoboa.limits - - modoboa_admin_relaydomains modoboa.relaydomains - --------------------------------------------------- - -Then, apply the following steps: - -1. Uninstall old extensions: - - $ pip uninstall modoboa-admin modoboa-admin-limits modoboa-admin-relaydomains - -2. Install all extension updates using pip (check the *Modoboa \> - Information* page) - -3. Manually migrate database: - - $ cd - $ python manage.py migrate auth - $ python manage.py migrate admin 0001 --fake - $ python manage.py migrate admin - $ python manage.py migrate limits 0001 --fake - $ python manage.py migrate relaydomains 0001 --fake - $ python manage.py migrate - -4. Finally, update static files: - - $ python manage.py collectstatic - -This version also introduces a REST API. To enable it: - -1. Add `'rest_framework.authtoken'` to the `INSTALLED_APPS` variable - -2. Add the following configuration inside `settings.py`: - - # Rest framework settings - - REST_FRAMEWORK = { - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'rest_framework.authentication.TokenAuthentication', - ), - 'DEFAULT_PERMISSION_CLASSES': ( - 'rest_framework.permissions.IsAuthenticated', - ) - } - -3. Run the following command: - - $ python manage.py migrate - -### Version 1.3.5 - -To enhance security, Modoboa now checks the [strength of user passwords -\\_]{.title-ref}. - -To use this feature, add the following configuration into the -`settings.py` file: - - # django-passwords - - PASSWORD_MIN_LENGTH = 8 - - PASSWORD_COMPLEXITY = { - "UPPER": 1, - "LOWER": 1, - "DIGITS": 1 - } - -### Version 1.3.2 - -Modoboa now uses the *atomic requests* mode to preserve database -consistency -([reference](https://docs.djangoproject.com/en/1.7/topics/db/transactions/#tying-transactions-to-http-requests)). - -To enable it, update the `DATABASES` variable in `settings.py` as -follows: - - DATABASES = { - "default": { - # stuff before... - "ATOMIC_REQUESTS": True - }, - "amavis": { - # stuff before... - "ATOMIC_REQUESTS": True - } - } - -### Version 1.3.0 - -This release does not bring awesome new features but it is a necessary -bridge to the future of Modoboa. All extensions now have their own git -repository and the deploy process has been updated to reflect this -change. - -Another important update is the use of Django 1.7. Besides its new -features, the migration system has been reworked and is now more robust -than before. - -Before we begin with the procedure, here is a table showing old -extension names and their new name: - - ---------------------------------------------------------------------------------------------------- - Old name New package name New module name - ------------------------------------------ ---------------------------- ---------------------------- - modoboa.extensions.admin modoboa-admin modoboa_admin - - modoboa.extensions.limits modoboa-admin-limits modoboa_admin_limits - - modoboa.extensions.postfix_autoreply modoboa-postfix-autoreply modoboa_postfix_autoreply - - modoboa.extensions.postfix_relay_domains modoboa-admin-relaydomains modoboa_admin_relaydomains - - modoboa.extensions.radicale modoboa-radicale modoboa_radicale - - modoboa.extensions.sievefilters modoboa-sievefilters modoboa_sievefilters - - modoboa.extensions.stats modoboa-stats modoboa_stats - - modoboa.extensions.webmail modoboa-webmail modoboa_webmail - ---------------------------------------------------------------------------------------------------- - -Here are the required steps: - -1. Install the extensions using pip (look at the second column in the - table above): - - $ pip install - -2. Remove `south` from `INSTALLED_APPS` - -3. Rename old extension names inside `MODOBOA_APPS` (look at the third - column in the table above) - -4. Remove `modoboa.lib.middleware.ExtControlMiddleware` from - `MIDDLEWARE_CLASSES` - -5. Change `DATABASE_ROUTERS` to: - - DATABASE_ROUTERS = ["modoboa_amavis.dbrouter.AmavisRouter"] - -6. Run the following commands: - - $ cd - $ python manage.py migrate - -7. Reply `yes` to the question - -8. Run the following commands: - - $ python manage.py load_initial_data - $ python manage.py collectstatic - -9. The cleanup job has been renamed in Django, so you have to modify - your crontab entry: - - - 0 0 * * * /manage.py cleanup - + 0 0 * * * /manage.py clearsessions - -### Version 1.2.0 - -A new notification service let administrators know about new Modoboa -versions. To activate it, you need to update the -`TEMPLATE_CONTEXT_PROCESSORS` variable like this: - - from django.conf import global_settings - - TEMPLATE_CONTEXT_PROCESSORS = global_settings.TEMPLATE_CONTEXT_PROCESSORS + ( - 'modoboa.core.context_processors.top_notifications', - ) - -and to define the new `MODOBOA_API_URL` variable: - - MODOBOA_API_URL = 'http://api.modoboa.org/1/' - -The location of external static files has changed. To use them, add a -new path to the `STATICFILES_DIRS`: - - # Additional locations of static files - STATICFILES_DIRS = ( - # Put strings here, like "/home/html/static" or "C:/www/django/static". - # Always use forward slashes, even on Windows. - # Don't forget to use absolute paths, not relative paths. - "/bower_components", - ) - -Run the following commands to define the hostname of your instance: - - $ cd - $ python manage.py set_default_site - -If you plan to use the Radicale extension: - -1. Add `'modoboa.extensions.radicale'` to the `MODOBOA_APPS` variable - -2. Run the following commands: - - $ cd - $ python manage.py syncdb - -:::: warning -::: title -Warning -::: - -You also have to note that the `sitestatic`{.interpreted-text -role="file"} directory has moved from `` to -`` (it\'s probably the parent directory). You have -to adapt your web server configuration to reflect this change. -:::: \ No newline at end of file From 2899691e5d9c64d6142b171fa958bdb3481c4db2 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Thu, 15 Jan 2026 17:08:05 +0100 Subject: [PATCH 7/8] Updated RTD config --- .readthedocs.yml | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 487c3c902..5f4e4e763 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,10 +1,17 @@ version: 2 -sphinx: - configuration: doc/conf.py + build: - os: "ubuntu-22.04" + os: ubuntu-lts-latest tools: - python: "3.10" -python: - install: - - requirements: doc/requirements.txt + nodejs: "latest" + jobs: + install: + - npm install vitepress + build: + html: + # The site was created by running `vitepress init` + # and following the official guide + # https://vitepress.dev/guide/getting-started + - ./node_modules/.bin/vitepress build doc-ng + - mkdir -p $READTHEDOCS_OUTPUT/ + - mv doc-ng/.vitepress/dist $READTHEDOCS_OUTPUT/html From dc6baecb80eab253b2a6323d0b88ce042011b66f Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Thu, 15 Jan 2026 17:21:20 +0100 Subject: [PATCH 8/8] Fixed incorrect dist path --- .github/workflows/documentation.yml | 1 - .github/workflows/modoboa.yml | 8 ++++++++ .readthedocs.yml | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 784dcb476..6a3fad742 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -31,4 +31,3 @@ jobs: - name: Build documentation run: cd doc-ng && pnpm run build - \ No newline at end of file diff --git a/.github/workflows/modoboa.yml b/.github/workflows/modoboa.yml index 0229ee8fc..83eeda45c 100644 --- a/.github/workflows/modoboa.yml +++ b/.github/workflows/modoboa.yml @@ -11,6 +11,10 @@ on: - 'frontend/**' - 'doc/**' - 'doc-ng/**' + - '.editorconfig' + - '.readthedocs.yml' + - '.pre-commit-config.yaml' + - '.gitignore' pull_request: branches: [ master ] paths-ignore: @@ -18,6 +22,10 @@ on: - 'frontend/**' - 'doc/**' - 'doc-ng/**' + - '.editorconfig' + - '.readthedocs.yml' + - '.pre-commit-config.yaml' + - '.gitignore' release: branches: [ master ] types: [ published ] diff --git a/.readthedocs.yml b/.readthedocs.yml index 5f4e4e763..bab00cccd 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -14,4 +14,4 @@ build: # https://vitepress.dev/guide/getting-started - ./node_modules/.bin/vitepress build doc-ng - mkdir -p $READTHEDOCS_OUTPUT/ - - mv doc-ng/.vitepress/dist $READTHEDOCS_OUTPUT/html + - mv doc-ng/dist $READTHEDOCS_OUTPUT/html