diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 000000000..6a3fad742 --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,33 @@ +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: cd doc-ng && pnpm install + + - name: Build documentation + run: cd doc-ng && pnpm run build diff --git a/.github/workflows/modoboa.yml b/.github/workflows/modoboa.yml index fd102787f..6eec728ec 100644 --- a/.github/workflows/modoboa.yml +++ b/.github/workflows/modoboa.yml @@ -10,12 +10,22 @@ on: - 'modoboa/**.po' - 'frontend/**' - 'doc/**' + - 'doc-ng/**' + - '.editorconfig' + - '.readthedocs.yml' + - '.pre-commit-config.yaml' + - '.gitignore' pull_request: branches: [ master ] paths-ignore: - 'modoboa/**.po' - 'frontend/**' - 'doc/**' + - 'doc-ng/**' + - '.editorconfig' + - '.readthedocs.yml' + - '.pre-commit-config.yaml' + - '.gitignore' release: branches: [ master ] types: [ published ] diff --git a/.gitignore b/.gitignore index 4488700e0..3b3c04c36 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,8 @@ node_modules /frontend/webpack-stats.json /frontend/src/translations.json /frontend/.__mf__temp/ +doc-ng/.vitepress/cache/ +doc-ng/.vitepress/dist/ # TOX stuff test_project/modoboa.db diff --git a/.readthedocs.yml b/.readthedocs.yml index 487c3c902..bab00cccd 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/dist $READTHEDOCS_OUTPUT/html diff --git a/doc-ng/.vitepress/config.mjs b/doc-ng/.vitepress/config.mjs new file mode 100644 index 000000000..55bd278ec --- /dev/null +++ b/doc-ng/.vitepress/config.mjs @@ -0,0 +1,168 @@ +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', + 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 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' }, + ], + }, + ], + + 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 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: { + 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, + 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/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..e0ffb1cf4 --- /dev/null +++ b/doc-ng/contributing/getting_start.md @@ -0,0 +1,150 @@ +--- +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? +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..52d1256dc --- /dev/null +++ b/doc-ng/contributing/plugin_api.md @@ -0,0 +1,143 @@ +--- +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 + +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, []) +``` diff --git a/doc-ng/contributing/translation.md b/doc-ng/contributing/translation.md new file mode 100644 index 000000000..42e6d68ce --- /dev/null +++ b/doc-ng/contributing/translation.md @@ -0,0 +1,40 @@ +--- +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/) + +## 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 +transifex to pick up your changes. + +To do so, after finishing editing, go to the frontend directory and run +the following command: + +```shell +$ yarn gettext:extract +``` diff --git a/doc-ng/contributors.md b/doc-ng/contributors.md new file mode 100644 index 000000000..8ec727c83 --- /dev/null +++ b/doc-ng/contributors.md @@ -0,0 +1,17 @@ +--- +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/) +- [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..671ede471 --- /dev/null +++ b/doc-ng/index.md @@ -0,0 +1,43 @@ +--- +# 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" + 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..2876ba84d --- /dev/null +++ b/doc-ng/installation/amavis.md @@ -0,0 +1,281 @@ +--- +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 + +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..684e7765a --- /dev/null +++ b/doc-ng/installation/dovecot.md @@ -0,0 +1,665 @@ +--- +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 + +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..32f0de028 --- /dev/null +++ b/doc-ng/installation/installer.md @@ -0,0 +1,62 @@ +--- +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 + +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..3a531fde0 --- /dev/null +++ b/doc-ng/installation/manual.md @@ -0,0 +1,22 @@ +--- +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, + +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..eeaac2919 --- /dev/null +++ b/doc-ng/installation/modoboa.md @@ -0,0 +1,326 @@ +--- +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). + +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..7c1218cd0 --- /dev/null +++ b/doc-ng/installation/opendkim.md @@ -0,0 +1,88 @@ +--- +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 +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..f8523af92 --- /dev/null +++ b/doc-ng/installation/postfix.md @@ -0,0 +1,112 @@ +--- +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 +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..be864bf2a --- /dev/null +++ b/doc-ng/installation/radicale.md @@ -0,0 +1,151 @@ +--- +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 +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..6a1c3e4ed --- /dev/null +++ b/doc-ng/installation/requirements.md @@ -0,0 +1,35 @@ +--- +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: + +- CPU: 4 +- RAM: 4GB +- Disk: 10GB +- Python 3.10+ +- Postgres 14+ or MariaDB 10.5+ or MySQL 8.0.11+ or sqlite3 + +::: 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) diff --git a/doc-ng/installation/upgrade.md b/doc-ng/installation/upgrade.md new file mode 100644 index 000000000..00cfd9e24 --- /dev/null +++ b/doc-ng/installation/upgrade.md @@ -0,0 +1,2122 @@ +--- +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 + +::: 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..5e9fdab5e --- /dev/null +++ b/doc-ng/installation/webserver.md @@ -0,0 +1,356 @@ +--- +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 +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..da59f7d9f --- /dev/null +++ b/doc-ng/moving.md @@ -0,0 +1,179 @@ +--- +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 +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 000000000..484026c59 Binary files /dev/null and b/doc-ng/public/admin_dashboard.png differ diff --git a/doc-ng/public/api_access_form.png b/doc-ng/public/api_access_form.png new file mode 100644 index 000000000..9fe51d206 Binary files /dev/null and b/doc-ng/public/api_access_form.png differ diff --git a/doc-ng/public/modoboa_logo.png b/doc-ng/public/modoboa_logo.png new file mode 100644 index 000000000..f15d39c79 Binary files /dev/null and b/doc-ng/public/modoboa_logo.png differ diff --git a/doc-ng/public/user_webmail.png b/doc-ng/public/user_webmail.png new file mode 100644 index 000000000..f670759f2 Binary files /dev/null and b/doc-ng/public/user_webmail.png differ diff --git a/doc-ng/rebuild_venv.md b/doc-ng/rebuild_venv.md new file mode 100644 index 000000000..71b10397d --- /dev/null +++ b/doc-ng/rebuild_venv.md @@ -0,0 +1,79 @@ +--- +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 +Ubuntu 17.10) your virtual environment running modoboa can get corrupted. + +Your first response will be to panic but fear not! +The solution is in this document. + +First things first: + +## Recover your database password + +You will need to recover your database password (if using mysql or +postgresql). + +You will find this in `/etc/postfix/sql-aliases.cf` or any +file with `sql-*.cf` in the `/etc/postfix` directory. + +Make note of this as you will need it when reconfiguring modoboa. + +## Reinstall Modoboa + +Start out by backup up your modoboa settings file located in the `modoboa instance` directory +(`/srv/modoboa/instance/instance/settings.py` if you used the default installer configuration). +This contains your current configuration. + +Next, you want to remove all current modoboa files. + +After doing this, follow the manual installation instructions for `modoboa_manual_install` **only** as everything should be working properly. + +After this completes, simply restore your backed up settings file to `/srv/instance/instance/settings.py` +(if you used installer default configuration). +You will then need to reinstall your [extensions](http://modoboa.readthedocs.io/en/latest/index.html). + +You can find which plugins you had in your `settings.py` file under the `MODOBOA_APPS` variable. + +Instructions to install extensions can also be [found here](http://modoboa.readthedocs.io/en/latest/installation.html#extensions). + +Once you have completed this step, you will need to run the following commands: + +``` bash +$ (env) $ cd +$ (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..9314ccbdc --- /dev/null +++ b/doc-ng/rest_api.md @@ -0,0 +1,44 @@ +--- +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. + +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/* + +::: warning +You need to be logged in for the API docs to show. +::: + +**Examples:** + +* [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 +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