4 Commits

Author SHA1 Message Date
Spitap
af9d8c2849 Apply fixes by @tomas-kucera
Co-Authored-By: Katzman <tku@centrum.cz>
2022-12-27 20:03:16 +01:00
Spitap
6b4302b566 Update from master
commit 5c22600d98
Merge: bc12ca7 bcdbb4a
Author: Antoine Nguyen <tonio@ngyn.org>
Date:   Tue Nov 29 16:54:28 2022 +0100

    Merge pull request #462 from Spitfireap/randomize-api-call-time

    randomize api call time

commit bcdbb4a2ce
Author: Spitap <dev@asdrip.fr>
Date:   Tue Nov 29 14:53:05 2022 +0100

    fix typo

commit bd1ddcef21
Author: Spitap <dev@asdrip.fr>
Date:   Tue Nov 29 13:45:31 2022 +0100

    randomize api call time

commit bc12ca7327
Merge: d364239 bd0ecd0
Author: Antoine Nguyen <tonio@ngyn.org>
Date:   Mon Nov 14 15:49:41 2022 +0100

    Merge pull request #458 from Spitfireap/fix-include_try

    fix typo in dovecot configuration file

commit bd0ecd0949
Author: Spitap <dev@asdrip.fr>
Date:   Thu Nov 10 14:57:43 2022 +0100

    fix typo in dovecot configuration file

commit d364239348
Merge: 61838db 3763300
Author: Antoine Nguyen <tonio@ngyn.org>
Date:   Wed Nov 9 10:51:30 2022 +0100

    Merge pull request #456 from modoboa/feature/improved_backup_restore

    WIP: Improved backup/restore system.

commit 37633008cb
Author: Antoine Nguyen <tonio@ngyn.org>
Date:   Wed Nov 9 10:30:44 2022 +0100

    Fixed restore mode

commit d6f9a5b913
Author: Antoine Nguyen <tonio@ngyn.org>
Date:   Tue Nov 8 17:20:25 2022 +0100

    Few fixes.

commit 8b1d60ee59
Author: Antoine Nguyen <tonio@ngyn.org>
Date:   Tue Nov 8 17:19:23 2022 +0100

    Few fixes

commit 2b5edae5d5
Author: Antoine Nguyen <tonio@ngyn.org>
Date:   Sun Nov 6 10:30:24 2022 +0100

    WIP: Improved backup/restore system.

commit 61838dbe4d
Author: Antoine Nguyen <tonio@ngyn.org>
Date:   Sat Nov 5 09:30:50 2022 +0100

    Check if restore is defined before doing anything else.

    fix #453

commit 962cac3ad9
Merge: 1b192c5 ef2359a
Author: Antoine Nguyen <tonio@ngyn.org>
Date:   Fri Nov 4 09:41:20 2022 +0100

    Merge pull request #450 from Spitfireap/fixed-super-call

    fixed super call in modoboa's script

commit ef2359a2a8
Author: Spitap <dev@asdrip.fr>
Date:   Thu Nov 3 23:10:21 2022 +0100

    fixed super call

commit 1b192c5fd5
Merge: 754d652 b0b0146
Author: Antoine Nguyen <tonio@ngyn.org>
Date:   Thu Nov 3 15:34:48 2022 +0100

    Merge pull request #449 from Spitfireap/fixed-import-typo

    fixed constants import

commit b0b01465d9
Author: Spitap <dev@asdrip.fr>
Date:   Thu Nov 3 15:00:07 2022 +0100

    fixed constants import

commit 754d652fc2
Author: Antoine Nguyen <tonio@ngyn.org>
Date:   Thu Nov 3 12:27:04 2022 +0100

    Few fixes

commit cb5fa75693
Merge: 1afb8e6 e01265a
Author: Antoine Nguyen <tonio@ngyn.org>
Date:   Thu Nov 3 12:20:25 2022 +0100

    Merge pull request #444 from Spitfireap/tighter-config-file-perm

    tighter config file permission

commit 1afb8e61fc
Merge: 15c1779 8dd0b7d
Author: Antoine Nguyen <tonio@ngyn.org>
Date:   Thu Nov 3 12:17:16 2022 +0100

    Merge pull request #424 from Spitfireap/restore

    Backup & restore system

commit 8dd0b7d497
Author: Spitap <dev@asdrip.fr>
Date:   Thu Nov 3 10:57:03 2022 +0100

    Last camelCase

commit 554611b366
Author: Spitap <dev@asdrip.fr>
Date:   Thu Nov 3 10:54:06 2022 +0100

    review fix

commit 15c17796f2
Merge: ce8e7e6 84d1363
Author: Antoine Nguyen <tonio@ngyn.org>
Date:   Fri Oct 28 09:43:30 2022 +0200

    Merge pull request #446 from Spitfireap/fix-ssl-min-protocol

    fixed ssl_min_protocol setting

commit 84d13633a1
Author: Spitap <dev@asdrip.fr>
Date:   Thu Oct 27 22:37:47 2022 +0200

    fixed ssl_min_protocol setting

commit ce8e7e6027
Merge: 8e8ae5f fe7df27
Author: Antoine Nguyen <tonio@ngyn.org>
Date:   Thu Oct 27 17:56:37 2022 +0200

    Merge pull request #445 from Spitfireap/dovecot-fixes

    Fixes ssl permission error, updated ssl_protocol parameter

commit e01265a4ee
Merge: a5fba03 235ef3b
Author: Spitap <dev@asdrip.fr>
Date:   Thu Oct 27 17:44:37 2022 +0200

    Merge branch 'tighter-config-file-perm' of https://github.com/Spitfireap/modoboa-installer into tighter-config-file-perm

commit a5fba03264
Author: Spitap <dev@asdrip.fr>
Date:   Thu Oct 27 11:13:47 2022 +0200

    tighter config file permission

commit fe7df276fc
Author: Spitap <dev@asdrip.fr>
Date:   Thu Oct 27 17:25:39 2022 +0200

    Check dovecot version greater

commit 8f34f0af6f
Author: Spitap <dev@asdrip.fr>
Date:   Thu Oct 27 17:00:58 2022 +0200

    Fixes ssl permission error, updated ssl_protocol parameter

commit 8e8ae5fb9c
Merge: 67f6cee fefbf54
Author: Antoine Nguyen <tonio@ngyn.org>
Date:   Thu Oct 27 16:49:20 2022 +0200

    Merge pull request #439 from stefaweb/master

    Update config_dict_template.py for default max_servers value

commit 235ef3befb
Author: Spitap <dev@asdrip.fr>
Date:   Thu Oct 27 11:13:47 2022 +0200

    thighter config file permission

commit 67f6cee8ea
Merge: b84abbb 53f7f8e
Author: Antoine Nguyen <tonio@ngyn.org>
Date:   Tue Oct 25 19:32:37 2022 +0200

    Merge pull request #442 from Spitfireap/patch-1

    Set $max_server to 2 to avoid amavis crash

commit 5c9d5c9a03
Author: Spitap <dev@asdrip.fr>
Date:   Tue Oct 25 16:58:57 2022 +0200

    DKIM keys restore, Radicale backup/restore, fixes

commit 4c1f8710b5
Author: Spitap <dev@asdrip.fr>
Date:   Tue Oct 25 16:04:55 2022 +0200

    Added dkim key backup

commit e34eb4b337
Author: Spitap <dev@asdrip.fr>
Date:   Tue Oct 25 13:59:28 2022 +0200

    fix database path

commit 53f7f8ef9d
Author: Spitfireap <45575529+Spitfireap@users.noreply.github.com>
Date:   Wed Oct 19 08:19:40 2022 +0000

    Update config_dict_template.py

commit 35778cd614
Merge: 6726f5b b84abbb
Author: Spitfireap <45575529+Spitfireap@users.noreply.github.com>
Date:   Tue Oct 18 17:17:48 2022 +0200

    Merge branch 'modoboa:master' into restore

commit fefbf549a4
Author: Stephane Leclerc <sleclerc@actionweb.fr>
Date:   Thu Oct 6 13:36:13 2022 +0200

    Update config_dict_template.py for default max_server value

commit 6726f5b1a2
Author: Spitap <dev@asdrip.fr>
Date:   Mon Sep 26 13:39:28 2022 +0200

    Improved path generation, path mistake proofing

commit a192cbcbd0
Author: Spitap <dev@asdrip.fr>
Date:   Mon Sep 19 16:40:25 2022 +0200

    Updated doc, default path on conf file

commit 5bed9655ea
Author: Spitap <dev@asdrip.fr>
Date:   Mon Sep 19 15:53:19 2022 +0200

    fixed typo

commit 6b096a7470
Author: Spitap <dev@asdrip.fr>
Date:   Mon Sep 19 15:50:03 2022 +0200

    Simplified db dumps restore

commit e30add03fd
Merge: d75d83f 1f8dd1b
Author: Spitap <dev@asdrip.fr>
Date:   Mon Sep 19 15:39:05 2022 +0200

    Update from master

commit d75d83f202
Author: Spitap <dev@asdrip.fr>
Date:   Mon Sep 19 15:13:44 2022 +0200

    more refactoring

commit f3811b4b39
Author: Spitap <dev@asdrip.fr>
Date:   Mon Sep 19 14:59:43 2022 +0200

    refactoring

commit b0d56b3989
Author: Spitap <dev@asdrip.fr>
Date:   Thu Sep 15 11:32:57 2022 +0200

    PEP formating

commit 53e3e3ec58
Author: Spitap <dev@asdrip.fr>
Date:   Fri Aug 5 15:20:11 2022 +0200

    Better UX, use of os to concatenate path

commit e546d2cb23
Author: Spitap <dev@asdrip.fr>
Date:   Wed Jul 27 16:32:59 2022 +0200

    Better UX

commit 70faa1c5cb
Author: Spitap <dev@asdrip.fr>
Date:   Wed Jul 27 15:58:41 2022 +0200

    Fixed backupdir index

commit 563979a7dd
Author: Spitap <dev@asdrip.fr>
Date:   Wed Jul 27 15:51:22 2022 +0200

    fixed mail backup/restore

commit ee2ccf0647
Author: Spitap <dev@asdrip.fr>
Date:   Wed Jul 27 14:35:48 2022 +0200

    Fixed postfix install, added restore to readme

commit 2077c94b52
Author: Spitap <dev@asdrip.fr>
Date:   Tue Jul 26 17:05:00 2022 +0200

    Fix amavis config file not copied to right location

commit 4a7222bd24
Author: Spitap <dev@asdrip.fr>
Date:   Tue Jul 26 16:53:24 2022 +0200

    Fixed nginx call to uwsgi

commit e7b6104195
Author: Spitap <dev@asdrip.fr>
Date:   Tue Jul 26 16:39:41 2022 +0200

    fixed install within class

commit 4a00590354
Author: Spitap <dev@asdrip.fr>
Date:   Tue Jul 26 16:20:03 2022 +0200

    fixed restore disclamer

commit 15768c429e
Author: Spitap <dev@asdrip.fr>
Date:   Tue Jul 26 12:07:42 2022 +0200

    Restore workflow done

commit 439ffb94c4
Author: Spitap <dev@asdrip.fr>
Date:   Mon Jul 25 18:54:47 2022 +0200

    initial commit

commit 37bc21dfd3
Author: Spitap <dev@asdrip.fr>
Date:   Tue Jul 26 10:36:08 2022 +0200

    Backup postewhite.conf instead of custom whitelist

    Postwhite.conf contains a custom host list

commit 26204143af
Merge: 2097055 d495afd
Author: Spitap <dev@asdrip.fr>
Date:   Mon Jul 25 22:10:26 2022 +0200

    Merge branch 'master' into backup

commit 20970557de
Author: Spitap <dev@asdrip.fr>
Date:   Mon Jul 25 22:05:35 2022 +0200

    Allow to disable mail backup

commit 632c26596e
Author: Spitap <dev@asdrip.fr>
Date:   Mon Jul 25 21:52:15 2022 +0200

    Update backup readme

commit 9e1c18cd6b
Author: Spitap <dev@asdrip.fr>
Date:   Thu Jul 21 19:09:53 2022 +0200

    Fix argument passed as list instead of string

commit db6457c5f5
Author: Spitap <dev@asdrip.fr>
Date:   Thu Jul 21 19:07:18 2022 +0200

    better path handling

commit 579faccfa5
Author: Spitap <dev@asdrip.fr>
Date:   Thu Jul 21 19:00:32 2022 +0200

    added an automatic bash option (no path provided) or a path provided bash (for cron job)

commit 5318fa279b
Author: Spitap <dev@asdrip.fr>
Date:   Thu Jul 21 18:00:50 2022 +0200

    bash option

commit 74de6a9bb1
Author: Spitap <dev@asdrip.fr>
Date:   Thu Jul 21 17:31:56 2022 +0200

    Reset pgpass before trying to backup secondary dbs

commit 54185a7c5a
Author: Spitap <dev@asdrip.fr>
Date:   Thu Jul 21 17:26:40 2022 +0200

    Fix database backup logic issue

commit 1f9d69c37c
Author: Spitap <dev@asdrip.fr>
Date:   Thu Jul 21 17:21:59 2022 +0200

    Fix copy issue

commit 8d02d2a9fb
Author: Spitap <dev@asdrip.fr>
Date:   Thu Jul 21 17:09:23 2022 +0200

    added safe mkdir in utils, use utils.mkdir_safe() in backup

commit 6f604a5fec
Author: Spitap <dev@asdrip.fr>
Date:   Thu Jul 21 16:53:56 2022 +0200

    Fix loop logic

commit 568c4a65a0
Author: Spitap <dev@asdrip.fr>
Date:   Thu Jul 21 16:51:32 2022 +0200

    fix none-type passed to os.path

commit dc84a79528
Author: Spitap <dev@asdrip.fr>
Date:   Thu Jul 21 14:12:35 2022 +0200

    Note : capitalize affects only first letter

commit 304e25fa3c
Author: Spitap <dev@asdrip.fr>
Date:   Thu Jul 21 14:10:57 2022 +0200

    Fix getattr

commit 070efd61c4
Author: Spitap <dev@asdrip.fr>
Date:   Thu Jul 21 14:08:39 2022 +0200

    Fix import

commit 9917d8023e
Author: Spitap <dev@asdrip.fr>
Date:   Thu Jul 21 14:02:41 2022 +0200

    Edited README, fix backup run process

commit 27b9de6755
Author: Spitap <dev@asdrip.fr>
Date:   Thu Jul 21 13:48:44 2022 +0200

    database backup

commit 56ed214fb5
Author: Spitap <dev@asdrip.fr>
Date:   Tue Jul 19 19:06:53 2022 +0200

    Starting work on backup system
2022-12-22 18:47:55 +01:00
Spitap
d9ced63e99 centos9 fixes 2022-10-18 10:19:32 +02:00
Spitap
b13cdbf0ba Centos9 support and removed centos7 support 2022-10-14 20:10:39 +02:00
76 changed files with 475 additions and 2003 deletions

3
.github/FUNDING.yml vendored
View File

@@ -1,3 +0,0 @@
# These are supported funding model platforms
github: [modoboa]

View File

@@ -1,7 +1,7 @@
# Impacted versions # Impacted versions
* Distribution: Debian / Ubuntu / Centos * Distribution: Debian / Ubuntu / Centos
* Codename: Jessie / Trusty / Centos 7 / ... * Codename: Jessie / Trusty / Centos 9 Stream / ...
* Arch: 32 Bits / 64 Bits * Arch: 32 Bits / 64 Bits
* Database: PostgreSQL / MySQL * Database: PostgreSQL / MySQL

View File

@@ -11,48 +11,47 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: [3.9, '3.10', '3.11', '3.12'] python-version: [3.7, 3.8, 3.9]
fail-fast: false fail-fast: false
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5 uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install dependencies - name: Install dependencies
run: | run: |
pip install -r test-requirements.txt pip install -r test-requirements.txt
- name: Run tests - name: Run tests
if: ${{ matrix.python-version != '3.12' }} if: ${{ matrix.python-version != '3.9' }}
run: | run: |
python tests.py python tests.py
- name: Run tests and coverage - name: Run tests and coverage
if: ${{ matrix.python-version == '3.12' }} if: ${{ matrix.python-version == '3.9' }}
run: | run: |
coverage run tests.py coverage run tests.py
- name: Upload coverage result - name: Upload coverage result
if: ${{ matrix.python-version == '3.12' }} if: ${{ matrix.python-version == '3.9' }}
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v2
with: with:
name: coverage-results name: coverage-results
path: .coverage path: .coverage
include-hidden-files: true
coverage: coverage:
needs: test needs: test
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v2
with: with:
python-version: '3.12' python-version: '3.9'
- name: Install dependencies - name: Install dependencies
run: | run: |
pip install codecov pip install codecov
- name: Download coverage results - name: Download coverage results
uses: actions/download-artifact@v4 uses: actions/download-artifact@v2
with: with:
name: coverage-results name: coverage-results
- name: Report coverage - name: Report coverage

View File

@@ -1,32 +0,0 @@
name: Update version file
on:
workflow_run:
branches: [ master ]
workflows: [Modoboa installer]
types:
- completed
jobs:
update-version:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository.
ref: ${{ github.head_ref }}
- name: Overwrite file
uses: "DamianReeves/write-file-action@master"
with:
path: version.txt
write-mode: overwrite
contents: ${{ github.sha }}
- name: Commit & Push
uses: Andro999b/push@v1.3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ github.ref_name }}
force: true
message: '[GitHub Action] Updated version file'

5
.gitignore vendored
View File

@@ -58,8 +58,3 @@ target/
# PyCharm # PyCharm
.idea/ .idea/
#KDE
*.kdev4
installer.cfg

View File

@@ -1,5 +1,5 @@
**modoboa-installer** modoboa-installer
===================== =================
|workflow| |codecov| |workflow| |codecov|
@@ -9,12 +9,14 @@ An installer which deploy a complete mail server based on Modoboa.
This tool is still in beta stage, it has been tested on: This tool is still in beta stage, it has been tested on:
* Debian 12 and upper * Debian Buster (10) / Bullseye (11)
* Ubuntu Focal Fossa (20.04) and upper * Ubuntu Bionic Beaver (18.04) and upper
* CentOS 9 Stream
.. warning:: .. warning::
``/tmp`` partition must be mounted without the ``noexec`` option. ``/tmp`` partition must be mounted without the ``noexec`` option.
Centos 7 support has been depreceated since modoboa requires python 3.7>=.
.. note:: .. note::
@@ -43,7 +45,7 @@ The following components are installed by the installer:
* Nginx and uWSGI * Nginx and uWSGI
* Postfix * Postfix
* Dovecot * Dovecot
* Amavis (with SpamAssassin and ClamAV) or Rspamd * Amavis (with SpamAssassin and ClamAV)
* automx (autoconfiguration service) * automx (autoconfiguration service)
* OpenDKIM * OpenDKIM
* Radicale (CalDAV and CardDAV server) * Radicale (CalDAV and CardDAV server)
@@ -76,7 +78,7 @@ If you want more information about the installation process, add the
``--debug`` option to your command line. ``--debug`` option to your command line.
Upgrade mode Upgrade mode
============ ------------
An experimental upgrade mode is available. An experimental upgrade mode is available.
@@ -92,7 +94,7 @@ You can activate it as follows::
It will automatically install latest versions of modoboa and its plugins. It will automatically install latest versions of modoboa and its plugins.
Backup mode Backup mode
=========== ------------
An experimental backup mode is available. An experimental backup mode is available.
@@ -107,19 +109,7 @@ You can start the process as follows::
Then follow the step on the console. Then follow the step on the console.
There is also a non-interactive mode:: There is also a non-interactive mode:
$ sudo ./run.py --silent-backup <your domain>
You can also add a path, else it will be saved in ./modoboa_backup/Backup_M_Y_d_H_M::
$ sudo ./run.py --silent-backup --backup-path "/My_Backup_Path" <your domain>
if you want to disable mail backup::
$ sudo ./run.py --backup --no-mail <your domain>
This can be useful for larger instance
1. Silent mode 1. Silent mode
@@ -141,7 +131,7 @@ configuration file (set enabled to False).
This can be useful for larger instance. This can be useful for larger instance.
Restore mode Restore mode
============ ------------
An experimental restore mode is available. An experimental restore mode is available.
@@ -152,7 +142,7 @@ You can start the process as follows::
Then wait for the process to finish. Then wait for the process to finish.
Change the generated hostname Change the generated hostname
============================= -----------------------------
By default, the installer will setup your email server using the By default, the installer will setup your email server using the
following hostname: ``mail.<your domain>``. If you want a different following hostname: ``mail.<your domain>``. If you want a different
@@ -171,24 +161,12 @@ modifications.
Finally, run the installer without the Finally, run the installer without the
``--stop-after-configfile-check`` option. ``--stop-after-configfile-check`` option.
Certificate Let's Encrypt certificate
=========== -------------------------
Self-signed
-----------
It is the default type of certificate the installer will generate, it
is however not recommended for production use.
Letsencrypt
-----------
.. warning:: .. warning::
Please note that by using this option, you agree to the `ToS Please note this option requires the hostname you're using to be
<https://community.letsencrypt.org/tos>`_ of
letsencrypt and that your IP will be logged (see ToS).
Please also note this option requires the hostname you're using to be
valid (ie. it can be resolved with a DNS query) and to match the valid (ie. it can be resolved with a DNS query) and to match the
server you're installing Modoboa on. server you're installing Modoboa on.
@@ -199,8 +177,6 @@ modify the following settings::
[certificate] [certificate]
generate = true generate = true
type = letsencrypt type = letsencrypt
tls_cert_file_path =
tls_key_file_path =
[letsencrypt] [letsencrypt]
email = admin@example.com email = admin@example.com
@@ -208,43 +184,6 @@ modify the following settings::
Change the ``email`` setting to a valid value since it will be used Change the ``email`` setting to a valid value since it will be used
for account recovery. for account recovery.
Manual
------
.. warning::
It is not possible to configure manual certs interactively, so
you'll have to do it in 2 steps. Please run ``run.py`` with
`--stop-after-configfile-check` first, configure your file as
desired and apply the configuration as written bellow. Then run
``run.py`` again but without `--stop-after-configfile-check` or
`--interactive`.
If you want to use already generated certs, simply edit the
``installer.cfg`` file and modify the following settings::
[certificate]
generate = true
type = manual
tls_cert_file_path = *path to tls fullchain file*
tls_key_file_path = *path to tls key file*
Antispam
========
You have 3 options regarding antispam : disabled, Amavis, Rspamd
Amavis
------
Amavis
Rspamd
------
Rspamd
.. |workflow| image:: https://github.com/modoboa/modoboa-installer/workflows/Modoboa%20installer/badge.svg .. |workflow| image:: https://github.com/modoboa/modoboa-installer/workflows/Modoboa%20installer/badge.svg
.. |codecov| image:: https://codecov.io/gh/modoboa/modoboa-installer/graph/badge.svg?token=Fo2o1GdHZq .. |codecov| image:: http://codecov.io/github/modoboa/modoboa-installer/coverage.svg?branch=master
:target: https://codecov.io/gh/modoboa/modoboa-installer :target: http://codecov.io/github/modoboa/modoboa-installer?branch=master

View File

@@ -1,37 +0,0 @@
"""Checks to be performed before any install or upgrade"""
import sys
from urllib.request import urlopen
from modoboa_installer import utils
def check_version():
local_version = ""
with open("version.txt", "r") as version:
local_version = version.readline()
remote_version = ""
with urlopen("https://raw.githubusercontent.com/modoboa/modoboa-installer/master/version.txt") as r_version:
remote_version = r_version.read().decode()
if local_version == "" or remote_version == "":
utils.printcolor(
"Could not check that your installer is up-to-date: "
f"local version: {local_version}, "
f"remote version: {remote_version}",
utils.YELLOW
)
if remote_version != local_version:
utils.error(
"Your installer seems outdated.\n"
"Check README file for instructions about how to update.\n"
"No support will be provided without an up-to-date installer!"
)
answer = utils.user_input("Continue anyway? (y/N) ")
if not answer.lower().startswith("y"):
sys.exit(0)
else:
utils.success("Installer seems up to date!")
def handle():
check_version()

View File

@@ -20,27 +20,9 @@ COMPATIBILITY_MATRIX = {
"modoboa-pdfcredentials": ">=1.1.1", "modoboa-pdfcredentials": ">=1.1.1",
"modoboa-sievefilters": ">=1.1.1", "modoboa-sievefilters": ">=1.1.1",
"modoboa-webmail": ">=1.2.0", "modoboa-webmail": ">=1.2.0",
}, }
} }
EXTENSIONS_AVAILABILITY = { EXTENSIONS_AVAILABILITY = {
"modoboa-contacts": "1.7.4", "modoboa-contacts": "1.7.4",
} }
REMOVED_EXTENSIONS = {
"modoboa-pdfcredentials": "2.1.0",
"modoboa-dmarc": "2.1.0",
"modoboa-imap-migration": "2.1.0",
"modoboa-sievefilters": "2.3.0",
"modoboa-postfix-autoreply": "2.3.0",
"modoboa-contacts": "2.4.0",
"modoboa-radicale": "2.4.0",
"modoboa-webmail": "2.4.0",
}
APP_INCOMPATIBILITY = {
"opendkim": ["rspamd"],
"amavis": ["rspamd"],
"postwhite": ["rspamd"],
"spamassassin": ["rspamd"]
}

View File

@@ -27,50 +27,25 @@ ConfigDictTemplate = [
} }
] ]
}, },
{
"name": "antispam",
"values": [
{
"option": "enabled",
"default": "true",
"customizable": True,
"values": ["true", "false"],
"question": "Do you want to setup an antispam utility?"
},
{
"option": "type",
"default": "amavis",
"customizable": True,
"question": "Please select your antispam utility",
"values": ["rspamd", "amavis"],
"if": ["antispam.enabled=true"]
}
]
},
{ {
"name": "certificate", "name": "certificate",
"values": [ "values": [
{
"option": "generate",
"default": "true",
},
{ {
"option": "type", "option": "type",
"default": "self-signed", "default": "self-signed",
"customizable": True, "customizable": True,
"question": "Please choose your certificate type", "question": "Please choose your certificate type",
"values": ["self-signed", "letsencrypt", "manual"], "values": ["self-signed", "letsencrypt"],
"non_interactive_values": ["manual"],
},
{
"option": "tls_cert_file_path",
"default": ""
},
{
"option": "tls_key_file_path",
"default": ""
} }
], ],
}, },
{ {
"name": "letsencrypt", "name": "letsencrypt",
"if": ["certificate.type=letsencrypt"], "if": "certificate.type=letsencrypt",
"values": [ "values": [
{ {
"option": "email", "option": "email",
@@ -105,7 +80,7 @@ ConfigDictTemplate = [
}, },
{ {
"name": "postgres", "name": "postgres",
"if": ["database.engine=postgres"], "if": "database.engine=postgres",
"values": [ "values": [
{ {
"option": "user", "option": "user",
@@ -121,7 +96,7 @@ ConfigDictTemplate = [
}, },
{ {
"name": "mysql", "name": "mysql",
"if": ["database.engine=mysql"], "if": "database.engine=mysql",
"values": [ "values": [
{ {
"option": "user", "option": "user",
@@ -143,31 +118,6 @@ ConfigDictTemplate = [
} }
] ]
}, },
{
"name": "fail2ban",
"values": [
{
"option": "enabled",
"default": "true",
},
{
"option": "config_dir",
"default": "/etc/fail2ban"
},
{
"option": "max_retry",
"default": "20"
},
{
"option": "ban_time",
"default": "3600"
},
{
"option": "find_time",
"default": "30"
},
]
},
{ {
"name": "modoboa", "name": "modoboa",
"values": [ "values": [
@@ -205,16 +155,14 @@ ConfigDictTemplate = [
"customizable": True, "customizable": True,
"question": "Please enter Modoboa db password", "question": "Please enter Modoboa db password",
}, },
{
"option": "cron_error_recipient",
"default": "root",
"customizable": True,
"question":
"Please enter a mail recipient for cron error reports"
},
{ {
"option": "extensions", "option": "extensions",
"default": "" "default": (
"modoboa-amavis modoboa-pdfcredentials "
"modoboa-postfix-autoreply modoboa-sievefilters "
"modoboa-webmail modoboa-contacts "
"modoboa-radicale"
),
}, },
{ {
"option": "devmode", "option": "devmode",
@@ -251,60 +199,12 @@ ConfigDictTemplate = [
}, },
] ]
}, },
{
"name": "rspamd",
"if": ["antispam.enabled=true", "antispam.type=rspamd"],
"values": [
{
"option": "enabled",
"default": ["antispam.enabled=true", "antispam.type=rspamd"],
},
{
"option": "user",
"default": "_rspamd",
},
{
"option": "password",
"default": make_password,
"customizable": True,
"question": "Please enter Rspamd interface password",
},
{
"option": "dnsbl",
"default": "true",
},
{
"option": "dkim_keys_storage_dir",
"default": "/var/lib/dkim"
},
{
"option": "key_map_path",
"default": "/var/lib/dkim/keys.path.map"
},
{
"option": "selector_map_path",
"default": "/var/lib/dkim/selectors.path.map"
},
{
"option": "greylisting",
"default": "true"
},
{
"option": "whitelist_auth",
"default": "true"
},
{
"option": "whitelist_auth_weigth",
"default": "-5"
}
],
},
{ {
"name": "amavis", "name": "amavis",
"values": [ "values": [
{ {
"option": "enabled", "option": "enabled",
"default": ["antispam.enabled=true", "antispam.type=amavis"], "default": "true",
}, },
{ {
"option": "user", "option": "user",
@@ -325,6 +225,8 @@ ConfigDictTemplate = [
{ {
"option": "dbpassword", "option": "dbpassword",
"default": make_password, "default": make_password,
"customizable": True,
"question": "Please enter amavis db password"
}, },
], ],
}, },
@@ -354,7 +256,7 @@ ConfigDictTemplate = [
}, },
{ {
"option": "user", "option": "user",
"default": "dovecot", "default": "vmail",
}, },
{ {
"option": "home_dir", "option": "home_dir",
@@ -374,11 +276,7 @@ ConfigDictTemplate = [
}, },
{ {
"option": "radicale_auth_socket_path", "option": "radicale_auth_socket_path",
"default": "/var/run/dovecot/auth-radicale", "default": "/var/run/dovecot/auth-radicale"
},
{
"option": "move_spam_to_junk",
"default": "true",
}, },
] ]
}, },
@@ -400,7 +298,7 @@ ConfigDictTemplate = [
"values": [ "values": [
{ {
"option": "enabled", "option": "enabled",
"default": "false", "default": "true",
}, },
{ {
"option": "config_dir", "option": "config_dir",
@@ -423,10 +321,6 @@ ConfigDictTemplate = [
"option": "message_size_limit", "option": "message_size_limit",
"default": "11534336", "default": "11534336",
}, },
{
"option": "dhe_group",
"default": "4096"
}
] ]
}, },
{ {
@@ -434,7 +328,7 @@ ConfigDictTemplate = [
"values": [ "values": [
{ {
"option": "enabled", "option": "enabled",
"default": ["antispam.enabled=true", "antispam.type=amavis"], "default": "true",
}, },
{ {
"option": "config_dir", "option": "config_dir",
@@ -444,11 +338,10 @@ ConfigDictTemplate = [
}, },
{ {
"name": "spamassassin", "name": "spamassassin",
"if": ["antispam.enabled=true", "antispam.type=amavis"],
"values": [ "values": [
{ {
"option": "enabled", "option": "enabled",
"default": ["antispam.enabled=true", "antispam.type=amavis"], "default": "true",
}, },
{ {
"option": "config_dir", "option": "config_dir",
@@ -483,7 +376,7 @@ ConfigDictTemplate = [
}, },
{ {
"option": "nb_processes", "option": "nb_processes",
"default": "4", "default": "2",
}, },
] ]
}, },
@@ -514,11 +407,10 @@ ConfigDictTemplate = [
}, },
{ {
"name": "opendkim", "name": "opendkim",
"if": ["antispam.enabled=true", "antispam.type=amavis"],
"values": [ "values": [
{ {
"option": "enabled", "option": "enabled",
"default": ["antispam.enabled=true", "antispam.type=amavis"], "default": "true",
}, },
{ {
"option": "user", "option": "user",

View File

@@ -42,7 +42,7 @@ class PostgreSQL(Database):
default_port = 5432 default_port = 5432
packages = { packages = {
"deb": ["postgresql", "postgresql-server-dev-all"], "deb": ["postgresql", "postgresql-server-dev-all"],
"rpm": ["postgresql-server", "postgresql-devel"] "rpm": ["postgresql-server", "postgresql-server-devel", "postgresql"]
} }
service = "postgresql" service = "postgresql"
@@ -54,19 +54,7 @@ class PostgreSQL(Database):
"""Install database if required.""" """Install database if required."""
name, version = utils.dist_info() name, version = utils.dist_info()
if "CentOS" in name: if "CentOS" in name:
if version.startswith("7"): initdb_cmd = "postgresql-setup --initdb"
# Install newer version of postgres in this case
package.backend.install(
"https://download.postgresql.org/pub/repos/yum/"
"reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm"
)
self.packages["rpm"] = [
"postgresql10-server", "postgresql10-devel"]
self.service = "postgresql-10"
initdb_cmd = "/usr/pgsql-10/bin/postgresql-10-setup initdb"
cfgfile = "/var/lib/pgsql/10/data/pg_hba.conf"
else:
initdb_cmd = "postgresql-setup initdb"
cfgfile = "/var/lib/pgsql/data/pg_hba.conf" cfgfile = "/var/lib/pgsql/data/pg_hba.conf"
package.backend.install_many(self.packages[package.backend.FORMAT]) package.backend.install_many(self.packages[package.backend.FORMAT])
utils.exec_cmd(initdb_cmd) utils.exec_cmd(initdb_cmd)
@@ -103,7 +91,7 @@ class PostgreSQL(Database):
def create_database(self, name, owner): def create_database(self, name, owner):
"""Create a database.""" """Create a database."""
code, output = utils.exec_cmd( code, output = utils.exec_cmd(
"psql -lqt | cut -d \\| -f 1 | grep -w {} | wc -l" "psql -lqt | cut -d \| -f 1 | grep -w {} | wc -l"
.format(name), sudo_user=self.dbuser) .format(name), sudo_user=self.dbuser)
if code: if code:
return return
@@ -178,15 +166,11 @@ class MySQL(Database):
if name.startswith("debian"): if name.startswith("debian"):
if version.startswith("8"): if version.startswith("8"):
self.packages["deb"].append("libmysqlclient-dev") self.packages["deb"].append("libmysqlclient-dev")
elif version.startswith("11") or version.startswith("12"): elif version.startswith("11"):
self.packages["deb"].append("libmariadb-dev") self.packages["deb"].append("libmariadb-dev")
else: else:
self.packages["deb"].append("libmariadbclient-dev") self.packages["deb"].append("libmariadbclient-dev")
elif name == "ubuntu": elif name == "ubuntu":
if version.startswith("2"):
# Works for Ubuntu 20, 22, and 24.
self.packages["deb"].append("libmariadb-dev")
else:
self.packages["deb"].append("libmysqlclient-dev") self.packages["deb"].append("libmysqlclient-dev")
super(MySQL, self).install_package() super(MySQL, self).install_package()
queries = [] queries = []
@@ -199,10 +183,7 @@ class MySQL(Database):
"mariadb-server", "root_password_again", "password", "mariadb-server", "root_password_again", "password",
self.dbpassword) self.dbpassword)
return return
if ( if version.startswith("11"):
(name.startswith("debian") and (version.startswith("11") or version.startswith("12"))) or
(name.startswith("ubuntu") and int(version[:2]) >= 22)
):
queries = [ queries = [
"SET PASSWORD FOR 'root'@'localhost' = PASSWORD('{}')" "SET PASSWORD FOR 'root'@'localhost' = PASSWORD('{}')"
.format(self.dbpassword), .format(self.dbpassword),

View File

@@ -1,51 +0,0 @@
from . import utils
def installation_disclaimer(args, config):
"""Display installation disclaimer."""
hostname = config.get("general", "hostname")
utils.printcolor(
"Notice:\n"
"It is recommanded to run this installer on a FRESHLY installed server.\n"
"(ie. with nothing special already installed on it)\n",
utils.CYAN
)
utils.printcolor(
"Warning:\n"
"Before you start the installation, please make sure the following "
"DNS records exist for domain '{}':\n"
" {} IN A <IP ADDRESS OF YOUR SERVER>\n"
" @ IN MX {}.\n".format(
args.domain,
hostname.replace(".{}".format(args.domain), ""),
hostname
),
utils.YELLOW
)
utils.printcolor(
"Your mail server will be installed with the following components:",
utils.BLUE)
def upgrade_disclaimer(config):
"""Display upgrade disclaimer."""
utils.printcolor(
"Your mail server is about to be upgraded and the following components"
" will be impacted:", utils.BLUE
)
def backup_disclaimer():
"""Display backup disclamer. """
utils.printcolor(
"Your mail server will be backed up locally.\n"
" !! You should really transfer the backup somewhere else...\n"
" !! Custom configuration (like for postfix) won't be saved.", utils.BLUE)
def restore_disclaimer():
"""Display restore disclamer. """
utils.printcolor(
"You are about to restore a previous installation of Modoboa.\n"
"If a new version has been released in between, please update your database!",
utils.BLUE)

View File

@@ -2,12 +2,10 @@
import re import re
from os.path import isfile as file_exists
from . import utils from . import utils
class Package: class Package(object):
"""Base classe.""" """Base classe."""
def __init__(self, dist_name): def __init__(self, dist_name):
@@ -31,17 +29,10 @@ class DEBPackage(Package):
FORMAT = "deb" FORMAT = "deb"
def __init__(self, dist_name): def __init__(self, dist_name):
super().__init__(dist_name) super(DEBPackage, self).__init__(dist_name)
self.index_updated = False self.index_updated = False
self.policy_file = "/usr/sbin/policy-rc.d" self.policy_file = "/usr/sbin/policy-rc.d"
def enable_backports(self, codename):
code, output = utils.exec_cmd(f"grep {codename}-backports /etc/apt/sources.list")
if code:
with open(f"/etc/apt/sources.list.d/backports.list", "w") as fp:
fp.write(f"deb http://deb.debian.org/debian {codename}-backports main\n")
self.update(force=True)
def prepare_system(self): def prepare_system(self):
"""Make sure services don't start at installation.""" """Make sure services don't start at installation."""
with open(self.policy_file, "w") as fp: with open(self.policy_file, "w") as fp:
@@ -51,34 +42,11 @@ class DEBPackage(Package):
def restore_system(self): def restore_system(self):
utils.exec_cmd("rm -f {}".format(self.policy_file)) utils.exec_cmd("rm -f {}".format(self.policy_file))
def add_custom_repository(self, def update(self):
name: str,
url: str,
key_url: str,
codename: str,
with_source: bool = True):
key_file = f"/etc/apt/keyrings/{name}.gpg"
utils.exec_cmd(
f"wget -O - {key_url} | gpg --dearmor | tee {key_file} > /dev/null"
)
line_types = ["deb"]
if with_source:
line_types.append("deb-src")
for line_type in line_types:
line = (
f"{line_type} [arch=amd64 signed-by={key_file}] "
f"{url} {codename} main"
)
target_file = f"/etc/apt/sources.list.d/{name}.list"
tee_option = "-a" if file_exists(target_file) else ""
utils.exec_cmd(f'echo "{line}" | tee {tee_option} {target_file}')
self.index_updated = False
def update(self, force=False):
"""Update local cache.""" """Update local cache."""
if self.index_updated and not force: if self.index_updated:
return return
utils.exec_cmd("apt-get -o Dpkg::Progress-Fancy=0 update --quiet") utils.exec_cmd("apt-get update --quiet")
self.index_updated = True self.index_updated = True
def preconfigure(self, name, question, qtype, answer): def preconfigure(self, name, question, qtype, answer):
@@ -89,18 +57,18 @@ class DEBPackage(Package):
def install(self, name): def install(self, name):
"""Install a package.""" """Install a package."""
self.update() self.update()
utils.exec_cmd("apt-get -o Dpkg::Progress-Fancy=0 install --quiet --assume-yes -o DPkg::options::=--force-confold {}".format(name)) utils.exec_cmd("apt-get install --quiet --assume-yes {}".format(name))
def install_many(self, names): def install_many(self, names):
"""Install many packages.""" """Install many packages."""
self.update() self.update()
return utils.exec_cmd("apt-get -o Dpkg::Progress-Fancy=0 install --quiet --assume-yes -o DPkg::options::=--force-confold {}".format( return utils.exec_cmd("apt-get install --quiet --assume-yes {}".format(
" ".join(names))) " ".join(names)))
def get_installed_version(self, name): def get_installed_version(self, name):
"""Get installed package version.""" """Get installed package version."""
code, output = utils.exec_cmd( code, output = utils.exec_cmd(
"dpkg -s {} | grep Version".format(name)) "dpkg -s {} | grep Version".format(name), capture_output=True)
match = re.match(r"Version: (\d:)?(.+)-\d", output.decode()) match = re.match(r"Version: (\d:)?(.+)-\d", output.decode())
if match: if match:
return match.group(2) return match.group(2)
@@ -114,22 +82,32 @@ class RPMPackage(Package):
def __init__(self, dist_name): def __init__(self, dist_name):
"""Initialize backend.""" """Initialize backend."""
super().__init__(dist_name) self.dist_name = dist_name
if "centos" in dist_name: super(RPMPackage, self).__init__(dist_name)
def prepare_system(self):
if "centos" in self.dist_name:
utils.exec_cmd("dnf config-manager --set-enabled crb")
self.install("epel-release") self.install("epel-release")
self.update()
def update(self):
"""Update the database repo."""
utils.exec_cmd("dnf update -y --quiet")
def install(self, name): def install(self, name):
"""Install a package.""" """Install a package."""
utils.exec_cmd("yum install -y --quiet {}".format(name)) """Need to add check for rrdtool, sendmail-milter, libmemcached and --enablerepo=crb"""
utils.exec_cmd("dnf install -y --quiet {}".format(name))
def install_many(self, names): def install_many(self, names):
"""Install many packages.""" """Install many packages."""
return utils.exec_cmd("yum install -y --quiet {}".format(" ".join(names))) return utils.exec_cmd("dnf install -y --quiet {}".format(" ".join(names)))
def get_installed_version(self, name): def get_installed_version(self, name):
"""Get installed package version.""" """Get installed package version."""
code, output = utils.exec_cmd( code, output = utils.exec_cmd(
"rpm -qi {} | grep Version".format(name)) "rpm -qi {} | grep Version".format(name), capture_output=True)
match = re.match(r"Version\s+: (.+)", output.decode()) match = re.match(r"Version\s+: (.+)", output.decode())
if match: if match:
return match.group(1) return match.group(1)
@@ -140,7 +118,7 @@ def get_backend():
"""Return the appropriate package backend.""" """Return the appropriate package backend."""
distname = utils.dist_name() distname = utils.dist_name()
backend = None backend = None
if distname in ["debian", "debian gnu/linux", "ubuntu", "linuxmint"]: if distname in ["debian", "debian gnu/linux", "ubuntu"]:
backend = DEBPackage backend = DEBPackage
elif "centos" in distname: elif "centos" in distname:
backend = RPMPackage backend = RPMPackage

View File

@@ -1,7 +1,6 @@
"""Python related tools.""" """Python related tools."""
import os import os
import sys
from . import package from . import package
from . import utils from . import utils
@@ -46,40 +45,6 @@ def install_packages(names, venv=None, upgrade=False, **kwargs):
utils.exec_cmd(cmd, **kwargs) utils.exec_cmd(cmd, **kwargs)
def get_package_version(name, venv=None, **kwargs):
"""Returns the version of an installed package."""
cmd = "{} show {}".format(
get_pip_path(venv),
name
)
exit_code, output = utils.exec_cmd(cmd, **kwargs)
if exit_code != 0:
utils.error(f"Failed to get version of {name}. "
f"Output is: {output}")
sys.exit(1)
version_list_clean = []
for line in output.decode().split("\n"):
if not line.startswith("Version:"):
continue
version_item_list = line.split(":")
version_list = version_item_list[1].split(".")
for element in version_list:
try:
version_list_clean.append(int(element))
except ValueError:
utils.printcolor(
f"Failed to decode some part of the version of {name}",
utils.YELLOW)
version_list_clean.append(element)
if len(version_list_clean) == 0:
utils.printcolor(
f"Failed to find the version of {name}",
utils.RED)
sys.exit(1)
return version_list_clean
def install_package_from_repository(name, url, vcs="git", venv=None, **kwargs): def install_package_from_repository(name, url, vcs="git", venv=None, **kwargs):
"""Install a Python package from its repository.""" """Install a Python package from its repository."""
if vcs == "git": if vcs == "git":
@@ -89,10 +54,16 @@ def install_package_from_repository(name, url, vcs="git", venv=None, **kwargs):
utils.exec_cmd(cmd, **kwargs) utils.exec_cmd(cmd, **kwargs)
def setup_virtualenv(path, sudo_user=None): def setup_virtualenv(path, sudo_user=None, python_version=2):
"""Install a virtualenv if needed.""" """Install a virtualenv if needed."""
if os.path.exists(path): if os.path.exists(path):
return return
if python_version == 2:
python_binary = "python"
packages = ["python-virtualenv"]
if utils.dist_name() == "debian":
packages.append("virtualenv")
else:
if utils.dist_name().startswith("centos"): if utils.dist_name().startswith("centos"):
python_binary = "python3" python_binary = "python3"
packages = ["python3"] packages = ["python3"]
@@ -101,5 +72,8 @@ def setup_virtualenv(path, sudo_user=None):
packages = ["python3-venv"] packages = ["python3-venv"]
package.backend.install_many(packages) package.backend.install_many(packages)
with utils.settings(sudo_user=sudo_user): with utils.settings(sudo_user=sudo_user):
if python_version == 2:
utils.exec_cmd("virtualenv {}".format(path))
else:
utils.exec_cmd("{} -m venv {}".format(python_binary, path)) utils.exec_cmd("{} -m venv {}".format(python_binary, path))
install_packages(["pip", "setuptools"], venv=path, upgrade=True) install_packages(["pip", "setuptools\<58.0.0"], venv=path, upgrade=True)

View File

@@ -21,7 +21,7 @@ class Amavis(base.Installer):
"unrar-free", "unrar-free",
], ],
"rpm": [ "rpm": [
"amavisd-new", "arj", "lz4", "lzop", "p7zip", "amavis", "arj", "lz4", "lzop", "p7zip",
], ],
} }
with_db = True with_db = True

View File

@@ -44,21 +44,22 @@ class Automx(base.Installer):
sql_query = ( sql_query = (
"SELECT first_name || ' ' || last_name AS display_name, email" "SELECT first_name || ' ' || last_name AS display_name, email"
", SPLIT_PART(email, '@', 2) AS domain " ", SPLIT_PART(email, '@', 2) AS domain "
"FROM core_user WHERE email='%s' AND is_active;") "FROM core_user WHERE email='%s' AND is_active")
else: else:
sql_query = ( sql_query = (
"SELECT concat(first_name, ' ', last_name) AS display_name, " "SELECT concat(first_name, ' ', last_name) AS display_name, "
"email, SUBSTRING_INDEX(email, '@', -1) AS domain " "email, SUBSTRING_INDEX(email, '@', -1) AS domain "
"FROM core_user WHERE email='%s' AND is_active=1;" "FROM core_user WHERE email='%s' AND is_active=1"
) )
context.update({"sql_dsn": sql_dsn, "sql_query": sql_query}) context.update({"sql_dsn": sql_dsn, "sql_query": sql_query})
return context return context
def _setup_venv(self): def _setup_venv(self):
"""Prepare a python virtualenv.""" """Prepare a python virtualenv."""
python.setup_virtualenv(self.venv_path, sudo_user=self.user) python.setup_virtualenv(
self.venv_path, sudo_user=self.user, python_version=3)
packages = [ packages = [
"future", "lxml", "ipaddress", "sqlalchemy < 2.0", "python-memcached", "future", "lxml", "ipaddress", "sqlalchemy", "python-memcached",
"python-dateutil", "configparser" "python-dateutil", "configparser"
] ]
if self.dbengine == "postgres": if self.dbengine == "postgres":

View File

@@ -44,7 +44,8 @@ class Backup:
path_exists = os.path.exists(path) path_exists = os.path.exists(path)
if path_exists and os.path.isfile(path): if path_exists and os.path.isfile(path):
utils.error("Error, you provided a file instead of a directory!") utils.printcolor(
"Error, you provided a file instead of a directory!", utils.RED)
return False return False
if not path_exists: if not path_exists:
@@ -57,7 +58,9 @@ class Backup:
utils.mkdir_safe(path, stat.S_IRWXU | utils.mkdir_safe(path, stat.S_IRWXU |
stat.S_IRWXG, pw[2], pw[3]) stat.S_IRWXG, pw[2], pw[3])
else: else:
utils.error("Error, backup directory not present.") utils.printcolor(
"Error, backup directory not present.", utils.RED
)
return False return False
if len(os.listdir(path)) != 0: if len(os.listdir(path)) != 0:
@@ -77,7 +80,9 @@ class Backup:
shutil.rmtree(os.path.join(path, "databases"), shutil.rmtree(os.path.join(path, "databases"),
ignore_errors=False) ignore_errors=False)
else: else:
utils.error("Error: backup directory not clean.") utils.printcolor(
"Error: backup directory not clean.", utils.RED
)
return False return False
self.backup_path = path self.backup_path = path
@@ -126,8 +131,8 @@ class Backup:
home_path = self.config.get("dovecot", "home_dir") home_path = self.config.get("dovecot", "home_dir")
if not os.path.exists(home_path) or os.path.isfile(home_path): if not os.path.exists(home_path) or os.path.isfile(home_path):
utils.error("Error backing up Email, provided path " utils.printcolor("Error backing up Email, provided path "
f" ({home_path}) seems not right...") f" ({home_path}) seems not right...", utils.RED)
else: else:
dst = os.path.join(self.backup_path, "mails/") dst = os.path.join(self.backup_path, "mails/")
@@ -142,7 +147,7 @@ class Backup:
""" """
Custom config : Custom config :
- DKIM keys: {{keys_storage_dir}} - DKIM keys: {{keys_storage_dir}}
- Radicale collection (calendars, contacts): {{home_dir}} - Radicale collection (calendat, contacts): {{home_dir}}
- Amavis : /etc/amavis/conf.d/99-custom - Amavis : /etc/amavis/conf.d/99-custom
- Postwhite : /etc/postwhite.conf - Postwhite : /etc/postwhite.conf
Feel free to suggest to add others! Feel free to suggest to add others!

View File

@@ -5,12 +5,11 @@ import sys
from .. import database from .. import database
from .. import package from .. import package
from .. import python
from .. import system from .. import system
from .. import utils from .. import utils
class Installer: class Installer(object):
"""Simple installer for one application.""" """Simple installer for one application."""
appname = None appname = None
@@ -43,20 +42,6 @@ class Installer:
self.dbuser = self.config.get(self.appname, "dbuser") self.dbuser = self.config.get(self.appname, "dbuser")
self.dbpasswd = self.config.get(self.appname, "dbpassword") self.dbpasswd = self.config.get(self.appname, "dbpassword")
@property
def modoboa_2_2_or_greater(self):
# Check if modoboa version > 2.2
modoboa_version = python.get_package_version(
"modoboa",
self.config.get("modoboa", "venv_path"),
sudo_user=self.config.get("modoboa", "user")
)
condition = (
(int(modoboa_version[0]) == 2 and int(modoboa_version[1]) >= 2) or
int(modoboa_version[0]) > 2
)
return condition
@property @property
def config_dir(self): def config_dir(self):
"""Return main configuration directory.""" """Return main configuration directory."""
@@ -146,7 +131,7 @@ class Installer:
return return
exitcode, output = package.backend.install_many(packages) exitcode, output = package.backend.install_many(packages)
if exitcode: if exitcode:
utils.error("Failed to install dependencies") utils.printcolor("Failed to install dependencies", utils.RED)
sys.exit(1) sys.exit(1)
def get_config_files(self): def get_config_files(self):

View File

@@ -15,7 +15,7 @@ class Clamav(base.Installer):
packages = { packages = {
"deb": ["clamav-daemon"], "deb": ["clamav-daemon"],
"rpm": [ "rpm": [
"clamav", "clamav-update", "clamav-server", "clamav-server-systemd" "clamav", "clamav-update", "clamd"
], ],
} }
@@ -42,7 +42,6 @@ class Clamav(base.Installer):
"""Additional tasks.""" """Additional tasks."""
if package.backend.FORMAT == "deb": if package.backend.FORMAT == "deb":
user = self.config.get(self.appname, "user") user = self.config.get(self.appname, "user")
if self.config.getboolean("amavis", "enabled"):
system.add_user_to_group( system.add_user_to_group(
user, self.config.get("amavis", "user") user, self.config.get("amavis", "user")
) )
@@ -58,7 +57,7 @@ class Clamav(base.Installer):
# Check if not present before # Check if not present before
path = "/usr/lib/systemd/system/clamd@.service" path = "/usr/lib/systemd/system/clamd@.service"
code, output = utils.exec_cmd( code, output = utils.exec_cmd(
r"grep 'WantedBy\s*=\s*multi-user.target' {}".format(path)) "grep 'WantedBy=multi-user.target' {}".format(path))
if code: if code:
utils.exec_cmd( utils.exec_cmd(
"""cat <<EOM >> {} """cat <<EOM >> {}

View File

@@ -4,8 +4,6 @@ import glob
import os import os
import pwd import pwd
import shutil import shutil
import stat
import uuid
from .. import database from .. import database
from .. import package from .. import package
@@ -28,33 +26,13 @@ class Dovecot(base.Installer):
"dovecot", "dovecot-pigeonhole"] "dovecot", "dovecot-pigeonhole"]
} }
config_files = [ config_files = [
"dovecot.conf", "dovecot.conf", "dovecot-dict-sql.conf.ext", "conf.d/10-ssl.conf",
"dovecot-dict-sql.conf.ext", "conf.d/10-master.conf", "conf.d/20-lmtp.conf", "conf.d/10-ssl-keys.try"]
"conf.d/10-ssl.conf",
"conf.d/10-master.conf",
"conf.d/20-lmtp.conf",
"conf.d/10-ssl-keys.try",
"conf.d/dovecot-oauth2.conf.ext",
]
with_user = True with_user = True
def setup_user(self):
"""Setup mailbox user."""
super().setup_user()
self.mailboxes_owner = self.app_config["mailboxes_owner"]
system.create_user(self.mailboxes_owner, self.home_dir)
def get_config_files(self): def get_config_files(self):
"""Additional config files.""" """Additional config files."""
_config_files = self.config_files return self.config_files + [
if self.app_config["move_spam_to_junk"]:
_config_files += [
"conf.d/custom_after_sieve/spam-to-junk.sieve",
"conf.d/90-sieve.conf",
]
return _config_files + [
"dovecot-sql-{}.conf.ext=dovecot-sql.conf.ext" "dovecot-sql-{}.conf.ext=dovecot-sql.conf.ext"
.format(self.dbengine), .format(self.dbengine),
"dovecot-sql-master-{}.conf.ext=dovecot-sql-master.conf.ext" "dovecot-sql-master-{}.conf.ext=dovecot-sql-master.conf.ext"
@@ -69,27 +47,18 @@ class Dovecot(base.Installer):
if package.backend.FORMAT == "deb": if package.backend.FORMAT == "deb":
if "pop3" in self.config.get("dovecot", "extra_protocols"): if "pop3" in self.config.get("dovecot", "extra_protocols"):
packages += ["dovecot-pop3d"] packages += ["dovecot-pop3d"]
packages += super().get_packages() return super(Dovecot, self).get_packages() + packages
backports_codename = getattr(self, "backports_codename", None)
if backports_codename:
packages = [f"{package}/{backports_codename}-backports" for package in packages]
return packages
def install_packages(self): def install_packages(self):
"""Preconfigure Dovecot if needed.""" """Preconfigure Dovecot if needed."""
name, version = utils.dist_info()
name = name.lower()
if name.startswith("debian") and version.startswith("12"):
package.backend.enable_backports("bookworm")
self.backports_codename = "bookworm"
package.backend.preconfigure( package.backend.preconfigure(
"dovecot-core", "create-ssl-cert", "boolean", "false") "dovecot-core", "create-ssl-cert", "boolean", "false")
super().install_packages() super(Dovecot, self).install_packages()
def get_template_context(self): def get_template_context(self):
"""Additional variables.""" """Additional variables."""
context = super().get_template_context() context = super(Dovecot, self).get_template_context()
pw_mailbox = pwd.getpwnam(self.mailboxes_owner) pw = pwd.getpwnam(self.user)
dovecot_package = {"deb": "dovecot-core", "rpm": "dovecot"} dovecot_package = {"deb": "dovecot-core", "rpm": "dovecot"}
ssl_protocol_parameter = "ssl_protocols" ssl_protocol_parameter = "ssl_protocols"
if package.backend.get_installed_version(dovecot_package[package.backend.FORMAT]) > "2.3": if package.backend.get_installed_version(dovecot_package[package.backend.FORMAT]) > "2.3":
@@ -108,20 +77,10 @@ class Dovecot(base.Installer):
else: else:
# Protocols are automatically guessed on debian/ubuntu # Protocols are automatically guessed on debian/ubuntu
protocols = "" protocols = ""
oauth2_client_id, oauth2_client_secret = utils.create_oauth2_app(
"Dovecot", "dovecot", self.config)
hostname = self.config.get("general", "hostname")
oauth2_introspection_url = (
f"https://{oauth2_client_id}:{oauth2_client_secret}"
f"@{hostname}/api/o/introspect/"
)
context.update({ context.update({
"db_driver": self.db_driver, "db_driver": self.db_driver,
"mailboxes_owner_uid": pw_mailbox[2], "mailboxes_owner_uid": pw[2],
"mailboxes_owner_gid": pw_mailbox[3], "mailboxes_owner_gid": pw[3],
"mailbox_owner": self.mailboxes_owner,
"modoboa_user": self.config.get("modoboa", "user"), "modoboa_user": self.config.get("modoboa", "user"),
"modoboa_dbname": self.config.get("modoboa", "dbname"), "modoboa_dbname": self.config.get("modoboa", "dbname"),
"modoboa_dbuser": self.config.get("modoboa", "dbuser"), "modoboa_dbuser": self.config.get("modoboa", "dbuser"),
@@ -129,24 +88,12 @@ class Dovecot(base.Installer):
"protocols": protocols, "protocols": protocols,
"ssl_protocols": ssl_protocols, "ssl_protocols": ssl_protocols,
"ssl_protocol_parameter": ssl_protocol_parameter, "ssl_protocol_parameter": ssl_protocol_parameter,
"modoboa_2_2_or_greater": "" if self.modoboa_2_2_or_greater else "#", "radicale_user": self.config.get("radicale", "user"),
"not_modoboa_2_2_or_greater": "" if not self.modoboa_2_2_or_greater else "#", "radicale_auth_socket_path": os.path.basename(
"do_move_spam_to_junk": "" if self.app_config["move_spam_to_junk"] else "#", self.config.get("dovecot", "radicale_auth_socket_path"))
"oauth2_introspection_url": oauth2_introspection_url
}) })
return context return context
def install_config_files(self):
"""Create sieve dir if needed."""
if self.app_config["move_spam_to_junk"]:
utils.mkdir_safe(
f"{self.config_dir}/conf.d/custom_after_sieve",
stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP |
stat.S_IROTH | stat.S_IXOTH,
0, 0
)
super().install_config_files()
def post_run(self): def post_run(self):
"""Additional tasks.""" """Additional tasks."""
if self.dbengine == "postgres": if self.dbengine == "postgres":
@@ -163,20 +110,15 @@ class Dovecot(base.Installer):
self.get_file_path("fix_modoboa_postgres_schema.sql") self.get_file_path("fix_modoboa_postgres_schema.sql")
) )
for f in glob.glob("{}/*".format(self.get_file_path("conf.d"))): for f in glob.glob("{}/*".format(self.get_file_path("conf.d"))):
if os.path.isfile(f):
utils.copy_file(f, "{}/conf.d".format(self.config_dir)) utils.copy_file(f, "{}/conf.d".format(self.config_dir))
# Make postlogin script executable # Make postlogin script executable
utils.exec_cmd("chmod +x /usr/local/bin/postlogin.sh") utils.exec_cmd("chmod +x /usr/local/bin/postlogin.sh")
# Only root should have read access to the 10-ssl-keys.try
# See https://github.com/modoboa/modoboa/issues/2570
utils.exec_cmd("chmod 600 /etc/dovecot/conf.d/10-ssl-keys.try")
# Add mailboxes user to dovecot group for modoboa mailbox commands. # Add mailboxes user to dovecot group for modoboa mailbox commands.
# See https://github.com/modoboa/modoboa/issues/2157. # See https://github.com/modoboa/modoboa/issues/2157.
if self.app_config["move_spam_to_junk"]: system.add_user_to_group(
# Compile sieve script self.config.get("dovecot", "mailboxes_owner"),
sieve_file = f"{self.config_dir}/conf.d/custom_after_sieve/spam-to-junk.sieve" 'dovecot'
utils.exec_cmd(f"/usr/bin/sievec {sieve_file}") )
system.add_user_to_group(self.mailboxes_owner, 'dovecot')
def restart_daemon(self): def restart_daemon(self):
"""Restart daemon process. """Restart daemon process.
@@ -219,10 +161,10 @@ class Dovecot(base.Installer):
shutil.copytree(mail_dir, home_dir) shutil.copytree(mail_dir, home_dir)
# Resetting permission for vmail # Resetting permission for vmail
for dirpath, dirnames, filenames in os.walk(home_dir): for dirpath, dirnames, filenames in os.walk(home_dir):
shutil.chown(dirpath, self.mailboxes_owner, self.mailboxes_owner) shutil.chown(dirpath, self.user, self.user)
for filename in filenames: for filename in filenames:
shutil.chown(os.path.join(dirpath, filename), shutil.chown(os.path.join(dirpath, filename),
self.mailboxes_owner, self.mailboxes_owner) self.user, self.user)
else: else:
utils.printcolor( utils.printcolor(
"It seems that emails were not backed up, skipping restoration.", "It seems that emails were not backed up, skipping restoration.",

View File

@@ -1,17 +0,0 @@
"""fail2ban related functions."""
from . import base
class Fail2ban(base.Installer):
"""Fail2ban installer."""
appname = "fail2ban"
packages = {
"deb": ["fail2ban"],
"rpm": ["fail2ban"]
}
config_files = [
"jail.d/modoboa.conf",
"filter.d/modoboa-auth.conf",
]

View File

@@ -1,213 +0,0 @@
-- Amavis 2.11.0 MySQL schema
-- Provided by Modoboa
-- Warning: foreign key creations are enabled
-- local users
CREATE TABLE users (
id int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY, -- unique id
priority integer NOT NULL DEFAULT '7', -- sort field, 0 is low prior.
policy_id integer unsigned NOT NULL DEFAULT '1', -- JOINs with policy.id
email varbinary(255) NOT NULL UNIQUE,
fullname varchar(255) DEFAULT NULL -- not used by amavisd-new
-- local char(1) -- Y/N (optional field, see note further down)
);
-- any e-mail address (non- rfc2822-quoted), external or local,
-- used as senders in wblist
CREATE TABLE mailaddr (
id int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY,
priority integer NOT NULL DEFAULT '7', -- 0 is low priority
email varbinary(255) NOT NULL UNIQUE
);
-- per-recipient whitelist and/or blacklist,
-- puts sender and recipient in relation wb (white or blacklisted sender)
CREATE TABLE wblist (
rid integer unsigned NOT NULL, -- recipient: users.id
sid integer unsigned NOT NULL, -- sender: mailaddr.id
wb varchar(10) NOT NULL, -- W or Y / B or N / space=neutral / score
PRIMARY KEY (rid,sid)
);
CREATE TABLE policy (
id int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY,
-- 'id' this is the _only_ required field
policy_name varchar(32), -- not used by amavisd-new, a comment
virus_lover char(1) default NULL, -- Y/N
spam_lover char(1) default NULL, -- Y/N
unchecked_lover char(1) default NULL, -- Y/N
banned_files_lover char(1) default NULL, -- Y/N
bad_header_lover char(1) default NULL, -- Y/N
bypass_virus_checks char(1) default NULL, -- Y/N
bypass_spam_checks char(1) default NULL, -- Y/N
bypass_banned_checks char(1) default NULL, -- Y/N
bypass_header_checks char(1) default NULL, -- Y/N
virus_quarantine_to varchar(64) default NULL,
spam_quarantine_to varchar(64) default NULL,
banned_quarantine_to varchar(64) default NULL,
unchecked_quarantine_to varchar(64) default NULL,
bad_header_quarantine_to varchar(64) default NULL,
clean_quarantine_to varchar(64) default NULL,
archive_quarantine_to varchar(64) default NULL,
spam_tag_level float default NULL, -- higher score inserts spam info headers
spam_tag2_level float default NULL, -- inserts 'declared spam' header fields
spam_tag3_level float default NULL, -- inserts 'blatant spam' header fields
spam_kill_level float default NULL, -- higher score triggers evasive actions
-- e.g. reject/drop, quarantine, ...
-- (subject to final_spam_destiny setting)
spam_dsn_cutoff_level float default NULL,
spam_quarantine_cutoff_level float default NULL,
addr_extension_virus varchar(64) default NULL,
addr_extension_spam varchar(64) default NULL,
addr_extension_banned varchar(64) default NULL,
addr_extension_bad_header varchar(64) default NULL,
warnvirusrecip char(1) default NULL, -- Y/N
warnbannedrecip char(1) default NULL, -- Y/N
warnbadhrecip char(1) default NULL, -- Y/N
newvirus_admin varchar(64) default NULL,
virus_admin varchar(64) default NULL,
banned_admin varchar(64) default NULL,
bad_header_admin varchar(64) default NULL,
spam_admin varchar(64) default NULL,
spam_subject_tag varchar(64) default NULL,
spam_subject_tag2 varchar(64) default NULL,
spam_subject_tag3 varchar(64) default NULL,
message_size_limit integer default NULL, -- max size in bytes, 0 disable
banned_rulenames varchar(64) default NULL, -- comma-separated list of ...
-- names mapped through %banned_rules to actual banned_filename tables
disclaimer_options varchar(64) default NULL,
forward_method varchar(64) default NULL,
sa_userconf varchar(64) default NULL,
sa_username varchar(64) default NULL
);
-- R/W part of the dataset (optional)
-- May reside in the same or in a separate database as lookups database;
-- REQUIRES SUPPORT FOR TRANSACTIONS; specified in @storage_sql_dsn
--
-- MySQL note ( http://dev.mysql.com/doc/mysql/en/storage-engines.html ):
-- ENGINE is the preferred term, but cannot be used before MySQL 4.0.18.
-- TYPE is available beginning with MySQL 3.23.0, the first version of
-- MySQL for which multiple storage engines were available. If you omit
-- the ENGINE or TYPE option, the default storage engine is used.
-- By default this is MyISAM.
--
-- Please create additional indexes on keys when needed, or drop suggested
-- ones as appropriate to optimize queries needed by a management application.
-- See your database documentation for further optimization hints. With MySQL
-- see Chapter 15 of the reference manual. For example the chapter 15.17 says:
-- InnoDB does not keep an internal count of rows in a table. To process a
-- SELECT COUNT(*) FROM T statement, InnoDB must scan an index of the table,
-- which takes some time if the index is not entirely in the buffer pool.
--
-- Wayne Smith adds: When using MySQL with InnoDB one might want to
-- increase buffer size for both pool and log, and might also want
-- to change flush settings for a little better performance. Example:
-- innodb_buffer_pool_size = 384M
-- innodb_log_buffer_size = 8M
-- innodb_flush_log_at_trx_commit = 0
-- The big performance increase is the first two, the third just helps with
-- lowering disk activity. Consider also adjusting the key_buffer_size.
-- provide unique id for each e-mail address, avoids storing copies
CREATE TABLE maddr (
partition_tag integer DEFAULT 0, -- see $partition_tag
id bigint unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY,
email varbinary(255) NOT NULL, -- full mail address
domain varchar(255) NOT NULL, -- only domain part of the email address
-- with subdomain fields in reverse
CONSTRAINT part_email UNIQUE (partition_tag,email)
) ENGINE=InnoDB;
-- information pertaining to each processed message as a whole;
-- NOTE: records with NULL msgs.content should be ignored by utilities,
-- as such records correspond to messages just being processes, or were lost
-- NOTE: instead of a character field time_iso, one might prefer:
-- time_iso TIMESTAMP NOT NULL DEFAULT 0,
-- but the following MUST then be set in amavisd.conf: $timestamp_fmt_mysql=1
CREATE TABLE msgs (
partition_tag integer DEFAULT 0, -- see $partition_tag
mail_id varbinary(16) NOT NULL, -- long-term unique mail id, dflt 12 ch
secret_id varbinary(16) DEFAULT '', -- authorizes release of mail_id, 12 ch
am_id varchar(20) NOT NULL, -- id used in the log
time_num integer unsigned NOT NULL, -- rx_time: seconds since Unix epoch
time_iso char(16) NOT NULL, -- rx_time: ISO8601 UTC ascii time
sid bigint unsigned NOT NULL, -- sender: maddr.id
policy varchar(255) DEFAULT '', -- policy bank path (like macro %p)
client_addr varchar(255) DEFAULT '', -- SMTP client IP address (IPv4 or v6)
size integer unsigned NOT NULL, -- message size in bytes
originating char(1) DEFAULT ' ' NOT NULL, -- sender from inside or auth'd
content char(1), -- content type: V/B/U/S/Y/M/H/O/T/C
-- virus/banned/unchecked/spam(kill)/spammy(tag2)/
-- /bad-mime/bad-header/oversized/mta-err/clean
-- is NULL on partially processed mail
-- (prior to 2.7.0 the CC_SPAMMY was logged as 's', now 'Y' is used;
-- to avoid a need for case-insenstivity in queries)
quar_type char(1), -- quarantined as: ' '/F/Z/B/Q/M/L
-- none/file/zipfile/bsmtp/sql/
-- /mailbox(smtp)/mailbox(lmtp)
quar_loc varbinary(255) DEFAULT '', -- quarantine location (e.g. file)
dsn_sent char(1), -- was DSN sent? Y/N/q (q=quenched)
spam_level float, -- SA spam level (no boosts)
message_id varchar(255) DEFAULT '', -- mail Message-ID header field
from_addr varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '',
-- mail From header field, UTF8
subject varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '',
-- mail Subject header field, UTF8
host varchar(255) NOT NULL, -- hostname where amavisd is running
PRIMARY KEY (partition_tag,mail_id),
FOREIGN KEY (sid) REFERENCES maddr(id) ON DELETE RESTRICT
) ENGINE=InnoDB;
CREATE INDEX msgs_idx_sid ON msgs (sid);
CREATE INDEX msgs_idx_mess_id ON msgs (message_id); -- useful with pen pals
CREATE INDEX msgs_idx_time_num ON msgs (time_num);
-- alternatively when purging based on time_iso (instead of msgs_idx_time_num):
CREATE INDEX msgs_idx_time_iso ON msgs (time_iso);
-- When using FOREIGN KEY contraints, InnoDB requires index on a field
-- (an the field must be the first field in the index). Hence create it:
CREATE INDEX msgs_idx_mail_id ON msgs (mail_id);
-- per-recipient information related to each processed message;
-- NOTE: records in msgrcpt without corresponding msgs.mail_id record are
-- orphaned and should be ignored and eventually deleted by external utilities
CREATE TABLE msgrcpt (
partition_tag integer DEFAULT 0, -- see $partition_tag
mail_id varbinary(16) NOT NULL, -- (must allow duplicates)
rseqnum integer DEFAULT 0 NOT NULL, -- recip's enumeration within msg
rid bigint unsigned NOT NULL, -- recipient: maddr.id (dupl. allowed)
is_local char(1) DEFAULT ' ' NOT NULL, -- recip is: Y=local, N=foreign
content char(1) DEFAULT ' ' NOT NULL, -- content type V/B/U/S/Y/M/H/O/T/C
ds char(1) NOT NULL, -- delivery status: P/R/B/D/T
-- pass/reject/bounce/discard/tempfail
rs char(1) NOT NULL, -- release status: initialized to ' '
bl char(1) DEFAULT ' ', -- sender blacklisted by this recip
wl char(1) DEFAULT ' ', -- sender whitelisted by this recip
bspam_level float, -- per-recipient (total) spam level
smtp_resp varchar(255) DEFAULT '', -- SMTP response given to MTA
PRIMARY KEY (partition_tag,mail_id,rseqnum),
FOREIGN KEY (rid) REFERENCES maddr(id) ON DELETE RESTRICT,
FOREIGN KEY (mail_id) REFERENCES msgs(mail_id) ON DELETE CASCADE
) ENGINE=InnoDB;
CREATE INDEX msgrcpt_idx_mail_id ON msgrcpt (mail_id);
CREATE INDEX msgrcpt_idx_rid ON msgrcpt (rid);
-- Additional index on rs since Modoboa uses it to filter its quarantine
CREATE INDEX msgrcpt_idx_rs ON msgrcpt (rs);
-- mail quarantine in SQL, enabled by $*_quarantine_method='sql:'
-- NOTE: records in quarantine without corresponding msgs.mail_id record are
-- orphaned and should be ignored and eventually deleted by external utilities
CREATE TABLE quarantine (
partition_tag integer DEFAULT 0, -- see $partition_tag
mail_id varbinary(16) NOT NULL, -- long-term unique mail id
chunk_ind integer unsigned NOT NULL, -- chunk number, starting with 1
mail_text blob NOT NULL, -- store mail as chunks of octets
PRIMARY KEY (partition_tag,mail_id,chunk_ind),
FOREIGN KEY (mail_id) REFERENCES msgs(mail_id) ON DELETE CASCADE
) ENGINE=InnoDB;

View File

@@ -1,189 +0,0 @@
CREATE TABLE policy (
id serial PRIMARY KEY, -- 'id' is the _only_ required field
policy_name varchar(32), -- not used by amavisd-new, a comment
virus_lover char(1) default NULL, -- Y/N
spam_lover char(1) default NULL, -- Y/N
unchecked_lover char(1) default NULL, -- Y/N
banned_files_lover char(1) default NULL, -- Y/N
bad_header_lover char(1) default NULL, -- Y/N
bypass_virus_checks char(1) default NULL, -- Y/N
bypass_spam_checks char(1) default NULL, -- Y/N
bypass_banned_checks char(1) default NULL, -- Y/N
bypass_header_checks char(1) default NULL, -- Y/N
virus_quarantine_to varchar(64) default NULL,
spam_quarantine_to varchar(64) default NULL,
banned_quarantine_to varchar(64) default NULL,
unchecked_quarantine_to varchar(64) default NULL,
bad_header_quarantine_to varchar(64) default NULL,
clean_quarantine_to varchar(64) default NULL,
archive_quarantine_to varchar(64) default NULL,
spam_tag_level real default NULL, -- higher score inserts spam info headers
spam_tag2_level real default NULL, -- inserts 'declared spam' header fields
spam_tag3_level real default NULL, -- inserts 'blatant spam' header fields
spam_kill_level real default NULL, -- higher score triggers evasive actions
-- e.g. reject/drop, quarantine, ...
-- (subject to final_spam_destiny setting)
spam_dsn_cutoff_level real default NULL,
spam_quarantine_cutoff_level real default NULL,
addr_extension_virus varchar(64) default NULL,
addr_extension_spam varchar(64) default NULL,
addr_extension_banned varchar(64) default NULL,
addr_extension_bad_header varchar(64) default NULL,
warnvirusrecip char(1) default NULL, -- Y/N
warnbannedrecip char(1) default NULL, -- Y/N
warnbadhrecip char(1) default NULL, -- Y/N
newvirus_admin varchar(64) default NULL,
virus_admin varchar(64) default NULL,
banned_admin varchar(64) default NULL,
bad_header_admin varchar(64) default NULL,
spam_admin varchar(64) default NULL,
spam_subject_tag varchar(64) default NULL,
spam_subject_tag2 varchar(64) default NULL,
spam_subject_tag3 varchar(64) default NULL,
message_size_limit integer default NULL, -- max size in bytes, 0 disable
banned_rulenames varchar(64) default NULL, -- comma-separated list of ...
-- names mapped through %banned_rules to actual banned_filename tables
disclaimer_options varchar(64) default NULL,
forward_method varchar(64) default NULL,
sa_userconf varchar(64) default NULL,
sa_username varchar(64) default NULL
);
-- local users
CREATE TABLE users (
id serial PRIMARY KEY, -- unique id
priority integer NOT NULL DEFAULT 7, -- sort field, 0 is low prior.
policy_id integer NOT NULL DEFAULT 1 CHECK (policy_id >= 0) REFERENCES policy(id),
email bytea NOT NULL UNIQUE, -- email address, non-rfc2822-quoted
fullname varchar(255) DEFAULT NULL -- not used by amavisd-new
-- local char(1) -- Y/N (optional, see SQL section in README.lookups)
);
-- any e-mail address (non- rfc2822-quoted), external or local,
-- used as senders in wblist
CREATE TABLE mailaddr (
id serial PRIMARY KEY,
priority integer NOT NULL DEFAULT 9, -- 0 is low priority
email bytea NOT NULL UNIQUE
);
-- per-recipient whitelist and/or blacklist,
-- puts sender and recipient in relation wb (white or blacklisted sender)
CREATE TABLE wblist (
rid integer NOT NULL CHECK (rid >= 0) REFERENCES users(id),
sid integer NOT NULL CHECK (sid >= 0) REFERENCES mailaddr(id),
wb varchar(10) NOT NULL, -- W or Y / B or N / space=neutral / score
PRIMARY KEY (rid,sid)
);
-- grant usage rights:
GRANT select ON policy TO amavis;
GRANT select ON users TO amavis;
GRANT select ON mailaddr TO amavis;
GRANT select ON wblist TO amavis;
-- R/W part of the dataset (optional)
-- May reside in the same or in a separate database as lookups database;
-- REQUIRES SUPPORT FOR TRANSACTIONS; specified in @storage_sql_dsn
--
-- Please create additional indexes on keys when needed, or drop suggested
-- ones as appropriate to optimize queries needed by a management application.
-- See your database documentation for further optimization hints.
-- provide unique id for each e-mail address, avoids storing copies
CREATE TABLE maddr (
id serial PRIMARY KEY,
partition_tag integer DEFAULT 0, -- see $partition_tag
email bytea NOT NULL, -- full e-mail address
domain varchar(255) NOT NULL, -- only domain part of the email address
-- with subdomain fields in reverse
CONSTRAINT part_email UNIQUE (partition_tag,email)
);
-- information pertaining to each processed message as a whole;
-- NOTE: records with a NULL msgs.content should be ignored by utilities,
-- as such records correspond to messages just being processed, or were lost
CREATE TABLE msgs (
partition_tag integer DEFAULT 0, -- see $partition_tag
mail_id bytea NOT NULL, -- long-term unique mail id, dflt 12 ch
secret_id bytea DEFAULT '', -- authorizes release of mail_id, 12 ch
am_id varchar(20) NOT NULL, -- id used in the log
time_num integer NOT NULL CHECK (time_num >= 0),
-- rx_time: seconds since Unix epoch
time_iso timestamp WITH TIME ZONE NOT NULL,-- rx_time: ISO8601 UTC ascii time
sid integer NOT NULL CHECK (sid >= 0), -- sender: maddr.id
policy varchar(255) DEFAULT '', -- policy bank path (like macro %p)
client_addr varchar(255) DEFAULT '', -- SMTP client IP address (IPv4 or v6)
size integer NOT NULL CHECK (size >= 0), -- message size in bytes
originating char(1) DEFAULT ' ' NOT NULL, -- sender from inside or auth'd
content char(1), -- content type: V/B/U/S/Y/M/H/O/T/C
-- virus/banned/unchecked/spam(kill)/spammy(tag2)/
-- /bad-mime/bad-header/oversized/mta-err/clean
-- is NULL on partially processed mail
-- (prior to 2.7.0 the CC_SPAMMY was logged as 's', now 'Y' is used;
--- to avoid a need for case-insenstivity in queries)
quar_type char(1), -- quarantined as: ' '/F/Z/B/Q/M/L
-- none/file/zipfile/bsmtp/sql/
-- /mailbox(smtp)/mailbox(lmtp)
quar_loc varchar(255) DEFAULT '', -- quarantine location (e.g. file)
dsn_sent char(1), -- was DSN sent? Y/N/q (q=quenched)
spam_level real, -- SA spam level (no boosts)
message_id varchar(255) DEFAULT '', -- mail Message-ID header field
from_addr varchar(255) DEFAULT '', -- mail From header field, UTF8
subject varchar(255) DEFAULT '', -- mail Subject header field, UTF8
host varchar(255) NOT NULL, -- hostname where amavisd is running
CONSTRAINT msgs_partition_mail UNIQUE (partition_tag,mail_id),
PRIMARY KEY (partition_tag,mail_id)
--FOREIGN KEY (sid) REFERENCES maddr(id) ON DELETE RESTRICT
);
CREATE INDEX msgs_idx_sid ON msgs (sid);
CREATE INDEX msgs_idx_mess_id ON msgs (message_id); -- useful with pen pals
CREATE INDEX msgs_idx_time_iso ON msgs (time_iso);
CREATE INDEX msgs_idx_time_num ON msgs (time_num); -- optional
-- per-recipient information related to each processed message;
-- NOTE: records in msgrcpt without corresponding msgs.mail_id record are
-- orphaned and should be ignored and eventually deleted by external utilities
CREATE TABLE msgrcpt (
partition_tag integer DEFAULT 0, -- see $partition_tag
mail_id bytea NOT NULL, -- (must allow duplicates)
rseqnum integer DEFAULT 0 NOT NULL, -- recip's enumeration within msg
rid integer NOT NULL, -- recipient: maddr.id (duplicates allowed)
is_local char(1) DEFAULT ' ' NOT NULL, -- recip is: Y=local, N=foreign
content char(1) DEFAULT ' ' NOT NULL, -- content type V/B/U/S/Y/M/H/O/T/C
ds char(1) NOT NULL, -- delivery status: P/R/B/D/T
-- pass/reject/bounce/discard/tempfail
rs char(1) NOT NULL, -- release status: initialized to ' '
bl char(1) DEFAULT ' ', -- sender blacklisted by this recip
wl char(1) DEFAULT ' ', -- sender whitelisted by this recip
bspam_level real, -- per-recipient (total) spam level
smtp_resp varchar(255) DEFAULT '', -- SMTP response given to MTA
CONSTRAINT msgrcpt_partition_mail_rseq UNIQUE (partition_tag,mail_id,rseqnum),
PRIMARY KEY (partition_tag,mail_id,rseqnum)
--FOREIGN KEY (rid) REFERENCES maddr(id) ON DELETE RESTRICT,
--FOREIGN KEY (mail_id) REFERENCES msgs(mail_id) ON DELETE CASCADE
);
CREATE INDEX msgrcpt_idx_mail_id ON msgrcpt (mail_id);
CREATE INDEX msgrcpt_idx_rid ON msgrcpt (rid);
-- Additional index on rs since Modoboa uses it to filter its quarantine
CREATE INDEX msgrcpt_idx_rs ON msgrcpt (rs);
-- mail quarantine in SQL, enabled by $*_quarantine_method='sql:'
-- NOTE: records in quarantine without corresponding msgs.mail_id record are
-- orphaned and should be ignored and eventually deleted by external utilities
CREATE TABLE quarantine (
partition_tag integer DEFAULT 0, -- see $partition_tag
mail_id bytea NOT NULL, -- long-term unique mail id
chunk_ind integer NOT NULL CHECK (chunk_ind >= 0), -- chunk number, 1..
mail_text bytea NOT NULL, -- store mail as chunks of octects
PRIMARY KEY (partition_tag,mail_id,chunk_ind)
--FOREIGN KEY (mail_id) REFERENCES msgs(mail_id) ON DELETE CASCADE
);

View File

@@ -2,9 +2,6 @@
provider = %domain provider = %domain
domains = * domains = *
#debug=yes
#logfile = /srv/automx/automx.log
# Protect against DoS # Protect against DoS
memcache = 127.0.0.1:11211 memcache = 127.0.0.1:11211
memcache_ttl = 600 memcache_ttl = 600
@@ -19,8 +16,6 @@ host = %sql_dsn
query = %sql_query query = %sql_query
result_attrs = display_name, email result_attrs = display_name, email
display_name = ${display_name}
smtp = yes smtp = yes
smtp_server = %hostname smtp_server = %hostname
smtp_port = 587 smtp_port = 587
@@ -37,3 +32,10 @@ imap_encryption = starttls
imap_auth = plaintext imap_auth = plaintext
imap_auth_identity = ${email} imap_auth_identity = ${email}
imap_refresh_ttl = 6 imap_refresh_ttl = 6
pop = yes
pop_server = %hostname
pop_port = 110
pop_encryption = starttls
pop_auth = plaintext
pop_auth_identity = ${email}

View File

@@ -96,7 +96,7 @@ auth_master_user_separator = *
# plain login digest-md5 cram-md5 ntlm rpa apop anonymous gssapi otp skey # plain login digest-md5 cram-md5 ntlm rpa apop anonymous gssapi otp skey
# gss-spnego # gss-spnego
# NOTE: See also disable_plaintext_auth setting. # NOTE: See also disable_plaintext_auth setting.
auth_mechanisms = plain login oauthbearer xoauth2 auth_mechanisms = plain login
## ##
## Password and user databases ## Password and user databases
@@ -120,7 +120,6 @@ auth_mechanisms = plain login oauthbearer xoauth2
#!include auth-system.conf.ext #!include auth-system.conf.ext
!include auth-sql.conf.ext !include auth-sql.conf.ext
!include auth-oauth2.conf.ext
#!include auth-ldap.conf.ext #!include auth-ldap.conf.ext
#!include auth-passwdfile.conf.ext #!include auth-passwdfile.conf.ext
#!include auth-checkpassword.conf.ext #!include auth-checkpassword.conf.ext

View File

@@ -92,14 +92,14 @@ service postlogin {
service stats { service stats {
# To allow modoboa to access available cipher list. # To allow modoboa to access available cipher list.
unix_listener stats-reader { unix_listener stats-reader {
user = %{mailboxes_owner} user = vmail
group = %{mailboxes_owner} group = vmail
mode = 0660 mode = 0660
} }
unix_listener stats-writer { unix_listener stats-writer {
user = %{mailboxes_owner} user = vmail
group = %{mailboxes_owner} group = vmail
mode = 0660 mode = 0660
} }
} }
@@ -120,7 +120,7 @@ service auth {
# permissions (e.g. 0777 allows everyone full permissions). # permissions (e.g. 0777 allows everyone full permissions).
unix_listener auth-userdb { unix_listener auth-userdb {
#mode = 0666 #mode = 0666
user = %{mailboxes_owner} user = vmail
#group = #group =
} }
@@ -131,6 +131,13 @@ service auth {
group = postfix group = postfix
} }
# Radicale auth
%{radicale_enabled}unix_listener %{radicale_auth_socket_path} {
%{radicale_enabled} mode = 0666
%{radicale_enabled} user = %{radicale_user}
%{radicale_enabled} group = %{radicale_user}
%{radicale_enabled}}
# Auth process is run as this user. # Auth process is run as this user.
#user = $default_internal_user #user = $default_internal_user
} }
@@ -147,7 +154,7 @@ service dict {
# For example: mode=0660, group=vmail and global mail_access_groups=vmail # For example: mode=0660, group=vmail and global mail_access_groups=vmail
unix_listener dict { unix_listener dict {
mode = 0600 mode = 0600
user = %{mailboxes_owner} user = vmail
#group = #group =
} }
} }

View File

@@ -38,7 +38,7 @@ plugin {
# Identical to sieve_before, only the specified scripts are executed after the # Identical to sieve_before, only the specified scripts are executed after the
# user's script (only when keep is still in effect!). Multiple script file or # user's script (only when keep is still in effect!). Multiple script file or
# directory paths can be specified by appending an increasing number. # directory paths can be specified by appending an increasing number.
%{do_move_spam_to_junk}sieve_after = /etc/dovecot/conf.d/custom_after_sieve #sieve_after =
#sieve_after2 = #sieve_after2 =
#sieve_after2 = (etc...) #sieve_after2 = (etc...)

View File

@@ -1,5 +0,0 @@
passdb {
driver = oauth2
mechanisms = xoauth2 oauthbearer
args = /etc/dovecot/conf.d/dovecot-oauth2.conf.ext
}

View File

@@ -1,4 +0,0 @@
require "fileinto";
if header :contains "X-Spam-Status" "Yes" {
fileinto "Junk";
}

View File

@@ -1,6 +0,0 @@
introspection_mode = post
introspection_url = %{oauth2_introspection_url}
username_attribute = username
tls_ca_cert_file = /etc/ssl/certs/ca-certificates.crt
active_attribute = active
active_value = true

View File

@@ -123,8 +123,7 @@ connect = host=%dbhost port=%dbport dbname=%modoboa_dbname user=%modoboa_dbuser
#user_query = \ #user_query = \
# SELECT home, uid, gid \ # SELECT home, uid, gid \
# FROM users WHERE username = '%%n' AND domain = '%%d' # FROM users WHERE username = '%%n' AND domain = '%%d'
%{not_modoboa_2_2_or_greater}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.address='%%n' AND dom.name='%%d' 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.address='%%n' AND dom.name='%%d'
%{modoboa_2_2_or_greater}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'
# If you wish to avoid two SQL lookups (passdb + userdb), you can use # If you wish to avoid two SQL lookups (passdb + userdb), you can use
# userdb prefetch instead of userdb sql in dovecot.conf. In that case you'll # userdb prefetch instead of userdb sql in dovecot.conf. In that case you'll
@@ -134,8 +133,7 @@ connect = host=%dbhost port=%dbport dbname=%modoboa_dbname user=%modoboa_dbuser
# SELECT userid AS user, password, \ # SELECT userid AS user, password, \
# home AS userdb_home, uid AS userdb_uid, gid AS userdb_gid \ # home AS userdb_home, uid AS userdb_uid, gid AS userdb_gid \
# FROM users WHERE userid = '%%u' # FROM users WHERE userid = '%%u'
%{not_modoboa_2_2_or_greater}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 u.email='%%u' AND u.is_active=1 AND dom.enabled=1 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 u.email='%%u' AND u.is_active=1 AND dom.enabled=1
%{modoboa_2_2_or_greater}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
# Query to get a list of all usernames. # Query to get a list of all usernames.
#iterate_query = SELECT username AS user FROM users #iterate_query = SELECT username AS user FROM users

View File

@@ -123,8 +123,7 @@ connect = host=%dbhost port=%dbport dbname=%modoboa_dbname user=%modoboa_dbuser
#user_query = \ #user_query = \
# SELECT home, uid, gid \ # SELECT home, uid, gid \
# FROM users WHERE username = '%%n' AND domain = '%%d' # FROM users WHERE username = '%%n' AND domain = '%%d'
%{not_modoboa_2_2_or_greater}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.address='%%n' AND dom.name='%%d' 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.address='%%n' AND dom.name='%%d'
%{modoboa_2_2_or_greater}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'
# If you wish to avoid two SQL lookups (passdb + userdb), you can use # If you wish to avoid two SQL lookups (passdb + userdb), you can use
# userdb prefetch instead of userdb sql in dovecot.conf. In that case you'll # userdb prefetch instead of userdb sql in dovecot.conf. In that case you'll
@@ -134,8 +133,7 @@ connect = host=%dbhost port=%dbport dbname=%modoboa_dbname user=%modoboa_dbuser
# SELECT userid AS user, password, \ # SELECT userid AS user, password, \
# home AS userdb_home, uid AS userdb_uid, gid AS userdb_gid \ # home AS userdb_home, uid AS userdb_uid, gid AS userdb_gid \
# FROM users WHERE userid = '%%u' # FROM users WHERE userid = '%%u'
%{not_modoboa_2_2_or_greater}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 email='%%u' AND is_active AND dom.enabled 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 email='%%u' AND is_active AND dom.enabled
%{modoboa_2_2_or_greater}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
# Query to get a list of all usernames. # Query to get a list of all usernames.
#iterate_query = SELECT username AS user FROM users #iterate_query = SELECT username AS user FROM users

View File

@@ -1,9 +0,0 @@
# Fail2Ban filter Modoboa authentication
[INCLUDES]
before = common.conf
[Definition]
failregex = modoboa\.auth: WARNING Failed connection attempt from \'<HOST>\' as user \'.*?\'$

View File

@@ -1,9 +0,0 @@
[modoboa]
enabled = true
port = http,https
protocol = tcp
filter = modoboa-auth
maxretry = %max_retry
bantime = %ban_time
findtime = %find_time
logpath = /var/log/auth.log

View File

@@ -3,7 +3,6 @@
# #
PYTHON=%{venv_path}/bin/python PYTHON=%{venv_path}/bin/python
INSTANCE=%{instance_path} INSTANCE=%{instance_path}
MAILTO=%{cron_error_recipient}
# Operations on mailboxes # Operations on mailboxes
%{dovecot_enabled}* * * * * %{dovecot_mailboxes_owner} $PYTHON $INSTANCE/manage.py handle_mailbox_operations %{dovecot_enabled}* * * * * %{dovecot_mailboxes_owner} $PYTHON $INSTANCE/manage.py handle_mailbox_operations
@@ -34,4 +33,4 @@ MAILTO=%{cron_error_recipient}
%{minutes} %{hours} * * * root $PYTHON $INSTANCE/manage.py communicate_with_public_api %{minutes} %{hours} * * * root $PYTHON $INSTANCE/manage.py communicate_with_public_api
# Generate DKIM keys (they will belong to the user running this job) # Generate DKIM keys (they will belong to the user running this job)
%{dkim_cron_enabled}* * * * * %{opendkim_user} umask 077 && $PYTHON $INSTANCE/manage.py modo manage_dkim_keys %{opendkim_enabled}* * * * * %{opendkim_user} umask 077 && $PYTHON $INSTANCE/manage.py modo manage_dkim_keys

View File

@@ -1,9 +0,0 @@
[program:modoboa-base-worker]
autostart=true
autorestart=true
command=%{venv_path}/bin/python %{home_dir}/instance/manage.py rqworker modoboa
directory=%{home_dir}
user=%{user}
redirect_stderr=true
numprocs=1
stopsignal=TERM

View File

@@ -1,9 +0,0 @@
[program:modoboa-dkim-worker]
autostart=true
autorestart=true
command=%{venv_path}/bin/python %{home_dir}/instance/manage.py rqworker dkim
directory=%{home_dir}
user=%{dkim_user}
redirect_stderr=true
numprocs=1
stopsignal=TERM

View File

@@ -6,4 +6,3 @@ directory=%{home_dir}
redirect_stderr=true redirect_stderr=true
user=%{user} user=%{user}
numprocs=1 numprocs=1

View File

@@ -10,8 +10,8 @@ server {
} }
server { server {
listen 443 ssl http2; listen 443 ssl;
listen [::]:443 ssl http2; listen [::]:443 ssl;
server_name %hostname; server_name %hostname;
root %app_instance_path; root %app_instance_path;
@@ -37,20 +37,7 @@ server {
try_files $uri $uri/ =404; try_files $uri $uri/ =404;
} }
%{rspamd_enabled} location /rspamd/ { location ^~ /new-admin {
%{rspamd_enabled} proxy_pass http://localhost:11334/;
%{rspamd_enabled}
%{rspamd_enabled} proxy_set_header Host $host;
%{rspamd_enabled} proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
%{rspamd_enabled} }
location ~ ^/(api|accounts) {
include uwsgi_params;
uwsgi_param UWSGI_SCRIPT instance.wsgi:application;
uwsgi_pass modoboa;
}
location / {
alias %{app_instance_path}/frontend/; alias %{app_instance_path}/frontend/;
index index.html; index index.html;
@@ -61,5 +48,10 @@ server {
try_files $uri $uri/ /index.html = 404; try_files $uri $uri/ /index.html = 404;
} }
location / {
include uwsgi_params;
uwsgi_param UWSGI_SCRIPT instance.wsgi:application;
uwsgi_pass modoboa;
}
%{extra_config} %{extra_config}
} }

View File

@@ -1,11 +0,0 @@
if /^\s*Received:.*Authenticated sender.*\(Postfix\)/
/^Received: from .*? \([\w\-.]* \[.*?\]\)(.*|\n.*)\(Authenticated sender: (.+)\)\s+by.+\(Postfix\) with (.*)/
REPLACE Received: from [127.0.0.1] (localhost [127.0.0.1]) by localhost (Mailerdaemon) with $3
endif
if /^\s*Received: from .*rspamd.localhost .*\(Postfix\)/
/^Received: from.* (.*|\n.*)\((.+) (.+)\)\s+by (.+) \(Postfix\) with (.*)/
REPLACE Received: from rspamd (rspamd $3) by $4 (Postfix) with $5
endif
/^\s*X-Enigmail/ IGNORE
/^\s*X-Originating-IP/ IGNORE
/^\s*X-Forward/ IGNORE

View File

@@ -41,29 +41,22 @@ smtpd_tls_auth_only = no
smtpd_tls_CApath = /etc/ssl/certs smtpd_tls_CApath = /etc/ssl/certs
smtpd_tls_key_file = %tls_key_file smtpd_tls_key_file = %tls_key_file
smtpd_tls_cert_file = %tls_cert_file smtpd_tls_cert_file = %tls_cert_file
smtpd_tls_dh1024_param_file = ${config_directory}/ffdhe%{dhe_group}.pem smtpd_tls_dh1024_param_file = ${config_directory}/dh2048.pem
smtpd_tls_loglevel = 1 smtpd_tls_loglevel = 1
smtpd_tls_session_cache_database = btree:$data_directory/smtpd_tls_session_cache smtpd_tls_session_cache_database = btree:$data_directory/smtpd_tls_session_cache
smtpd_tls_security_level = may smtpd_tls_security_level = may
smtpd_tls_received_header = yes smtpd_tls_received_header = yes
# Disallow SSLv2 and SSLv3, only accept secure ciphers # Disallow SSLv2 and SSLv3, only accept secure ciphers
smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 smtpd_tls_protocols = !SSLv2, !SSLv3
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3
smtpd_tls_mandatory_ciphers = high smtpd_tls_mandatory_ciphers = high
smtpd_tls_mandatory_exclude_ciphers = aNULL, eNULL, EXPORT, DES, RC4, MD5, PSK, aECDH, EDH-DSS-DES-CBC3-SHA, EDH-RSA-DES-CBC3-SHA, KRB5-DES, CBC3-SHA, CAMELLIA, SEED-SHA, AES256-SHA, AES256-SHA256, AES256-GCM-SHA384, AES128-SHA, AES128-SHA256, AES128-GCM-SHA256, DHE-RSA-AES128-GCM-SHA256, DHE-RSA-AES128-SHA, DHE-RSA-AES128-SHA256, DHE-RSA-AES256-GCM-SHA384, DHE-RSA-AES256-SHA, DHE-RSA-AES256-SHA256, DHE-RSA-CHACHA20-POLY1305, ECDHE-RSA-AES128-SHA, ECDHE-RSA-AES256-SHA smtpd_tls_mandatory_exclude_ciphers = aNULL, MD5 , DES, ADH, RC4, PSD, SRP, 3DES, eNULL
smtpd_tls_exclude_ciphers = aNULL, eNULL, EXPORT, DES, RC4, MD5, PSK, aECDH, EDH-DSS-DES-CBC3-SHA, EDH-RSA-DES-CBC3-SHA, KRB5-DES, CBC3-SHA, CAMELLIA, SEED-SHA, AES256-SHA, AES256-SHA256, AES256-GCM-SHA384, AES128-SHA, AES128-SHA256, AES128-GCM-SHA256, DHE-RSA-AES128-GCM-SHA256, DHE-RSA-AES128-SHA, DHE-RSA-AES128-SHA256, DHE-RSA-AES256-GCM-SHA384, DHE-RSA-AES256-SHA, DHE-RSA-AES256-SHA256, DHE-RSA-CHACHA20-POLY1305, ECDHE-RSA-AES128-SHA, ECDHE-RSA-AES256-SHA smtpd_tls_exclude_ciphers = aNULL, MD5 , DES, ADH, RC4, PSD, SRP, 3DES, eNULL
tls_preempt_cipherlist = yes
tls_ssl_options = NO_COMPRESSION
# Enable elliptic curve cryptography # Enable elliptic curve cryptography
smtpd_tls_eecdh_grade = strong smtpd_tls_eecdh_grade = strong
# SMTP Smuggling prevention
# See https://www.postfix.org/smtp-smuggling.html
smtpd_data_restrictions = reject_unauth_pipelining
smtpd_forbid_unauth_pipelining = yes
# Use TLS if this is supported by the remote SMTP server, otherwise use plaintext. # Use TLS if this is supported by the remote SMTP server, otherwise use plaintext.
smtp_tls_CApath = /etc/ssl/certs smtp_tls_CApath = /etc/ssl/certs
smtp_tls_security_level = may smtp_tls_security_level = may
@@ -74,10 +67,10 @@ smtp_tls_exclude_ciphers = EXPORT, LOW
# #
%{dovecot_enabled}virtual_transport = lmtp:unix:private/dovecot-lmtp %{dovecot_enabled}virtual_transport = lmtp:unix:private/dovecot-lmtp
%{dovecot_enabled}virtual_mailbox_domains = proxy:%{db_driver}:/etc/postfix/sql-domains.cf virtual_mailbox_domains = proxy:%{db_driver}:/etc/postfix/sql-domains.cf
%{dovecot_enabled}virtual_alias_domains = proxy:%{db_driver}:/etc/postfix/sql-domain-aliases.cf virtual_alias_domains = proxy:%{db_driver}:/etc/postfix/sql-domain-aliases.cf
%{dovecot_enabled}virtual_alias_maps = virtual_alias_maps =
%{dovecot_enabled} proxy:%{db_driver}:/etc/postfix/sql-aliases.cf proxy:%{db_driver}:/etc/postfix/sql-aliases.cf
## Relay domains ## Relay domains
# #
@@ -122,19 +115,10 @@ strict_rfc821_envelopes = yes
%{opendkim_enabled}milter_default_action = accept %{opendkim_enabled}milter_default_action = accept
%{opendkim_enabled}milter_content_timeout = 30s %{opendkim_enabled}milter_content_timeout = 30s
# Rspamd setup
%{rspamd_enabled}smtpd_milters = inet:localhost:11332
%{rspamd_enabled}non_smtpd_milters = inet:localhost:11332
%{rspamd_enabled}milter_default_action = accept
%{rspamd_enabled}milter_protocol = 6
# List of authorized senders # List of authorized senders
smtpd_sender_login_maps = smtpd_sender_login_maps =
proxy:%{db_driver}:/etc/postfix/sql-sender-login-map.cf proxy:%{db_driver}:/etc/postfix/sql-sender-login-map.cf
# Add authenticated header to hide public client IP
smtpd_sasl_authenticated_header = yes
# Recipient restriction rules # Recipient restriction rules
smtpd_recipient_restrictions = smtpd_recipient_restrictions =
check_policy_service inet:127.0.0.1:9999 check_policy_service inet:127.0.0.1:9999
@@ -151,27 +135,28 @@ smtpd_recipient_restrictions =
## Postcreen settings ## Postcreen settings
# #
%{rspamd_disabled}postscreen_access_list = postscreen_access_list =
%{rspamd_disabled} permit_mynetworks permit_mynetworks
%{rspamd_disabled} cidr:/etc/postfix/postscreen_spf_whitelist.cidr cidr:/etc/postfix/postscreen_spf_whitelist.cidr
%{rspamd_disabled}postscreen_blacklist_action = enforce postscreen_blacklist_action = enforce
# Use some DNSBL # Use some DNSBL
%{rspamd_disabled}postscreen_dnsbl_sites = postscreen_dnsbl_sites =
%{rspamd_disabled} zen.spamhaus.org=127.0.0.[2..11]*3 zen.spamhaus.org=127.0.0.[2..11]*3
%{rspamd_disabled} bl.spameatingmonkey.net=127.0.0.2*2 bl.spameatingmonkey.net=127.0.0.2*2
%{rspamd_disabled} bl.spamcop.net=127.0.0.2 bl.spamcop.net=127.0.0.2
%{rspamd_disabled}postscreen_dnsbl_threshold = 3 dnsbl.sorbs.net=127.0.0.[2..15]
%{rspamd_disabled}postscreen_dnsbl_action = enforce postscreen_dnsbl_threshold = 3
postscreen_dnsbl_action = enforce
%{rspamd_disabled}postscreen_greet_banner = Welcome, please wait... postscreen_greet_banner = Welcome, please wait...
%{rspamd_disabled}postscreen_greet_action = enforce postscreen_greet_action = enforce
%{rspamd_disabled}postscreen_pipelining_enable = yes postscreen_pipelining_enable = yes
%{rspamd_disabled}postscreen_pipelining_action = enforce postscreen_pipelining_action = enforce
%{rspamd_disabled}postscreen_non_smtp_command_enable = yes postscreen_non_smtp_command_enable = yes
%{rspamd_disabled}postscreen_non_smtp_command_action = enforce postscreen_non_smtp_command_action = enforce
%{rspamd_disabled}postscreen_bare_newline_enable = yes postscreen_bare_newline_enable = yes
%{rspamd_disabled}postscreen_bare_newline_action = enforce postscreen_bare_newline_action = enforce

View File

@@ -9,8 +9,7 @@
# service type private unpriv chroot wakeup maxproc command + args # service type private unpriv chroot wakeup maxproc command + args
# (yes) (yes) (yes) (never) (100) # (yes) (yes) (yes) (never) (100)
# ========================================================================== # ==========================================================================
%{rspamd_disabled}smtp inet n - - - 1 postscreen smtp inet n - - - 1 postscreen
%{rspamd_enabled}smtp inet n - - - - smtpd
smtpd pass - - - - - smtpd smtpd pass - - - - - smtpd
%{amavis_enabled} -o smtpd_proxy_filter=inet:[127.0.0.1]:10024 %{amavis_enabled} -o smtpd_proxy_filter=inet:[127.0.0.1]:10024
%{amavis_enabled} -o smtpd_proxy_options=speed_adjust %{amavis_enabled} -o smtpd_proxy_options=speed_adjust
@@ -27,7 +26,6 @@ submission inet n - - - - smtpd
-o smtpd_helo_restrictions= -o smtpd_helo_restrictions=
-o smtpd_sender_restrictions=reject_sender_login_mismatch -o smtpd_sender_restrictions=reject_sender_login_mismatch
-o milter_macro_daemon_name=ORIGINATING -o milter_macro_daemon_name=ORIGINATING
-o cleanup_service_name=ascleanup
%{amavis_enabled} -o smtpd_proxy_filter=inet:[127.0.0.1]:10026 %{amavis_enabled} -o smtpd_proxy_filter=inet:[127.0.0.1]:10026
#smtps inet n - - - - smtpd #smtps inet n - - - - smtpd
# -o syslog_name=postfix/smtps # -o syslog_name=postfix/smtps
@@ -43,8 +41,6 @@ submission inet n - - - - smtpd
#628 inet n - - - - qmqpd #628 inet n - - - - qmqpd
pickup unix n - - 60 1 pickup pickup unix n - - 60 1 pickup
cleanup unix n - - - 0 cleanup cleanup unix n - - - 0 cleanup
ascleanup unix n - - - 0 cleanup
-o header_checks=pcre:/etc/postfix/anonymize_headers.pcre
qmgr unix n - n 300 1 qmgr qmgr unix n - n 300 1 qmgr
#qmgr unix n - n 300 1 oqmgr #qmgr unix n - n 300 1 oqmgr
tlsmgr unix - - - 1000? 1 tlsmgr tlsmgr unix - - - 1000? 1 tlsmgr
@@ -82,7 +78,7 @@ scache unix - - - - 1 scache
# Also specify in main.cf: maildrop_destination_recipient_limit=1 # Also specify in main.cf: maildrop_destination_recipient_limit=1
# #
maildrop unix - n n - - pipe maildrop unix - n n - - pipe
flags=DRhu user=%{dovecot_mailboxes_owner} argv=/usr/bin/maildrop -d ${recipient} flags=DRhu user=vmail argv=/usr/bin/maildrop -d ${recipient}
# #
# ==================================================================== # ====================================================================
# #
@@ -128,6 +124,11 @@ mailman unix - n n - - pipe
flags=FR user=list argv=/usr/lib/mailman/bin/postfix-to-mailman.py flags=FR user=list argv=/usr/lib/mailman/bin/postfix-to-mailman.py
${nexthop} ${user} ${nexthop} ${user}
# Modoboa autoreply service
#
autoreply unix - n n - - pipe
flags= user=%{dovecot_mailboxes_owner}:%{dovecot_mailboxes_owner} argv=%{modoboa_venv_path}/bin/python %{modoboa_instance_path}/manage.py autoreply $sender $mailbox
# Amavis return path # Amavis return path
# #
%{amavis_enabled}127.0.0.1:10025 inet n - n - - smtpd %{amavis_enabled}127.0.0.1:10025 inet n - n - - smtpd
@@ -148,4 +149,4 @@ mailman unix - n n - - pipe
%{amavis_enabled} -o smtpd_client_connection_count_limit=0 %{amavis_enabled} -o smtpd_client_connection_count_limit=0
%{amavis_enabled} -o smtpd_client_connection_rate_limit=0 %{amavis_enabled} -o smtpd_client_connection_rate_limit=0
%{amavis_enabled} -o receive_override_options=no_header_body_checks,no_unknown_recipient_checks %{amavis_enabled} -o receive_override_options=no_header_body_checks,no_unknown_recipient_checks
%{amavis_enabled} -o local_header_rewrite_clients=permit_mynetworks,permit_sasl_authenticated %{amavis_enabled} -o local_header_rewrite_clients=

View File

@@ -71,7 +71,7 @@
# Authentication method # Authentication method
# Value: none | htpasswd | remote_user | http_x_remote_user # Value: none | htpasswd | remote_user | http_x_remote_user
type = radicale_modoboa_auth_oauth2 type = radicale_dovecot_auth
# Htpasswd filename # Htpasswd filename
# htpasswd_filename = users # htpasswd_filename = users
@@ -85,7 +85,7 @@ type = radicale_modoboa_auth_oauth2
# Incorrect authentication delay (seconds) # Incorrect authentication delay (seconds)
#delay = 1 #delay = 1
oauth2_introspection_endpoint = %{oauth2_introspection_url} auth_socket = %{auth_socket_path}
[rights] [rights]

View File

@@ -1,14 +0,0 @@
clamav {
scan_mime_parts = true;
scan_text_mime = true;
scan_image_mime = true;
retransmits = 2;
timeout = 30;
symbol = "CLAM_VIRUS";
type = "clamav";
servers = "127.0.0.1:3310"
patterns {
# symbol_name = "pattern";
JUST_EICAR = "Test.EICAR";
}
}

View File

@@ -1,3 +0,0 @@
try_fallback = false;
selector_map = "%selector_map_path";
path_map = "%key_map_path";

View File

@@ -1,3 +0,0 @@
try_fallback = false;
selector_map = "%selector_map_path";
path_map = "%key_map_path";

View File

@@ -1,21 +0,0 @@
reporting {
# Required attributes
enabled = true; # Enable reports in general
email = 'postmaster@%hostname'; # Source of DMARC reports
domain = '%hostname'; # Domain to serve
org_name = '%hostname'; # Organisation
# Optional parameters
#bcc_addrs = ["postmaster@example.com"]; # additional addresses to copy on reports
report_local_controller = false; # Store reports for local/controller scans (for testing only)
#helo = 'rspamd.localhost'; # Helo used in SMTP dialog
#smtp = '127.0.0.1'; # SMTP server IP
#smtp_port = 25; # SMTP server port
from_name = '%hostname DMARC REPORT'; # SMTP FROM
msgid_from = 'rspamd'; # Msgid format
#max_entries = 1k; # Maxiumum amount of entries per domain
#keys_expire = 2d; # Expire date for Redis keys
#only_domains = '/path/to/map'; # Only store reports from domains or eSLDs listed in this map
# Available from 3.3
#exclude_domains = '/path/to/map'; # Exclude reports from domains or eSLDs listed in this map
#exclude_domains = ["example.com", "another.com"]; # Alternative, use array to exclude reports from domains or eSLDs
}

View File

@@ -1,5 +0,0 @@
rules {
DMARC_POLICY_QUARANTINE {
action = "add header";
}
}

View File

@@ -1,2 +0,0 @@
%{greylisting_disabled}enabled = false;
servers = "127.0.0.1:6379";

View File

@@ -1,5 +0,0 @@
symbols {
"WHITELIST_AUTHENTICATED" {
weight = %whitelist_auth_weigth;
}
}

View File

@@ -1,20 +0,0 @@
actions {
reject = 15; # normal value is 15, 150 so it will never be rejected
add_header = 6; # set to 0.1 for testing, 6 for normal operation.
rewrite_subject = 8; # Default: 8
greylist = 4; # Default: 4
}
group "antivirus" {
symbol "JUST_EICAR" {
weight = 10;
description = "Eicar test signature";
}
symbol "CLAM_VIRUS_FAIL" {
weight = 0;
}
symbol "CLAM_VIRUS" {
weight = 10;
description = "ClamAV found a Virus";
}
}

View File

@@ -1,16 +0,0 @@
use = ["x-spam-status","x-virus","authentication-results" ];
extended_spam_headers = false;
skip_local = false;
skip_authenticated = false;
routines {
x-virus {
header = "X-Virus";
remove = 1;
symbols = ["CLAM_VIRUS", "JUST_EICAR"];
}
}

View File

@@ -1 +0,0 @@
enabled = true;

View File

@@ -1,6 +0,0 @@
# to disable all predefined rules if the user doesn't want dnsbl
url_whitelist = [];
rbls {
}

View File

@@ -1,2 +0,0 @@
write_servers = "localhost";
read_servers = "localhost";

View File

@@ -1,8 +0,0 @@
authenticated {
priority = high;
authenticated = yes;
apply {
groups_disabled = ["rbl", "spf"];
}
%{whitelist_auth_enabled} symbols ["WHITELIST_AUTHENTICATED"];
}

View File

@@ -1,6 +0,0 @@
spf_cache_size = 1k;
spf_cache_expire = 1d;
max_dns_nesting = 10;
max_dns_requests = 30;
min_cache_ttl = 5m;
disable_ipv6 = false;

View File

@@ -1 +0,0 @@
enable_password = %controller_password

View File

@@ -1 +0,0 @@
enabled = false;

View File

@@ -1,3 +0,0 @@
upstream "local" {
self_scan = yes;
}

View File

@@ -13,5 +13,3 @@ socket = %uwsgi_socket_path
chmod-socket = 660 chmod-socket = 660
vacuum = true vacuum = true
single-interpreter = True single-interpreter = True
max-requests = 5000
buffer-size = 8192

View File

@@ -26,8 +26,7 @@ class Modoboa(base.Installer):
"deb": [ "deb": [
"build-essential", "python3-dev", "libxml2-dev", "libxslt-dev", "build-essential", "python3-dev", "libxml2-dev", "libxslt-dev",
"libjpeg-dev", "librrd-dev", "rrdtool", "libffi-dev", "cron", "libjpeg-dev", "librrd-dev", "rrdtool", "libffi-dev", "cron",
"libssl-dev", "redis-server", "supervisor", "pkg-config", "libssl-dev", "redis-server", "supervisor"
"libcairo2-dev"
], ],
"rpm": [ "rpm": [
"gcc", "gcc-c++", "python3-devel", "libxml2-devel", "libxslt-devel", "gcc", "gcc-c++", "python3-devel", "libxml2-devel", "libxslt-devel",
@@ -44,73 +43,80 @@ class Modoboa(base.Installer):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Get configuration.""" """Get configuration."""
super().__init__(*args, **kwargs) super(Modoboa, self).__init__(*args, **kwargs)
self.venv_path = self.config.get("modoboa", "venv_path") self.venv_path = self.config.get("modoboa", "venv_path")
self.instance_path = self.config.get("modoboa", "instance_path") self.instance_path = self.config.get("modoboa", "instance_path")
self.extensions = self.config.get("modoboa", "extensions").split() self.extensions = self.config.get("modoboa", "extensions").split()
self.devmode = self.config.getboolean("modoboa", "devmode") self.devmode = self.config.getboolean("modoboa", "devmode")
self.amavis_enabled = self.config.getboolean("amavis", "enabled") # Sanity check for amavis
self.amavis_enabled = False
if "modoboa-amavis" in self.extensions:
if self.config.getboolean("amavis", "enabled"):
self.amavis_enabled = True
else:
self.extensions.remove("modoboa-amavis")
if "modoboa-radicale" in self.extensions:
if not self.config.getboolean("radicale", "enabled"):
self.extensions.remove("modoboa-radicale")
self.dovecot_enabled = self.config.getboolean("dovecot", "enabled") self.dovecot_enabled = self.config.getboolean("dovecot", "enabled")
self.opendkim_enabled = self.config.getboolean("opendkim", "enabled") self.opendkim_enabled = self.config.getboolean("opendkim", "enabled")
self.dkim_cron_enabled = False
def is_extension_ok_for_version(self, extension, version): def is_extension_ok_for_version(self, extension, version):
"""Check if extension can be installed with this modo version.""" """Check if extension can be installed with this modo version."""
if extension not in compatibility_matrix.EXTENSIONS_AVAILABILITY:
return True
version = utils.convert_version_to_int(version) version = utils.convert_version_to_int(version)
if extension in compatibility_matrix.EXTENSIONS_AVAILABILITY:
min_version = compatibility_matrix.EXTENSIONS_AVAILABILITY[extension] min_version = compatibility_matrix.EXTENSIONS_AVAILABILITY[extension]
min_version = utils.convert_version_to_int(min_version) min_version = utils.convert_version_to_int(min_version)
return version >= min_version return version >= min_version
if extension in compatibility_matrix.REMOVED_EXTENSIONS:
max_version = compatibility_matrix.REMOVED_EXTENSIONS[extension]
max_version = utils.convert_version_to_int(max_version)
return version < max_version
return True
def _setup_venv(self): def _setup_venv(self):
"""Prepare a dedicated virtualenv.""" """Prepare a dedicated virtualenv."""
python.setup_virtualenv(self.venv_path, sudo_user=self.user) python.setup_virtualenv(
self.venv_path, sudo_user=self.user, python_version=3)
packages = ["rrdtool"] packages = ["rrdtool"]
version = self.config.get("modoboa", "version") version = self.config.get("modoboa", "version")
extras = "postgresql"
if self.dbengine != "postgres":
extras = "mysql"
if self.devmode:
extras += ",dev"
if version == "latest": if version == "latest":
packages += [f"modoboa[{extras}]"] + self.extensions packages += ["modoboa"] + self.extensions
for extension in list(self.extensions):
if extension in compatibility_matrix.REMOVED_EXTENSIONS.keys():
self.extensions.remove(extension)
self.extensions = [
extension for extension in self.extensions
if extension not in compatibility_matrix.REMOVED_EXTENSIONS
]
else: else:
matrix = compatibility_matrix.COMPATIBILITY_MATRIX[version] matrix = compatibility_matrix.COMPATIBILITY_MATRIX[version]
packages.append(f"modoboa[{extras}]=={version}") packages.append("modoboa=={}".format(version))
for extension in list(self.extensions): for extension in list(self.extensions):
if not self.is_extension_ok_for_version(extension, version): if not self.is_extension_ok_for_version(extension, version):
self.extensions.remove(extension) self.extensions.remove(extension)
continue continue
if extension in matrix: if extension in matrix:
req_version = matrix[extension] req_version = matrix[extension]
if req_version is None: req_version = req_version.replace("<", "\<")
continue req_version = req_version.replace(">", "\>")
req_version = req_version.replace("<", "\\<")
req_version = req_version.replace(">", "\\>")
packages.append("{}{}".format(extension, req_version)) packages.append("{}{}".format(extension, req_version))
else: else:
packages.append(extension) packages.append(extension)
# Temp fix for django-braces
python.install_package(
"django-braces", self.venv_path, upgrade=self.upgrade,
sudo_user=self.user
)
if self.dbengine == "postgres":
packages.append("psycopg2-binary\<2.9")
else:
packages.append("mysqlclient")
if sys.version_info.major == 2 and sys.version_info.micro < 9: if sys.version_info.major == 2 and sys.version_info.micro < 9:
# Add extra packages to fix the SNI issue # Add extra packages to fix the SNI issue
packages += ["pyOpenSSL"] packages += ["pyOpenSSL"]
# Temp fix for https://github.com/modoboa/modoboa/issues/2247
packages.append("django-webpack-loader==0.7.0")
python.install_packages( python.install_packages(
packages, self.venv_path, packages, self.venv_path,
upgrade=self.upgrade, upgrade=self.upgrade,
sudo_user=self.user, sudo_user=self.user,
beta=self.config.getboolean("modoboa", "install_beta") beta=self.config.getboolean("modoboa", "install_beta")
) )
if self.devmode:
# FIXME: use dev-requirements instead
python.install_packages(
["django-bower", "django-debug-toolbar"], self.venv_path,
upgrade=self.upgrade, sudo_user=self.user)
def _deploy_instance(self): def _deploy_instance(self):
"""Deploy Modoboa.""" """Deploy Modoboa."""
@@ -171,7 +177,7 @@ class Modoboa(base.Installer):
if self.upgrade and self.opendkim_enabled and self.dbengine == "postgres": if self.upgrade and self.opendkim_enabled and self.dbengine == "postgres":
# Restore view previously deleted # Restore view previously deleted
self.backend.load_sql_file( self.backend.load_sql_file(
self.dbname, self.dbuser, self.dbpasswd, self.dbname, self.dbuser, self.dbpassword,
self.get_file_path("dkim_view_{}.sql".format(self.dbengine)) self.get_file_path("dkim_view_{}.sql".format(self.dbengine))
) )
self.backend.grant_right_on_table( self.backend.grant_right_on_table(
@@ -181,7 +187,7 @@ class Modoboa(base.Installer):
def setup_database(self): def setup_database(self):
"""Additional config.""" """Additional config."""
super().setup_database() super(Modoboa, self).setup_database()
if not self.amavis_enabled: if not self.amavis_enabled:
return return
self.backend.grant_access( self.backend.grant_access(
@@ -189,7 +195,7 @@ class Modoboa(base.Installer):
def get_packages(self): def get_packages(self):
"""Include extra packages if needed.""" """Include extra packages if needed."""
packages = super().get_packages() packages = super(Modoboa, self).get_packages()
condition = ( condition = (
package.backend.FORMAT == "rpm" and package.backend.FORMAT == "rpm" and
sys.version_info.major == 2 and sys.version_info.major == 2 and
@@ -199,10 +205,6 @@ class Modoboa(base.Installer):
packages += ["openssl-devel"] packages += ["openssl-devel"]
return packages return packages
def setup_user(self):
super().setup_user()
self._setup_venv()
def get_config_files(self): def get_config_files(self):
"""Return appropriate path.""" """Return appropriate path."""
config_files = super().get_config_files() config_files = super().get_config_files()
@@ -211,13 +213,6 @@ class Modoboa(base.Installer):
else: else:
path = "supervisor=/etc/supervisord.d/policyd.ini" path = "supervisor=/etc/supervisord.d/policyd.ini"
config_files.append(path) config_files.append(path)
# Add worker for dkim if needed
if self.modoboa_2_2_or_greater:
config_files.append(
"supervisor-rq-dkim=/etc/supervisor/conf.d/modoboa-dkim-worker.conf")
config_files.append(
"supervisor-rq-base=/etc/supervisor/conf.d/modoboa-base-worker.conf")
return config_files return config_files
def get_template_context(self): def get_template_context(self):
@@ -226,20 +221,17 @@ class Modoboa(base.Installer):
extensions = self.config.get("modoboa", "extensions") extensions = self.config.get("modoboa", "extensions")
extensions = extensions.split() extensions = extensions.split()
random_hour = random.randint(0, 6) random_hour = random.randint(0, 6)
self.dkim_cron_enabled = (not self.modoboa_2_2_or_greater and
self.opendkim_enabled)
context.update({ context.update({
"sudo_user": ( "sudo_user": (
"uwsgi" if package.backend.FORMAT == "rpm" else context["user"] "uwsgi" if package.backend.FORMAT == "rpm" else context["user"]
), ),
"dovecot_mailboxes_owner": ( "dovecot_mailboxes_owner": (
self.config.get("dovecot", "mailboxes_owner")), self.config.get("dovecot", "mailboxes_owner")),
"radicale_enabled": (
"" if "modoboa-radicale" in extensions else "#"),
"opendkim_user": self.config.get("opendkim", "user"), "opendkim_user": self.config.get("opendkim", "user"),
"dkim_user": "_rspamd" if self.config.getboolean("rspamd", "enabled") else self.config.get("opendkim", "user"),
"minutes": random.randint(1, 59), "minutes": random.randint(1, 59),
"hours": f"{random_hour},{random_hour+12}", "hours" : f"{random_hour},{random_hour+12}"
"modoboa_2_2_or_greater": "" if self.modoboa_2_2_or_greater else "#",
"dkim_cron_enabled": "" if self.dkim_cron_enabled else "#"
}) })
return context return context
@@ -251,7 +243,7 @@ class Modoboa(base.Installer):
self.instance_path, "media", "webmail") self.instance_path, "media", "webmail")
pw = pwd.getpwnam(self.user) pw = pwd.getpwnam(self.user)
for d in [rrd_root_dir, pdf_storage_dir, webmail_media_dir]: for d in [rrd_root_dir, pdf_storage_dir, webmail_media_dir]:
utils.mkdir_safe(d, stat.S_IRWXU | stat.S_IRWXG, pw[2], pw[3]) utils.mkdir(d, stat.S_IRWXU | stat.S_IRWXG, pw[2], pw[3])
settings = { settings = {
"admin": { "admin": {
"handle_mailboxes": True, "handle_mailboxes": True,
@@ -263,10 +255,10 @@ class Modoboa(base.Installer):
"maillog": { "maillog": {
"rrd_rootdir": rrd_root_dir, "rrd_rootdir": rrd_root_dir,
}, },
"pdfcredentials": { "modoboa_pdfcredentials": {
"storage_dir": pdf_storage_dir "storage_dir": pdf_storage_dir
}, },
"calendars": { "modoboa_radicale": {
"server_location": "https://{}/radicale/".format( "server_location": "https://{}/radicale/".format(
self.config.get("general", "hostname")), self.config.get("general", "hostname")),
"rights_file_path": "{}/rights".format( "rights_file_path": "{}/rights".format(
@@ -279,15 +271,6 @@ class Modoboa(base.Installer):
if self.config.getboolean("opendkim", "enabled"): if self.config.getboolean("opendkim", "enabled"):
settings["admin"]["dkim_keys_storage_dir"] = ( settings["admin"]["dkim_keys_storage_dir"] = (
self.config.get("opendkim", "keys_storage_dir")) self.config.get("opendkim", "keys_storage_dir"))
if self.config.getboolean("rspamd", "enabled"):
settings["admin"]["dkim_keys_storage_dir"] = (
self.config.get("rspamd", "dkim_keys_storage_dir"))
settings["modoboa_rspamd"] = {
"key_map_path": self.config.get("rspamd", "key_map_path"),
"selector_map_path": self.config.get("rspamd", "selector_map_path")
}
settings = json.dumps(settings) settings = json.dumps(settings)
query = ( query = (
"UPDATE core_localconfig SET _parameters='{}'" "UPDATE core_localconfig SET _parameters='{}'"
@@ -298,18 +281,17 @@ class Modoboa(base.Installer):
def post_run(self): def post_run(self):
"""Additional tasks.""" """Additional tasks."""
if 'centos' in utils.dist_name(): self._setup_venv()
system.enable_and_start_service("redis")
else:
system.enable_and_start_service("redis-server")
self._deploy_instance() self._deploy_instance()
if not self.upgrade: if not self.upgrade:
self.apply_settings() self.apply_settings()
if 'centos' in utils.dist_name(): if 'centos' in utils.dist_name():
supervisor = "supervisord" supervisor = "supervisord"
system.enable_and_start_service("redis")
else: else:
supervisor = "supervisor" supervisor = "supervisor"
system.enable_and_start_service("redis-server")
# Restart supervisor # Restart supervisor
system.enable_service(supervisor) system.enable_service(supervisor)
utils.exec_cmd("service {} stop".format(supervisor)) utils.exec_cmd("service {} stop".format(supervisor))

View File

@@ -21,7 +21,7 @@ class Nginx(base.Installer):
def get_template_context(self, app): def get_template_context(self, app):
"""Additionnal variables.""" """Additionnal variables."""
context = super().get_template_context() context = super(Nginx, self).get_template_context()
context.update({ context.update({
"app_instance_path": ( "app_instance_path": (
self.config.get(app, "instance_path")), self.config.get(app, "instance_path")),

View File

@@ -82,13 +82,11 @@ class Opendkim(base.Installer):
"""Additional tasks. """Additional tasks.
Check linux distribution (package deb, rpm), to adapt Check linux distribution (package deb, rpm), to adapt
to config file location and syntax. to config file location and syntax.
- update opendkim isocket port config - update opendkim isocket port config for Debian based distro
- make sure opendkim starts after db service started - make sure opendkim starts after db service started
""" """
if package.backend.FORMAT == "deb": if package.backend.FORMAT == "deb":
params_file = "/etc/default/opendkim" params_file = "/etc/default/opendkim"
else:
params_file = "/etc/opendkim.conf"
pattern = r"s/^(SOCKET=.*)/#\1/" pattern = r"s/^(SOCKET=.*)/#\1/"
utils.exec_cmd( utils.exec_cmd(
"perl -pi -e '{}' {}".format(pattern, params_file)) "perl -pi -e '{}' {}".format(pattern, params_file))
@@ -115,20 +113,19 @@ class Opendkim(base.Installer):
"""Restore keys.""" """Restore keys."""
dkim_keys_backup = os.path.join( dkim_keys_backup = os.path.join(
self.archive_path, "custom/dkim") self.archive_path, "custom/dkim")
keys_storage_dir = self.app_config["keys_storage_dir"]
if os.path.isdir(dkim_keys_backup): if os.path.isdir(dkim_keys_backup):
for file in os.listdir(dkim_keys_backup): for file in os.listdir(dkim_keys_backup):
file_path = os.path.join(dkim_keys_backup, file) file_path = os.path.join(dkim_keys_backup, file)
if os.path.isfile(file_path): if os.path.isfile(file_path):
utils.copy_file(file_path, keys_storage_dir) utils.copy_file(file_path, self.config.get(
"opendkim", "keys_storage_dir", fallback="/var/lib/dkim"))
utils.success("DKIM keys restored from backup") utils.success("DKIM keys restored from backup")
# Setup permissions
user = self.config.get("opendkim", "user")
utils.exec_cmd(f"chown -R {user}:{user} {keys_storage_dir}")
def custom_backup(self, path): def custom_backup(self, path):
"""Backup DKIM keys.""" """Backup DKIM keys."""
if os.path.isdir(self.app_config["keys_storage_dir"]): storage_dir = self.config.get(
shutil.copytree(self.app_config["keys_storage_dir"], os.path.join(path, "dkim")) "opendkim", "keys_storage_dir", fallback="/var/lib/dkim")
if os.path.isdir(storage_dir):
shutil.copytree(storage_dir, os.path.join(path, "dkim"))
utils.printcolor( utils.printcolor(
"DKIM keys saved!", utils.GREEN) "DKIM keys saved!", utils.GREEN)

View File

@@ -14,13 +14,15 @@ from . import backup, install
class Postfix(base.Installer): class Postfix(base.Installer):
"""Postfix installer.""" """Postfix installer."""
appname = "postfix" appname = "postfix"
packages = { packages = {
"deb": ["postfix", "postfix-pcre"], "deb": ["postfix"],
"rpm": ["postfix"],
} }
config_files = ["main.cf", "master.cf", "anonymize_headers.pcre"] config_files = ["main.cf", "master.cf"]
def get_packages(self): def get_packages(self):
"""Additional packages.""" """Additional packages."""
@@ -28,28 +30,17 @@ class Postfix(base.Installer):
packages = ["postfix-{}".format(self.db_driver)] packages = ["postfix-{}".format(self.db_driver)]
else: else:
packages = [] packages = []
return super().get_packages() + packages return super(Postfix, self).get_packages() + packages
def install_packages(self): def install_packages(self):
"""Preconfigure postfix package installation.""" """Preconfigure postfix package installation."""
if "centos" in utils.dist_name():
config = configparser.ConfigParser()
with open("/etc/yum.repos.d/CentOS-Base.repo") as fp:
config.read_file(fp)
config.set("centosplus", "enabled", "1")
config.set("centosplus", "includepkgs", "postfix-*")
config.set("base", "exclude", "postfix-*")
config.set("updates", "exclude", "postfix-*")
with open("/etc/yum.repos.d/CentOS-Base.repo", "w") as fp:
config.write(fp)
package.backend.preconfigure( package.backend.preconfigure(
"postfix", "main_mailer_type", "select", "No configuration") "postfix", "main_mailer_type", "select", "No configuration")
super().install_packages() super(Postfix, self).install_packages()
def get_template_context(self): def get_template_context(self):
"""Additional variables.""" """Additional variables."""
context = super().get_template_context() context = super(Postfix, self).get_template_context()
context.update({ context.update({
"db_driver": self.db_driver, "db_driver": self.db_driver,
"dovecot_mailboxes_owner": self.config.get( "dovecot_mailboxes_owner": self.config.get(
@@ -59,19 +50,10 @@ class Postfix(base.Installer):
"modoboa_instance_path": self.config.get( "modoboa_instance_path": self.config.get(
"modoboa", "instance_path"), "modoboa", "instance_path"),
"opendkim_port": self.config.get( "opendkim_port": self.config.get(
"opendkim", "port"), "opendkim", "port")
"rspamd_disabled": "" if not self.config.getboolean(
"rspamd", "enabled") else "#"
}) })
return context return context
def check_dhe_group_file(self):
group = self.config.get(self.appname, "dhe_group")
file_name = f"ffdhe{group}.pem"
if not os.path.exists(f"{self.config_dir}/{file_name}"):
url = f"https://raw.githubusercontent.com/internetstandards/dhe_groups/main/{file_name}"
utils.exec_cmd(f"wget {url}", cwd=self.config_dir)
def post_run(self): def post_run(self):
"""Additional tasks.""" """Additional tasks."""
venv_path = self.config.get("modoboa", "venv_path") venv_path = self.config.get("modoboa", "venv_path")
@@ -93,8 +75,10 @@ class Postfix(base.Installer):
if not os.path.exists(path): if not os.path.exists(path):
utils.copy_file(os.path.join("/etc", f), path) utils.copy_file(os.path.join("/etc", f), path)
# Generate DHE group # Generate EDH parameters
self.check_dhe_group_file() if not os.path.exists("{}/dh2048.pem".format(self.config_dir)):
cmd = "openssl dhparam -dsaparam -out dh2048.pem 2048"
utils.exec_cmd(cmd, cwd=self.config_dir)
# Generate /etc/aliases.db file to avoid warnings # Generate /etc/aliases.db file to avoid warnings
aliases_file = "/etc/aliases" aliases_file = "/etc/aliases"
@@ -102,18 +86,8 @@ class Postfix(base.Installer):
utils.exec_cmd("postalias {}".format(aliases_file)) utils.exec_cmd("postalias {}".format(aliases_file))
# Postwhite # Postwhite
condition = (
not self.config.getboolean("rspamd", "enabled") and
self.config.getboolean("postwhite", "enabled")
)
if condition:
install("postwhite", self.config, self.upgrade, self.archive_path) install("postwhite", self.config, self.upgrade, self.archive_path)
def backup(self, path): def backup(self, path):
"""Launch postwhite backup.""" """Launch postwhite backup."""
condition = (
not self.config.getboolean("rspamd", "enabled") and
self.config.getboolean("postwhite", "enabled")
)
if condition:
backup("postwhite", self.config, path) backup("postwhite", self.config, path)

View File

@@ -47,10 +47,8 @@ class Postwhite(base.Installer):
self.install_from_archive(SPF_TOOLS_REPOSITORY, install_dir) self.install_from_archive(SPF_TOOLS_REPOSITORY, install_dir)
self.postw_dir = self.install_from_archive( self.postw_dir = self.install_from_archive(
POSTWHITE_REPOSITORY, install_dir) POSTWHITE_REPOSITORY, install_dir)
utils.copy_file( postw_bin = os.path.join(self.postw_dir, "postwhite")
os.path.join(self.postw_dir, "postwhite.conf"), self.config_dir) utils.exec_cmd("{} /etc/postwhite.conf".format(postw_bin))
self.postw_bin = os.path.join(self.postw_dir, "postwhite")
utils.exec_cmd("{} /etc/postwhite.conf".format(self.postw_bin))
def custom_backup(self, path): def custom_backup(self, path):
"""Backup custom configuration if any.""" """Backup custom configuration if any."""
@@ -67,3 +65,6 @@ class Postwhite(base.Installer):
if os.path.isfile(postwhite_backup_configuration): if os.path.isfile(postwhite_backup_configuration):
utils.copy_file(postwhite_backup_configuration, self.config_dir) utils.copy_file(postwhite_backup_configuration, self.config_dir)
utils.success("postwhite.conf restored from backup") utils.success("postwhite.conf restored from backup")
else:
utils.copy_file(
os.path.join(self.postw_dir, "postwhite.conf"), self.config_dir)

View File

@@ -31,9 +31,10 @@ class Radicale(base.Installer):
def _setup_venv(self): def _setup_venv(self):
"""Prepare a dedicated virtualenv.""" """Prepare a dedicated virtualenv."""
python.setup_virtualenv(self.venv_path, sudo_user=self.user) python.setup_virtualenv(
self.venv_path, sudo_user=self.user, python_version=3)
packages = [ packages = [
"Radicale", "pytz", "radicale-modoboa-auth-oauth2" "Radicale", "radicale-dovecot-auth", "pytz"
] ]
python.install_packages(packages, self.venv_path, sudo_user=self.user) python.install_packages(packages, self.venv_path, sudo_user=self.user)
python.install_package_from_repository( python.install_package_from_repository(
@@ -43,22 +44,17 @@ class Radicale(base.Installer):
def get_template_context(self): def get_template_context(self):
"""Additional variables.""" """Additional variables."""
context = super().get_template_context() context = super(Radicale, self).get_template_context()
oauth2_client_id, oauth2_client_secret = utils.create_oauth2_app( radicale_auth_socket_path = self.config.get(
"Radicale", "radicale", self.config) "dovecot", "radicale_auth_socket_path")
hostname = self.config.get("general", "hostname")
oauth2_introspection_url = (
f"https://{oauth2_client_id}:{oauth2_client_secret}"
f"@{hostname}/api/o/introspect/"
)
context.update({ context.update({
"oauth2_introspection_url": oauth2_introspection_url, "auth_socket_path": radicale_auth_socket_path
}) })
return context return context
def get_config_files(self): def get_config_files(self):
"""Return appropriate path.""" """Return appropriate path."""
config_files = super().get_config_files() config_files = super(Radicale, self).get_config_files()
if package.backend.FORMAT == "deb": if package.backend.FORMAT == "deb":
path = "supervisor=/etc/supervisor/conf.d/radicale.conf" path = "supervisor=/etc/supervisor/conf.d/radicale.conf"
else: else:

View File

@@ -13,14 +13,14 @@ class Restore:
""" """
if not os.path.isdir(restore): if not os.path.isdir(restore):
utils.error( utils.printcolor(
"Provided path is not a directory !") "Provided path is not a directory !", utils.RED)
sys.exit(1) sys.exit(1)
modoba_sql_file = os.path.join(restore, "databases/modoboa.sql") modoba_sql_file = os.path.join(restore, "databases/modoboa.sql")
if not os.path.isfile(modoba_sql_file): if not os.path.isfile(modoba_sql_file):
utils.error( utils.printcolor(
modoba_sql_file + " not found, please check your backup") modoba_sql_file + " not found, please check your backup", utils.RED)
sys.exit(1) sys.exit(1)
# Everything seems alright here, proceeding... # Everything seems alright here, proceeding...

View File

@@ -1,154 +0,0 @@
"""Rspamd related functions."""
import os
import pwd
import stat
from .. import package
from .. import utils
from .. import system
from . import base
from . import install
class Rspamd(base.Installer):
"""Rspamd installer."""
appname = "rspamd"
packages = {
"deb": [
"rspamd", "redis"
]
}
config_files = [
"local.d/arc.conf",
"local.d/dkim_signing.conf",
"local.d/dmarc.conf",
"local.d/force_actions.conf",
"local.d/greylist.conf",
"local.d/metrics.conf",
"local.d/milter_headers.conf",
"local.d/mx_check.conf",
"local.d/redis.conf",
"local.d/settings.conf",
"local.d/spf.conf",
"local.d/worker-normal.inc",
"local.d/worker-proxy.inc",
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.generate_password_condition = (
not self.upgrade or utils.user_input(
"Do you want to (re)generate rspamd password ? (y/N)").lower().startswith("y")
)
@property
def config_dir(self):
"""Return appropriate config dir."""
return "/etc/rspamd"
def install_packages(self):
debian_based_dist, codename = utils.is_dist_debian_based()
if debian_based_dist:
utils.mkdir_safe(
"/etc/apt/keyrings",
stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP |
stat.S_IROTH | stat.S_IXOTH,
0, 0
)
package.backend.add_custom_repository(
"rspamd",
"http://rspamd.com/apt-stable/",
"https://rspamd.com/apt-stable/gpg.key",
codename
)
package.backend.update()
return super().install_packages()
def install_config_files(self):
"""Make sure config directory exists."""
user = self.config.get(self.appname, "user")
pw = pwd.getpwnam(user)
targets = [
[self.app_config["dkim_keys_storage_dir"], pw[2], pw[3]]
]
for target in targets:
if not os.path.exists(target[0]):
utils.mkdir(
target[0],
stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP |
stat.S_IROTH | stat.S_IXOTH,
target[1], target[2]
)
super().install_config_files()
def get_config_files(self):
"""Return appropriate config files."""
_config_files = self.config_files
if self.config.getboolean("clamav", "enabled"):
_config_files.append("local.d/antivirus.conf")
if self.app_config["dnsbl"].lower() == "true":
_config_files.append("local.d/rbl.conf")
if self.app_config["whitelist_auth"].lower() == "true":
_config_files.append("local.d/groups.conf")
if self.generate_password_condition:
_config_files.append("local.d/worker-controller.inc")
return _config_files
def get_template_context(self):
_context = super().get_template_context()
_context["greylisting_disabled"] = "" if not self.app_config["greylisting"].lower() == "true" else "#"
_context["whitelist_auth_enabled"] = "" if self.app_config["whitelist_auth"].lower() == "true" else "#"
if self.generate_password_condition:
code, controller_password = utils.exec_cmd(
r"rspamadm pw -p {}".format(self.app_config["password"]))
if code != 0:
utils.error("Error setting rspamd password. "
"Please make sure it is not 'q1' or 'q2'."
"Storing the password in plain. See"
"https://rspamd.com/doc/quickstart.html#setting-the-controller-password")
_context["controller_password"] = self.app_config["password"]
else:
controller_password = controller_password.decode().replace("\n", "")
_context["controller_password"] = controller_password
return _context
def post_run(self):
"""Additional tasks."""
user = self.config.get(self.appname, "user")
system.add_user_to_group(
self.config.get("modoboa", "user"),
user
)
if self.config.getboolean("clamav", "enabled"):
install("clamav", self.config, self.upgrade, self.archive_path)
def custom_backup(self, path):
"""Backup custom configuration if any."""
custom_config_dir = os.path.join(self.config_dir,
"/local.d/")
custom_backup_dir = os.path.join(path, "/rspamd/")
local_files = [f for f in os.listdir(custom_config_dir)
if os.path.isfile(custom_config_dir, f)
]
for file in local_files:
utils.copy_file(file, custom_backup_dir)
if len(local_files) != 0:
utils.success("Rspamd custom configuration saved!")
def restore(self):
"""Restore custom config files."""
custom_config_dir = os.path.join(self.config_dir,
"/local.d/")
custom_backup_dir = os.path.join(self.archive_path, "/rspamd/")
backed_up_files = [
f for f in os.listdir(custom_backup_dir)
if os.path.isfile(custom_backup_dir, f)
]
for f in backed_up_files:
utils.copy_file(f, custom_config_dir)
utils.success("Custom Rspamd configuration restored.")

View File

@@ -57,7 +57,9 @@ class Spamassassin(base.Installer):
def post_run(self): def post_run(self):
"""Additional tasks.""" """Additional tasks."""
install("razor", self.config, self.upgrade, self.restore) amavis_user = self.config.get("amavis", "user")
pw = pwd.getpwnam(amavis_user)
install("razor", self.config, self.upgrade, self.archive_path)
if utils.dist_name() in ["debian", "ubuntu"]: if utils.dist_name() in ["debian", "ubuntu"]:
utils.exec_cmd( utils.exec_cmd(
"perl -pi -e 's/^CRON=0/CRON=1/' /etc/cron.daily/spamassassin") "perl -pi -e 's/^CRON=0/CRON=1/' /etc/cron.daily/spamassassin")

View File

@@ -17,7 +17,7 @@ class Uwsgi(base.Installer):
appname = "uwsgi" appname = "uwsgi"
packages = { packages = {
"deb": ["uwsgi", "uwsgi-plugin-python3"], "deb": ["uwsgi", "uwsgi-plugin-python3"],
"rpm": ["uwsgi", "uwsgi-plugin-python36"], "rpm": ["uwsgi", "uwsgi-plugin-python3"],
} }
def get_socket_path(self, app): def get_socket_path(self, app):
@@ -29,10 +29,7 @@ class Uwsgi(base.Installer):
def get_template_context(self, app): def get_template_context(self, app):
"""Additionnal variables.""" """Additionnal variables."""
context = super(Uwsgi, self).get_template_context() context = super(Uwsgi, self).get_template_context()
if package.backend.FORMAT == "deb":
uwsgi_plugin = "python3" uwsgi_plugin = "python3"
else:
uwsgi_plugin = "python36"
context.update({ context.update({
"app_user": self.config.get(app, "user"), "app_user": self.config.get(app, "user"),
"app_venv_path": self.config.get(app, "venv_path"), "app_venv_path": self.config.get(app, "venv_path"),

View File

@@ -7,7 +7,7 @@ from . import package
from . import utils from . import utils
class CertificateBackend: class CertificateBackend(object):
"""Base class.""" """Base class."""
def __init__(self, config): def __init__(self, config):
@@ -24,44 +24,13 @@ class CertificateBackend:
return False return False
return True return True
def generate_cert(self):
"""Create a certificate."""
pass
class ManualCertificate(CertificateBackend):
"""Use certificate provided."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
path_correct = True
self.tls_cert_file_path = self.config.get("certificate",
"tls_cert_file_path")
self.tls_key_file_path = self.config.get("certificate",
"tls_key_file_path")
if not os.path.exists(self.tls_key_file_path):
utils.error("'tls_key_file_path' path is not accessible")
path_correct = False
if not os.path.exists(self.tls_cert_file_path):
utils.error("'tls_cert_file_path' path is not accessible")
path_correct = False
if not path_correct:
sys.exit(1)
self.config.set("general", "tls_key_file",
self.tls_key_file_path)
self.config.set("general", "tls_cert_file",
self.tls_cert_file_path)
class SelfSignedCertificate(CertificateBackend): class SelfSignedCertificate(CertificateBackend):
"""Create a self signed certificate.""" """Create a self signed certificate."""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Sanity checks.""" """Sanity checks."""
super().__init__(*args, **kwargs) super(SelfSignedCertificate, self).__init__(*args, **kwargs)
if self.config.has_option("general", "tls_key_file"): if self.config.has_option("general", "tls_key_file"):
# Compatibility # Compatibility
return return
@@ -96,7 +65,7 @@ class LetsEncryptCertificate(CertificateBackend):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Update config.""" """Update config."""
super().__init__(*args, **kwargs) super(LetsEncryptCertificate, self).__init__(*args, **kwargs)
self.hostname = self.config.get("general", "hostname") self.hostname = self.config.get("general", "hostname")
self.config.set("general", "tls_cert_file", ( self.config.set("general", "tls_cert_file", (
"/etc/letsencrypt/live/{}/fullchain.pem".format(self.hostname))) "/etc/letsencrypt/live/{}/fullchain.pem".format(self.hostname)))
@@ -121,14 +90,14 @@ class LetsEncryptCertificate(CertificateBackend):
elif "centos" in name: elif "centos" in name:
package.backend.install("certbot") package.backend.install("certbot")
else: else:
utils.printcolor("Failed to install certbot, aborting.") utils.printcolor("Failed to install certbot, aborting.", utils.RED)
sys.exit(1) sys.exit(1)
# Nginx plugin certbot # Nginx plugin certbot
if ( if (
self.config.has_option("nginx", "enabled") and self.config.has_option("nginx", "enabled") and
self.config.getboolean("nginx", "enabled") self.config.getboolean("nginx", "enabled")
): ):
if name == "ubuntu" or name.startswith("debian"): if name == "ubuntu" or name.startswith("debian") or ("Centos" in name and version.startswith("9")):
package.backend.install("python3-certbot-nginx") package.backend.install("python3-certbot-nginx")
def generate_cert(self): def generate_cert(self):
@@ -146,24 +115,12 @@ class LetsEncryptCertificate(CertificateBackend):
cfg_file = "/etc/letsencrypt/renewal/{}.conf".format(self.hostname) cfg_file = "/etc/letsencrypt/renewal/{}.conf".format(self.hostname)
pattern = "s/authenticator = standalone/authenticator = nginx/" pattern = "s/authenticator = standalone/authenticator = nginx/"
utils.exec_cmd("perl -pi -e '{}' {}".format(pattern, cfg_file)) utils.exec_cmd("perl -pi -e '{}' {}".format(pattern, cfg_file))
with open("/etc/letsencrypt/renewal-hooks/deploy/reload-services.sh", "w") as fp:
fp.write(f"""#!/bin/bash
HOSTNAME=$(basename $RENEWED_LINEAGE)
if [ "$HOSTNAME" = "{self.hostname}" ]
then
systemctl reload dovecot
systemctl reload postfix
fi
""")
def get_backend(config): def get_backend(config):
"""Return the appropriate backend.""" """Return the appropriate backend."""
cert_type = config.get("certificate", "type") if not config.getboolean("certificate", "generate"):
if cert_type == "letsencrypt": return None
if config.get("certificate", "type") == "letsencrypt":
return LetsEncryptCertificate(config) return LetsEncryptCertificate(config)
if cert_type == "manual":
return ManualCertificate(config)
return SelfSignedCertificate(config) return SelfSignedCertificate(config)

View File

@@ -1,6 +1,5 @@
"""Utility functions.""" """Utility functions."""
import configparser
import contextlib import contextlib
import datetime import datetime
import getpass import getpass
@@ -13,10 +12,12 @@ import stat
import string import string
import subprocess import subprocess
import sys import sys
import uuid try:
import configparser
except ImportError:
import ConfigParser as configparser
from . import config_dict_template from . import config_dict_template
from .compatibility_matrix import APP_INCOMPATIBILITY
ENV = {} ENV = {}
@@ -32,19 +33,22 @@ class FatalError(Exception):
def user_input(message): def user_input(message):
"""Ask something to the user.""" """Ask something to the user."""
try:
from builtins import input
except ImportError:
answer = raw_input(message)
else:
answer = input(message) answer = input(message)
return answer return answer
def exec_cmd(cmd, sudo_user=None, login=True, **kwargs): def exec_cmd(cmd, sudo_user=None, pinput=None, login=True, **kwargs):
""" """Execute a shell command.
Execute a shell command.
Run a command using the current user. Set :keyword:`sudo_user` if Run a command using the current user. Set :keyword:`sudo_user` if
you need different privileges. you need different privileges.
:param str cmd: the command to execute :param str cmd: the command to execute
:param str sudo_user: a valid system username :param str sudo_user: a valid system username
:param str pinput: data to send to process's stdin
:rtype: tuple :rtype: tuple
:return: return code, command output :return: return code, command output
""" """
@@ -53,21 +57,23 @@ def exec_cmd(cmd, sudo_user=None, login=True, **kwargs):
cmd = "sudo {}-u {} {}".format("-i " if login else "", sudo_user, cmd) cmd = "sudo {}-u {} {}".format("-i " if login else "", sudo_user, cmd)
if "shell" not in kwargs: if "shell" not in kwargs:
kwargs["shell"] = True kwargs["shell"] = True
capture_output = True if pinput is not None:
kwargs["stdin"] = subprocess.PIPE
capture_output = False
if "capture_output" in kwargs: if "capture_output" in kwargs:
capture_output = kwargs.pop("capture_output") capture_output = kwargs.pop("capture_output")
elif not ENV.get("debug"):
capture_output = True
if capture_output: if capture_output:
kwargs.update(stdout=subprocess.PIPE, stderr=subprocess.STDOUT) kwargs.update(stdout=subprocess.PIPE, stderr=subprocess.PIPE)
kwargs["universal_newlines"] = True output = None
output: str = "" process = subprocess.Popen(cmd, **kwargs)
with subprocess.Popen(cmd, **kwargs) as process: if pinput or capture_output:
if capture_output: c_args = [pinput] if pinput is not None else []
for line in process.stdout: output = process.communicate(*c_args)[0]
output += line else:
if ENV.get("debug"): process.wait()
sys.stdout.write(line) return process.returncode, output
return process.returncode, output.encode()
def dist_info(): def dist_info():
@@ -95,17 +101,6 @@ def dist_name():
return dist_info()[0].lower() return dist_info()[0].lower()
def is_dist_debian_based() -> (bool, str):
"""Check if current OS is Debian based or not."""
status, codename = exec_cmd("lsb_release -c -s")
codename = codename.decode().strip().lower()
return codename in [
"bionic", "bookworm", "bullseye", "buster",
"focal", "jammy", "jessie", "sid", "stretch",
"trusty", "wheezy", "xenial"
], codename
def mkdir(path, mode, uid, gid): def mkdir(path, mode, uid, gid):
"""Create a directory.""" """Create a directory."""
if not os.path.exists(path): if not os.path.exists(path):
@@ -140,6 +135,7 @@ def settings(**kwargs):
class ConfigFileTemplate(string.Template): class ConfigFileTemplate(string.Template):
"""Custom class for configuration files.""" """Custom class for configuration files."""
delimiter = "%" delimiter = "%"
@@ -177,36 +173,32 @@ def copy_from_template(template, dest, context):
fp.write(ConfigFileTemplate(buf).substitute(context)) fp.write(ConfigFileTemplate(buf).substitute(context))
def check_config_file(dest, def check_config_file(dest, interactive=False, upgrade=False, backup=False, restore=False):
interactive=False,
upgrade=False,
backup=False,
restore=False):
"""Create a new installer config file if needed.""" """Create a new installer config file if needed."""
is_present = True is_present = True
if os.path.exists(dest): if os.path.exists(dest):
return is_present, update_config(dest, False) return is_present
if upgrade: if upgrade:
error( printcolor(
"You cannot upgrade an existing installation without a " "You cannot upgrade an existing installation without a "
"configuration file.") "configuration file.", RED)
sys.exit(1) sys.exit(1)
elif backup: elif backup:
is_present = False is_present = False
error( printcolor(
"Your configuration file hasn't been found. A new one will be generated. " "Your configuration file hasn't been found. A new one will be generated. "
"Please edit it with correct password for the databases !") "Please edit it with correct password for the databases !", RED)
elif restore: elif restore:
error( printcolor(
"You cannot restore an existing installation without a " "You cannot restore an existing installation without a "
f"configuration file. (file : {dest} has not been found...") f"configuration file. (file : {dest} has not been found...", RED)
sys.exit(1) sys.exit(1)
printcolor( printcolor(
"Configuration file {} not found, creating new one." "Configuration file {} not found, creating new one."
.format(dest), YELLOW) .format(dest), YELLOW)
gen_config(dest, interactive) gen_config(dest, interactive)
return is_present, None return is_present
def has_colours(stream): def has_colours(stream):
@@ -285,16 +277,6 @@ def random_key(l=16):
return key return key
def check_if_condition(config, entry):
"""Check if the "if" directive is present and computes it"""
section_if = True
for condition in entry:
config_key, value = condition.split("=")
section_name, option = config_key.split(".")
section_if = config.get(section_name, option) == value
return section_if
def validate(value, config_entry): def validate(value, config_entry):
if value is None: if value is None:
return False return False
@@ -315,14 +297,11 @@ def validate(value, config_entry):
return True return True
def get_entry_value(entry: dict, interactive: bool, config: configparser.ConfigParser) -> string: def get_entry_value(entry, interactive):
default_entry = entry["default"] if callable(entry["default"]):
if type(default_entry) is type(list()):
default_value = str(check_if_condition(config, default_entry)).lower()
elif callable(default_entry):
default_value = entry["default"]() default_value = entry["default"]()
else: else:
default_value = default_entry default_value = entry["default"]
user_value = None user_value = None
if entry.get("customizable") and interactive: if entry.get("customizable") and interactive:
while (user_value != '' and not validate(user_value, entry)): while (user_value != '' and not validate(user_value, entry)):
@@ -338,119 +317,26 @@ def get_entry_value(entry: dict, interactive: bool, config: configparser.ConfigP
if entry.get("values") and user_value != "": if entry.get("values") and user_value != "":
user_value = values[int(user_value)] user_value = values[int(user_value)]
non_interactive_values = entry.get("non_interactive_values", [])
if user_value in non_interactive_values:
error(
f"{user_value} cannot be set interactively. "
"Please configure installer.cfg manually by running "
"'python3 run.py --stop-after-configfile-check domain'. "
"Check modoboa-installer README for more information."
)
sys.exit(1)
return user_value if user_value else default_value return user_value if user_value else default_value
def load_config_template(interactive):
"""Instantiate a configParser object with the predefined template."""
tpl_dict = config_dict_template.ConfigDictTemplate
config = configparser.ConfigParser()
# only ask about options we need, else still generate default
for section in tpl_dict:
interactive_section = interactive
if "if" in section:
condition = check_if_condition(config, section["if"])
interactive_section = condition and interactive
config.add_section(section["name"])
for config_entry in section["values"]:
if config_entry.get("if") is not None:
interactive_section = (interactive_section and
check_if_condition(
config, config_entry["if"]
)
)
value = get_entry_value(config_entry,
interactive_section,
config)
config.set(section["name"], config_entry["option"], value)
return config
def update_config(path, apply_update=True):
"""Update an existing config file."""
config = configparser.ConfigParser()
with open(path) as fp:
config.read_file(fp)
new_config = load_config_template(False)
old_sections = config.sections()
new_sections = new_config.sections()
update = False
dropped_sections = list(set(old_sections) - set(new_sections))
added_sections = list(set(new_sections) - set(old_sections))
if len(dropped_sections) > 0 and apply_update:
printcolor("Following section(s) will not be ported "
"due to being deleted or renamed: " +
', '.join(dropped_sections),
RED)
if len(dropped_sections) + len(added_sections) > 0:
update = True
for section in new_sections:
if section in old_sections:
new_options = new_config.options(section)
old_options = config.options(section)
dropped_options = list(set(old_options) - set(new_options))
added_options = list(set(new_options) - set(old_options))
if len(dropped_options) > 0 and apply_update:
printcolor(f"Following option(s) from section: {section}, "
"will not be ported due to being "
"deleted or renamed: " +
', '.join(dropped_options),
RED)
if len(dropped_options) + len(added_options) > 0:
update = True
if apply_update:
for option in new_options:
if option in old_options:
value = config.get(section, option, raw=True)
if value != new_config.get(section, option, raw=True):
update = True
new_config.set(section, option, value)
if apply_update:
if update:
# Backing up old config file
date = datetime.datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
dest = f"{os.path.splitext(path)[0]}_{date}.old"
shutil.copy(path, dest)
# Overwritting old config file
with open(path, "w") as configfile:
new_config.write(configfile)
# Set file owner to running u+g, and set config file permission to 600
current_username = getpass.getuser()
current_user = pwd.getpwnam(current_username)
os.chown(dest, current_user[2], current_user[3])
os.chmod(dest, stat.S_IRUSR | stat.S_IWUSR)
return dest
return None
else:
# Simply check if current config file is outdated
return update
def gen_config(dest, interactive=False): def gen_config(dest, interactive=False):
"""Create config file from dict template""" """Create config file from dict template"""
config = load_config_template(interactive) tpl_dict = config_dict_template.ConfigDictTemplate
config = configparser.ConfigParser()
# only ask about options we need, else still generate default
for section in tpl_dict:
if "if" in section:
config_key, value = section.get("if").split("=")
section_name, option = config_key.split(".")
interactive_section = (
config.get(section_name, option) == value and interactive)
else:
interactive_section = interactive
config.add_section(section["name"])
for config_entry in section["values"]:
value = get_entry_value(config_entry, interactive_section)
config.set(section["name"], config_entry["option"], value)
with open(dest, "w") as configfile: with open(dest, "w") as configfile:
config.write(configfile) config.write(configfile)
@@ -473,7 +359,7 @@ def validate_backup_path(path: str, silent_mode: bool):
if not path_exists: if not path_exists:
if not silent_mode: if not silent_mode:
create_dir = input( create_dir = input(
f"\"{path}\" doesn't exist, would you like to create it? [y/N]\n" f"\"{path}\" doesn't exist, would you like to create it? [Y/n]\n"
).lower() ).lower()
if silent_mode or (not silent_mode and create_dir.startswith("y")): if silent_mode or (not silent_mode and create_dir.startswith("y")):
@@ -488,7 +374,7 @@ def validate_backup_path(path: str, silent_mode: bool):
if len(os.listdir(path)) != 0: if len(os.listdir(path)) != 0:
if not silent_mode: if not silent_mode:
delete_dir = input( delete_dir = input(
"Warning: backup directory is not empty, it will be purged if you continue... [y/N]\n").lower() "Warning: backup directory is not empty, it will be purged if you continue... [Y/n]\n").lower()
if silent_mode or (not silent_mode and delete_dir.startswith("y")): if silent_mode or (not silent_mode and delete_dir.startswith("y")):
try: try:
@@ -513,33 +399,3 @@ def validate_backup_path(path: str, silent_mode: bool):
mkdir_safe(os.path.join(backup_path, dir), mkdir_safe(os.path.join(backup_path, dir),
stat.S_IRWXU | stat.S_IRWXG, pw[2], pw[3]) stat.S_IRWXU | stat.S_IRWXG, pw[2], pw[3])
return backup_path return backup_path
def create_oauth2_app(app_name: str, client_id: str, config) -> tuple[str, str]:
"""Create a application for Oauth2 authentication."""
# FIXME: how can we check that application already exists ?
venv_path = config.get("modoboa", "venv_path")
python_path = os.path.join(venv_path, "bin", "python")
instance_path = config.get("modoboa", "instance_path")
script_path = os.path.join(instance_path, "manage.py")
client_secret = str(uuid.uuid4())
cmd = (
f"{python_path} {script_path} createapplication "
f"--name={app_name} --skip-authorization "
f"--client-id={client_id} --client-secret={client_secret} "
f"confidential client-credentials"
)
exec_cmd(cmd)
return client_id, client_secret
def check_app_compatibility(section, config):
"""Check that the app can be installed in regards to other enabled apps."""
incompatible_app = []
if section in APP_INCOMPATIBILITY.keys():
for app in APP_INCOMPATIBILITY[section]:
if config.getboolean(app, "enabled"):
error(f"{section} cannot be installed if {app} is enabled. "
"Please disable one of them.")
incompatible_app.append(app)
return len(incompatible_app) == 0

161
run.py
View File

@@ -3,12 +3,14 @@
"""An installer for Modoboa.""" """An installer for Modoboa."""
import argparse import argparse
import configparser
import datetime import datetime
import os import os
try:
import configparser
except ImportError:
import ConfigParser as configparser
import sys import sys
from modoboa_installer import checks
from modoboa_installer import compatibility_matrix from modoboa_installer import compatibility_matrix
from modoboa_installer import constants from modoboa_installer import constants
from modoboa_installer import package from modoboa_installer import package
@@ -16,24 +18,68 @@ from modoboa_installer import scripts
from modoboa_installer import ssl from modoboa_installer import ssl
from modoboa_installer import system from modoboa_installer import system
from modoboa_installer import utils from modoboa_installer import utils
from modoboa_installer import disclaimers
PRIMARY_APPS = [ PRIMARY_APPS = [
"fail2ban", "amavis",
"modoboa", "modoboa",
"automx", "automx",
"radicale", "radicale",
"uwsgi", "uwsgi",
"nginx", "nginx",
"opendkim",
"postfix", "postfix",
"dovecot" "dovecot"
] ]
def installation_disclaimer(args, config):
"""Display installation disclaimer."""
hostname = config.get("general", "hostname")
utils.printcolor(
"Warning:\n"
"Before you start the installation, please make sure the following "
"DNS records exist for domain '{}':\n"
" {} IN A <IP ADDRESS OF YOUR SERVER>\n"
" IN MX {}.\n".format(
args.domain,
hostname.replace(".{}".format(args.domain), ""),
hostname
),
utils.CYAN
)
utils.printcolor(
"Your mail server will be installed with the following components:",
utils.BLUE)
def upgrade_disclaimer(config):
"""Display upgrade disclaimer."""
utils.printcolor(
"Your mail server is about to be upgraded and the following components"
" will be impacted:", utils.BLUE
)
def backup_disclaimer():
"""Display backup disclamer. """
utils.printcolor(
"Your mail server will be backed up locally.\n"
" !! You should really transfer the backup somewhere else...\n"
" !! Custom configuration (like for postfix) won't be saved.", utils.BLUE)
def restore_disclaimer():
"""Display restore disclamer. """
utils.printcolor(
"You are about to restore a previous installation of Modoboa.\n"
"If a new version has been released in between, please update your database!",
utils.BLUE)
def backup_system(config, args): def backup_system(config, args):
"""Launch backup procedure.""" """Launch backup procedure."""
disclaimers.backup_disclaimer() backup_disclaimer()
backup_path = None backup_path = None
if args.silent_backup: if args.silent_backup:
if not args.backup_path: if not args.backup_path:
@@ -66,22 +112,11 @@ def backup_system(config, args):
utils.copy_file(args.configfile, backup_path) utils.copy_file(args.configfile, backup_path)
# Backup applications # Backup applications
for app in PRIMARY_APPS: for app in PRIMARY_APPS:
if app == "dovecot" and args.no_mail:
utils.printcolor("Skipping mail backup", utils.BLUE)
continue
scripts.backup(app, config, backup_path) scripts.backup(app, config, backup_path)
def config_file_update_complete(backup_location): def main(input_args):
utils.printcolor("Update complete. It seems successful.", """Install process."""
utils.BLUE)
if backup_location is not None:
utils.printcolor("You will find your old config file "
f"here: {backup_location}",
utils.BLUE)
def parser_setup(input_args):
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
versions = ( versions = (
["latest"] + list(compatibility_matrix.COMPATIBILITY_MATRIX.keys()) ["latest"] + list(compatibility_matrix.COMPATIBILITY_MATRIX.keys())
@@ -117,27 +152,15 @@ def parser_setup(input_args):
parser.add_argument( parser.add_argument(
"--silent-backup", action="store_true", default=False, "--silent-backup", action="store_true", default=False,
help="For script usage, do not require user interaction " help="For script usage, do not require user interaction "
"backup will be saved at ./modoboa_backup/Backup_M_Y_d_H_M " "backup will be saved at ./modoboa_backup/Backup_M_Y_d_H_M if --backup-path is not provided")
"if --backup-path is not provided")
parser.add_argument(
"--no-mail", action="store_true", default=False,
help="Disable mail backup (save space)")
parser.add_argument( parser.add_argument(
"--restore", type=str, metavar="path", "--restore", type=str, metavar="path",
help="Restore a previously backup up modoboa instance on a NEW machine. " help="Restore a previously backup up modoboa instance on a NEW machine. "
"You MUST provide backup directory" "You MUST provide backup directory"
) )
parser.add_argument(
"--skip-checks", action="store_true", default=False,
help="Skip the checks the installer performs initially")
parser.add_argument("domain", type=str, parser.add_argument("domain", type=str,
help="The main domain of your future mail server") help="The main domain of your future mail server")
return parser.parse_args(input_args) args = parser.parse_args(input_args)
def main(input_args):
"""Install process."""
args = parser_setup(input_args)
if args.debug: if args.debug:
utils.ENV["debug"] = True utils.ENV["debug"] = True
@@ -154,14 +177,7 @@ def main(input_args):
sys.exit(1) sys.exit(1)
utils.success("Welcome to Modoboa installer!\n") utils.success("Welcome to Modoboa installer!\n")
is_config_file_available = utils.check_config_file(
# Checks
if not args.skip_checks:
utils.printcolor("Checking the installer...", utils.BLUE)
checks.handle()
utils.success("Checks complete\n")
is_config_file_available, outdate_config = utils.check_config_file(
args.configfile, args.interactive, args.upgrade, args.backup, is_restoring) args.configfile, args.interactive, args.upgrade, args.backup, is_restoring)
if not is_config_file_available and ( if not is_config_file_available and (
@@ -169,20 +185,6 @@ def main(input_args):
utils.error("No config file found.") utils.error("No config file found.")
return return
# Check if config is outdated and ask user if it needs to be updated
if is_config_file_available and outdate_config:
answer = utils.user_input("It seems that your config file is outdated. "
"Would you like to update it? (Y/n) ")
if not answer or answer.lower().startswith("y"):
config_file_update_complete(utils.update_config(args.configfile))
if not args.stop_after_configfile_check:
answer = utils.user_input("Would you like to stop to review the updated config? (Y/n)")
if not answer or answer.lower().startswith("y"):
return
else:
utils.error("You might encounter unexpected errors ! "
"Make sure to update your config before opening an issue!")
if args.stop_after_configfile_check: if args.stop_after_configfile_check:
return return
@@ -196,36 +198,28 @@ def main(input_args):
config.set("modoboa", "version", args.version) config.set("modoboa", "version", args.version)
config.set("modoboa", "install_beta", str(args.beta)) config.set("modoboa", "install_beta", str(args.beta))
if config.get("antispam", "type") == "amavis":
antispam_apps = ["amavis", "opendkim"]
else:
antispam_apps = ["rspamd"]
if args.backup or args.silent_backup: if args.backup or args.silent_backup:
backup_system(config, args) backup_system(config, args)
return return
# Display disclaimer python 3 linux distribution # Display disclaimer python 3 linux distribution
if args.upgrade: if args.upgrade:
disclaimers.upgrade_disclaimer(config) upgrade_disclaimer(config)
elif args.restore: elif args.restore:
disclaimers.restore_disclaimer() restore_disclaimer()
scripts.restore_prep(args.restore) scripts.restore_prep(args.restore)
else: else:
disclaimers.installation_disclaimer(args, config) installation_disclaimer(args, config)
# Show concerned components # Show concerned components
components = [] components = []
for section in config.sections(): for section in config.sections():
if section in ["general", "antispam", "database", "mysql", "postgres", if section in ["general", "database", "mysql", "postgres",
"certificate", "letsencrypt", "backup"]: "certificate", "letsencrypt"]:
continue continue
if (config.has_option(section, "enabled") and if (config.has_option(section, "enabled") and
not config.getboolean(section, "enabled")): not config.getboolean(section, "enabled")):
continue continue
incompatible_app_detected = not utils.check_app_compatibility(section, config)
if incompatible_app_detected:
sys.exit(0)
components.append(section) components.append(section)
utils.printcolor(" ".join(components), utils.YELLOW) utils.printcolor(" ".join(components), utils.YELLOW)
if not args.force: if not args.force:
@@ -242,39 +236,22 @@ def main(input_args):
ssl_backend = ssl.get_backend(config) ssl_backend = ssl.get_backend(config)
if ssl_backend and not args.upgrade: if ssl_backend and not args.upgrade:
ssl_backend.generate_cert() ssl_backend.generate_cert()
for appname in PRIMARY_APPS + antispam_apps: for appname in PRIMARY_APPS:
scripts.install(appname, config, args.upgrade, args.restore) scripts.install(appname, config, args.upgrade, args.restore)
if package.backend.FORMAT == "deb":
system.restart_service("cron") system.restart_service("cron")
else:
system.restart_service("crond")
package.backend.restore_system() package.backend.restore_system()
hostname = config.get("general", "hostname")
if not args.restore: if not args.restore:
utils.success( utils.success(
f"Congratulations! You can enjoy Modoboa at https://{hostname} " "Congratulations! You can enjoy Modoboa at https://{} (admin:password)"
"(admin:password)" .format(config.get("general", "hostname"))
) )
else: else:
utils.success( utils.success(
f"Restore complete! You can enjoy Modoboa at https://{hostname} " "Restore complete! You can enjoy Modoboa at https://{} (same credentials as before)"
"(same credentials as before)" .format(config.get("general", "hostname"))
)
if config.getboolean("rspamd", "enabled"):
rspamd_password = config.get("rspamd", "password")
utils.success(
f"You can also enjoy rspamd at https://{hostname}/rspamd "
f"(password: {rspamd_password})"
)
utils.success(
"\n"
"Modoboa is a free software maintained by volunteers.\n"
"You like the project and want it to be sustainable?\n"
"Then don't wait anymore and go sponsor it here:\n"
)
utils.printcolor(
"https://github.com/sponsors/modoboa\n",
utils.YELLOW
)
utils.success(
"Thank you for your help :-)\n"
) )

View File

@@ -1,2 +1,3 @@
codecov codecov
mock mock
six

View File

@@ -6,13 +6,8 @@ import sys
import tempfile import tempfile
import unittest import unittest
from io import StringIO from six import StringIO
from pathlib import Path from six.moves import configparser
try:
import configparser
except ImportError:
import ConfigParser as configparser
try: try:
from unittest.mock import patch from unittest.mock import patch
except ImportError: except ImportError:
@@ -47,7 +42,7 @@ class ConfigFileTestCase(unittest.TestCase):
def test_interactive_mode(self, mock_user_input): def test_interactive_mode(self, mock_user_input):
"""Check interactive mode.""" """Check interactive mode."""
mock_user_input.side_effect = [ mock_user_input.side_effect = [
"0", "0", "", "", "", "", "", "" "0", "0", "", "", "", "", ""
] ]
with open(os.devnull, "w") as fp: with open(os.devnull, "w") as fp:
sys.stdout = fp sys.stdout = fp
@@ -62,44 +57,11 @@ class ConfigFileTestCase(unittest.TestCase):
self.assertEqual(config.get("certificate", "type"), "self-signed") self.assertEqual(config.get("certificate", "type"), "self-signed")
self.assertEqual(config.get("database", "engine"), "postgres") self.assertEqual(config.get("database", "engine"), "postgres")
@patch("modoboa_installer.utils.user_input")
def test_updating_configfile(self, mock_user_input):
"""Check configfile update mechanism."""
cfgfile_temp = os.path.join(self.workdir, "installer_old.cfg")
out = StringIO()
sys.stdout = out
run.main([
"--stop-after-configfile-check",
"--configfile", cfgfile_temp,
"example.test"])
self.assertTrue(os.path.exists(cfgfile_temp))
# Adding a dummy section
with open(cfgfile_temp, "a") as fp:
fp.write(
"""
[dummy]
weird_old_option = "hey
""")
mock_user_input.side_effect = ["y"]
out = StringIO()
sys.stdout = out
run.main([
"--stop-after-configfile-check",
"--configfile", cfgfile_temp,
"example.test"])
self.assertIn("dummy", out.getvalue())
self.assertTrue(Path(self.workdir).glob("*.old"))
self.assertIn("Update complete",
out.getvalue()
)
@patch("modoboa_installer.utils.user_input") @patch("modoboa_installer.utils.user_input")
def test_interactive_mode_letsencrypt(self, mock_user_input): def test_interactive_mode_letsencrypt(self, mock_user_input):
"""Check interactive mode.""" """Check interactive mode."""
mock_user_input.side_effect = [ mock_user_input.side_effect = [
"0", "0", "1", "admin@example.test", "0", "", "", "", "" "1", "admin@example.test", "0", "", "", "", "", ""
] ]
with open(os.devnull, "w") as fp: with open(os.devnull, "w") as fp:
sys.stdout = fp sys.stdout = fp
@@ -126,12 +88,8 @@ class ConfigFileTestCase(unittest.TestCase):
"example.test"]) "example.test"])
self.assertTrue(os.path.exists(self.cfgfile)) self.assertTrue(os.path.exists(self.cfgfile))
self.assertIn( self.assertIn(
"fail2ban modoboa automx amavis clamav dovecot nginx " "modoboa automx amavis clamav dovecot nginx razor postfix"
"postfix postwhite spamassassin uwsgi radicale opendkim", " postwhite spamassassin uwsgi",
out.getvalue()
)
self.assertNotIn(
"It seems that your config file is outdated.",
out.getvalue() out.getvalue()
) )

View File

@@ -1 +0,0 @@
53669b48de7ce85341a547ed2583380fcb06841b