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
48 changed files with 256 additions and 1217 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
* Distribution: Debian / Ubuntu / Centos
* Codename: Jessie / Trusty / Centos 7 / ...
* Codename: Jessie / Trusty / Centos 9 Stream / ...
* Arch: 32 Bits / 64 Bits
* Database: PostgreSQL / MySQL

View File

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

View File

@@ -1,5 +1,5 @@
**modoboa-installer**
=====================
modoboa-installer
=================
|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:
* Debian 10 and upper
* Debian Buster (10) / Bullseye (11)
* Ubuntu Bionic Beaver (18.04) and upper
* CentOS 9 Stream
.. warning::
``/tmp`` partition must be mounted without the ``noexec`` option.
Centos 7 support has been depreceated since modoboa requires python 3.7>=.
.. note::
@@ -76,7 +78,7 @@ If you want more information about the installation process, add the
``--debug`` option to your command line.
Upgrade mode
============
------------
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.
Backup mode
===========
------------
An experimental backup mode is available.
@@ -107,19 +109,7 @@ You can start the process as follows::
Then follow the step on the console.
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
There is also a non-interactive mode:
1. Silent mode
@@ -141,7 +131,7 @@ configuration file (set enabled to False).
This can be useful for larger instance.
Restore mode
============
------------
An experimental restore mode is available.
@@ -152,7 +142,7 @@ You can start the process as follows::
Then wait for the process to finish.
Change the generated hostname
=============================
-----------------------------
By default, the installer will setup your email server using the
following hostname: ``mail.<your domain>``. If you want a different
@@ -171,24 +161,12 @@ modifications.
Finally, run the installer without the
``--stop-after-configfile-check`` option.
Certificate
===========
Self-signed
-----------
It is the default type of certificate the installer will generate, it
is however not recommended for production use.
Letsencrypt
-----------
Let's Encrypt certificate
-------------------------
.. warning::
Please note that by using this option, you agree to the `ToS
<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
Please 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
server you're installing Modoboa on.
@@ -199,8 +177,6 @@ modify the following settings::
[certificate]
generate = true
type = letsencrypt
tls_cert_file_path =
tls_key_file_path =
[letsencrypt]
email = admin@example.com
@@ -208,27 +184,6 @@ modify the following settings::
Change the ``email`` setting to a valid value since it will be used
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*
.. |workflow| image:: https://github.com/modoboa/modoboa-installer/workflows/Modoboa%20installer/badge.svg
.. |codecov| image:: http://codecov.io/github/modoboa/modoboa-installer/coverage.svg?branch=master
: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,15 +20,9 @@ COMPATIBILITY_MATRIX = {
"modoboa-pdfcredentials": ">=1.1.1",
"modoboa-sievefilters": ">=1.1.1",
"modoboa-webmail": ">=1.2.0",
},
}
}
EXTENSIONS_AVAILABILITY = {
"modoboa-contacts": "1.7.4",
}
REMOVED_EXTENSIONS = {
"modoboa-pdfcredentials": "2.1.0",
"modoboa-dmarc": "2.1.0",
"modoboa-imap-migration": "2.1.0"
}

View File

@@ -30,21 +30,16 @@ ConfigDictTemplate = [
{
"name": "certificate",
"values": [
{
"option": "generate",
"default": "true",
},
{
"option": "type",
"default": "self-signed",
"customizable": True,
"question": "Please choose your certificate type",
"values": ["self-signed", "letsencrypt", "manual"],
"non_interactive_values": ["manual"],
},
{
"option": "tls_cert_file_path",
"default": ""
},
{
"option": "tls_key_file_path",
"default": ""
"values": ["self-signed", "letsencrypt"],
}
],
},
@@ -123,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",
"values": [
@@ -188,7 +158,7 @@ ConfigDictTemplate = [
{
"option": "extensions",
"default": (
"modoboa-amavis "
"modoboa-amavis modoboa-pdfcredentials "
"modoboa-postfix-autoreply modoboa-sievefilters "
"modoboa-webmail modoboa-contacts "
"modoboa-radicale"
@@ -286,7 +256,7 @@ ConfigDictTemplate = [
},
{
"option": "user",
"default": "dovecot",
"default": "vmail",
},
{
"option": "home_dir",
@@ -351,10 +321,6 @@ ConfigDictTemplate = [
"option": "message_size_limit",
"default": "11534336",
},
{
"option": "dhe_group",
"default": "4096"
}
]
},
{

View File

@@ -42,7 +42,7 @@ class PostgreSQL(Database):
default_port = 5432
packages = {
"deb": ["postgresql", "postgresql-server-dev-all"],
"rpm": ["postgresql-server", "postgresql-devel"]
"rpm": ["postgresql-server", "postgresql-server-devel", "postgresql"]
}
service = "postgresql"
@@ -54,19 +54,7 @@ class PostgreSQL(Database):
"""Install database if required."""
name, version = utils.dist_info()
if "CentOS" in name:
if version.startswith("7"):
# 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"
initdb_cmd = "postgresql-setup --initdb"
cfgfile = "/var/lib/pgsql/data/pg_hba.conf"
package.backend.install_many(self.packages[package.backend.FORMAT])
utils.exec_cmd(initdb_cmd)
@@ -178,15 +166,11 @@ class MySQL(Database):
if name.startswith("debian"):
if version.startswith("8"):
self.packages["deb"].append("libmysqlclient-dev")
elif version.startswith("11") or version.startswith("12"):
elif version.startswith("11"):
self.packages["deb"].append("libmariadb-dev")
else:
self.packages["deb"].append("libmariadbclient-dev")
elif name == "ubuntu":
if version.startswith("2"):
# Works for Ubuntu 22 and 20
self.packages["deb"].append("libmariadb-dev")
else:
self.packages["deb"].append("libmysqlclient-dev")
super(MySQL, self).install_package()
queries = []
@@ -199,10 +183,7 @@ class MySQL(Database):
"mariadb-server", "root_password_again", "password",
self.dbpassword)
return
if (
(name.startswith("debian") and (version.startswith("11") or version.startswith("12"))) or
(name.startswith("ubuntu") and version.startswith("22"))
):
if version.startswith("11"):
queries = [
"SET PASSWORD FOR 'root'@'localhost' = PASSWORD('{}')"
.format(self.dbpassword),

View File

@@ -46,7 +46,7 @@ class DEBPackage(Package):
"""Update local cache."""
if self.index_updated:
return
utils.exec_cmd("apt-get -o Dpkg::Progress-Fancy=0 update --quiet")
utils.exec_cmd("apt-get update --quiet")
self.index_updated = True
def preconfigure(self, name, question, qtype, answer):
@@ -57,18 +57,18 @@ class DEBPackage(Package):
def install(self, name):
"""Install a package."""
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):
"""Install many packages."""
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)))
def get_installed_version(self, name):
"""Get installed package version."""
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())
if match:
return match.group(2)
@@ -82,22 +82,32 @@ class RPMPackage(Package):
def __init__(self, dist_name):
"""Initialize backend."""
self.dist_name = dist_name
super(RPMPackage, self).__init__(dist_name)
if "centos" in 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.update()
def update(self):
"""Update the database repo."""
utils.exec_cmd("dnf update -y --quiet")
def install(self, name):
"""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):
"""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):
"""Get installed package version."""
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())
if match:
return match.group(1)
@@ -108,7 +118,7 @@ def get_backend():
"""Return the appropriate package backend."""
distname = utils.dist_name()
backend = None
if distname in ["debian", "debian gnu/linux", "ubuntu", "linuxmint"]:
if distname in ["debian", "debian gnu/linux", "ubuntu"]:
backend = DEBPackage
elif "centos" in distname:
backend = RPMPackage

View File

@@ -1,7 +1,6 @@
"""Python related tools."""
import os
import sys
from . import package
from . import utils
@@ -46,40 +45,6 @@ def install_packages(names, venv=None, upgrade=False, **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):
"""Install a Python package from its repository."""
if vcs == "git":
@@ -89,20 +54,16 @@ def install_package_from_repository(name, url, vcs="git", venv=None, **kwargs):
utils.exec_cmd(cmd, **kwargs)
def install_package_from_remote_requirements(url, venv=None, **kwargs):
"""Install a Python package from a file."""
cmd = "{} install {} {}".format(
get_pip_path(venv),
"-r",
url
)
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."""
if os.path.exists(path):
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"):
python_binary = "python3"
packages = ["python3"]
@@ -111,5 +72,8 @@ def setup_virtualenv(path, sudo_user=None):
packages = ["python3-venv"]
package.backend.install_many(packages)
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))
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",
],
"rpm": [
"amavisd-new", "arj", "lz4", "lzop", "p7zip",
"amavis", "arj", "lz4", "lzop", "p7zip",
],
}
with_db = True

View File

@@ -44,21 +44,22 @@ class Automx(base.Installer):
sql_query = (
"SELECT first_name || ' ' || last_name AS display_name, email"
", 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:
sql_query = (
"SELECT concat(first_name, ' ', last_name) AS display_name, "
"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})
return context
def _setup_venv(self):
"""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 = [
"future", "lxml", "ipaddress", "sqlalchemy < 2.0", "python-memcached",
"future", "lxml", "ipaddress", "sqlalchemy", "python-memcached",
"python-dateutil", "configparser"
]
if self.dbengine == "postgres":

View File

@@ -44,7 +44,8 @@ class Backup:
path_exists = os.path.exists(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
if not path_exists:
@@ -57,7 +58,9 @@ class Backup:
utils.mkdir_safe(path, stat.S_IRWXU |
stat.S_IRWXG, pw[2], pw[3])
else:
utils.error("Error, backup directory not present.")
utils.printcolor(
"Error, backup directory not present.", utils.RED
)
return False
if len(os.listdir(path)) != 0:
@@ -77,7 +80,9 @@ class Backup:
shutil.rmtree(os.path.join(path, "databases"),
ignore_errors=False)
else:
utils.error("Error: backup directory not clean.")
utils.printcolor(
"Error: backup directory not clean.", utils.RED
)
return False
self.backup_path = path
@@ -126,8 +131,8 @@ class Backup:
home_path = self.config.get("dovecot", "home_dir")
if not os.path.exists(home_path) or os.path.isfile(home_path):
utils.error("Error backing up Email, provided path "
f" ({home_path}) seems not right...")
utils.printcolor("Error backing up Email, provided path "
f" ({home_path}) seems not right...", utils.RED)
else:
dst = os.path.join(self.backup_path, "mails/")

View File

@@ -5,12 +5,11 @@ import sys
from .. import database
from .. import package
from .. import python
from .. import system
from .. import utils
class Installer:
class Installer(object):
"""Simple installer for one application."""
appname = None
@@ -43,20 +42,6 @@ class Installer:
self.dbuser = self.config.get(self.appname, "dbuser")
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
def config_dir(self):
"""Return main configuration directory."""
@@ -146,7 +131,7 @@ class Installer:
return
exitcode, output = package.backend.install_many(packages)
if exitcode:
utils.error("Failed to install dependencies")
utils.printcolor("Failed to install dependencies", utils.RED)
sys.exit(1)
def get_config_files(self):

View File

@@ -15,7 +15,7 @@ class Clamav(base.Installer):
packages = {
"deb": ["clamav-daemon"],
"rpm": [
"clamav", "clamav-update", "clamav-server", "clamav-server-systemd"
"clamav", "clamav-update", "clamd"
],
}
@@ -57,7 +57,7 @@ class Clamav(base.Installer):
# Check if not present before
path = "/usr/lib/systemd/system/clamd@.service"
code, output = utils.exec_cmd(
r"grep 'WantedBy\s*=\s*multi-user.target' {}".format(path))
"grep 'WantedBy=multi-user.target' {}".format(path))
if code:
utils.exec_cmd(
"""cat <<EOM >> {}

View File

@@ -30,12 +30,6 @@ class Dovecot(base.Installer):
"conf.d/10-master.conf", "conf.d/20-lmtp.conf", "conf.d/10-ssl-keys.try"]
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):
"""Additional config files."""
return self.config_files + [
@@ -64,7 +58,7 @@ class Dovecot(base.Installer):
def get_template_context(self):
"""Additional variables."""
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"}
ssl_protocol_parameter = "ssl_protocols"
if package.backend.get_installed_version(dovecot_package[package.backend.FORMAT]) > "2.3":
@@ -83,12 +77,10 @@ class Dovecot(base.Installer):
else:
# Protocols are automatically guessed on debian/ubuntu
protocols = ""
context.update({
"db_driver": self.db_driver,
"mailboxes_owner_uid": pw_mailbox[2],
"mailboxes_owner_gid": pw_mailbox[3],
"mailbox_owner": self.mailboxes_owner,
"mailboxes_owner_uid": pw[2],
"mailboxes_owner_gid": pw[3],
"modoboa_user": self.config.get("modoboa", "user"),
"modoboa_dbname": self.config.get("modoboa", "dbname"),
"modoboa_dbuser": self.config.get("modoboa", "dbuser"),
@@ -98,9 +90,7 @@ class Dovecot(base.Installer):
"ssl_protocol_parameter": ssl_protocol_parameter,
"radicale_user": self.config.get("radicale", "user"),
"radicale_auth_socket_path": os.path.basename(
self.config.get("dovecot", "radicale_auth_socket_path")),
"modoboa_2_2_or_greater": "" if self.modoboa_2_2_or_greater else "#",
"not_modoboa_2_2_or_greater": "" if not self.modoboa_2_2_or_greater else "#"
self.config.get("dovecot", "radicale_auth_socket_path"))
})
return context
@@ -123,12 +113,12 @@ class Dovecot(base.Installer):
utils.copy_file(f, "{}/conf.d".format(self.config_dir))
# Make postlogin script executable
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.
# See https://github.com/modoboa/modoboa/issues/2157.
system.add_user_to_group(self.mailboxes_owner, 'dovecot')
system.add_user_to_group(
self.config.get("dovecot", "mailboxes_owner"),
'dovecot'
)
def restart_daemon(self):
"""Restart daemon process.
@@ -171,10 +161,10 @@ class Dovecot(base.Installer):
shutil.copytree(mail_dir, home_dir)
# Resetting permission for vmail
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:
shutil.chown(os.path.join(dirpath, filename),
self.mailboxes_owner, self.mailboxes_owner)
self.user, self.user)
else:
utils.printcolor(
"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
domains = *
#debug=yes
#logfile = /srv/automx/automx.log
# Protect against DoS
memcache = 127.0.0.1:11211
memcache_ttl = 600
@@ -19,8 +16,6 @@ host = %sql_dsn
query = %sql_query
result_attrs = display_name, email
display_name = ${display_name}
smtp = yes
smtp_server = %hostname
smtp_port = 587
@@ -37,3 +32,10 @@ imap_encryption = starttls
imap_auth = plaintext
imap_auth_identity = ${email}
imap_refresh_ttl = 6
pop = yes
pop_server = %hostname
pop_port = 110
pop_encryption = starttls
pop_auth = plaintext
pop_auth_identity = ${email}

View File

@@ -92,14 +92,14 @@ service postlogin {
service stats {
# To allow modoboa to access available cipher list.
unix_listener stats-reader {
user = %{mailboxes_owner}
group = %{mailboxes_owner}
user = vmail
group = vmail
mode = 0660
}
unix_listener stats-writer {
user = %{mailboxes_owner}
group = %{mailboxes_owner}
user = vmail
group = vmail
mode = 0660
}
}
@@ -120,7 +120,7 @@ service auth {
# permissions (e.g. 0777 allows everyone full permissions).
unix_listener auth-userdb {
#mode = 0666
user = %{mailboxes_owner}
user = vmail
#group =
}
@@ -154,7 +154,7 @@ service dict {
# For example: mode=0660, group=vmail and global mail_access_groups=vmail
unix_listener dict {
mode = 0600
user = %{mailboxes_owner}
user = vmail
#group =
}
}

View File

@@ -123,8 +123,7 @@ connect = host=%dbhost port=%dbport dbname=%modoboa_dbname user=%modoboa_dbuser
#user_query = \
# SELECT home, uid, gid \
# 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'
%{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'
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'
# 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
@@ -134,8 +133,7 @@ connect = host=%dbhost port=%dbport dbname=%modoboa_dbname user=%modoboa_dbuser
# SELECT userid AS user, password, \
# home AS userdb_home, uid AS userdb_uid, gid AS userdb_gid \
# 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
%{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
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
# Query to get a list of all usernames.
#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 = \
# SELECT home, uid, gid \
# 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'
%{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'
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'
# 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
@@ -134,8 +133,7 @@ connect = host=%dbhost port=%dbport dbname=%modoboa_dbname user=%modoboa_dbuser
# SELECT userid AS user, password, \
# home AS userdb_home, uid AS userdb_uid, gid AS userdb_gid \
# 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
%{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
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
# Query to get a list of all usernames.
#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

@@ -33,4 +33,4 @@ INSTANCE=%{instance_path}
%{minutes} %{hours} * * * root $PYTHON $INSTANCE/manage.py communicate_with_public_api
# 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=%{opendkim_user}
redirect_stderr=true
numprocs=1
stopsignal=TERM

View File

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

View File

@@ -10,8 +10,8 @@ server {
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
listen 443 ssl;
listen [::]:443 ssl;
server_name %hostname;
root %app_instance_path;

View File

@@ -41,7 +41,7 @@ smtpd_tls_auth_only = no
smtpd_tls_CApath = /etc/ssl/certs
smtpd_tls_key_file = %tls_key_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_session_cache_database = btree:$data_directory/smtpd_tls_session_cache
smtpd_tls_security_level = may
@@ -57,11 +57,6 @@ smtpd_tls_exclude_ciphers = aNULL, MD5 , DES, ADH, RC4, PSD, SRP, 3DES, eNULL
# Enable elliptic curve cryptography
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.
smtp_tls_CApath = /etc/ssl/certs
smtp_tls_security_level = may
@@ -72,10 +67,10 @@ smtp_tls_exclude_ciphers = EXPORT, LOW
#
%{dovecot_enabled}virtual_transport = lmtp:unix:private/dovecot-lmtp
%{dovecot_enabled}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
%{dovecot_enabled}virtual_alias_maps =
%{dovecot_enabled} proxy:%{db_driver}:/etc/postfix/sql-aliases.cf
virtual_mailbox_domains = proxy:%{db_driver}:/etc/postfix/sql-domains.cf
virtual_alias_domains = proxy:%{db_driver}:/etc/postfix/sql-domain-aliases.cf
virtual_alias_maps =
proxy:%{db_driver}:/etc/postfix/sql-aliases.cf
## Relay domains
#
@@ -150,6 +145,7 @@ postscreen_dnsbl_sites =
zen.spamhaus.org=127.0.0.[2..11]*3
bl.spameatingmonkey.net=127.0.0.2*2
bl.spamcop.net=127.0.0.2
dnsbl.sorbs.net=127.0.0.[2..15]
postscreen_dnsbl_threshold = 3
postscreen_dnsbl_action = enforce

View File

@@ -78,7 +78,7 @@ scache unix - - - - 1 scache
# Also specify in main.cf: maildrop_destination_recipient_limit=1
#
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}
#
# ====================================================================
#
@@ -149,4 +149,4 @@ autoreply unix - n n - - pipe
%{amavis_enabled} -o smtpd_client_connection_count_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 local_header_rewrite_clients=permit_mynetworks,permit_sasl_authenticated
%{amavis_enabled} -o local_header_rewrite_clients=

View File

@@ -26,8 +26,7 @@ class Modoboa(base.Installer):
"deb": [
"build-essential", "python3-dev", "libxml2-dev", "libxslt-dev",
"libjpeg-dev", "librrd-dev", "rrdtool", "libffi-dev", "cron",
"libssl-dev", "redis-server", "supervisor", "pkg-config",
"libcairo2-dev"
"libssl-dev", "redis-server", "supervisor"
],
"rpm": [
"gcc", "gcc-c++", "python3-devel", "libxml2-devel", "libxslt-devel",
@@ -44,7 +43,7 @@ class Modoboa(base.Installer):
def __init__(self, *args, **kwargs):
"""Get configuration."""
super().__init__(*args, **kwargs)
super(Modoboa, self).__init__(*args, **kwargs)
self.venv_path = self.config.get("modoboa", "venv_path")
self.instance_path = self.config.get("modoboa", "instance_path")
self.extensions = self.config.get("modoboa", "extensions").split()
@@ -61,35 +60,24 @@ class Modoboa(base.Installer):
self.extensions.remove("modoboa-radicale")
self.dovecot_enabled = self.config.getboolean("dovecot", "enabled")
self.opendkim_enabled = self.config.getboolean("opendkim", "enabled")
self.dkim_cron_enabled = False
def is_extension_ok_for_version(self, extension, 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)
if extension in compatibility_matrix.EXTENSIONS_AVAILABILITY:
min_version = compatibility_matrix.EXTENSIONS_AVAILABILITY[extension]
min_version = utils.convert_version_to_int(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):
"""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"]
version = self.config.get("modoboa", "version")
if version == "latest":
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:
matrix = compatibility_matrix.COMPATIBILITY_MATRIX[version]
packages.append("modoboa=={}".format(version))
@@ -99,41 +87,36 @@ class Modoboa(base.Installer):
continue
if extension in matrix:
req_version = matrix[extension]
if req_version is None:
continue
req_version = req_version.replace("<", "\<")
req_version = req_version.replace(">", "\>")
packages.append("{}{}".format(extension, req_version))
else:
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:
# Add extra packages to fix the SNI issue
packages += ["pyOpenSSL"]
# Temp fix for https://github.com/modoboa/modoboa/issues/2247
packages.append("django-webpack-loader==0.7.0")
python.install_packages(
packages, self.venv_path,
upgrade=self.upgrade,
sudo_user=self.user,
beta=self.config.getboolean("modoboa", "install_beta")
)
# Install version specific modules to the venv
modoboa_version = ".".join(str(i) for i in python.get_package_version(
"modoboa", self.venv_path, sudo_user=self.user
))
# Database:
db_file = "postgresql"
if self.dbengine != "postgres":
db_file = "mysql"
db_file += "-requirements.txt"
python.install_package_from_remote_requirements(
f"https://raw.githubusercontent.com/modoboa/modoboa/{modoboa_version}/{db_file}",
venv=self.venv_path)
# Dev mode:
if self.devmode:
python.install_package_from_remote_requirements(
f"https://raw.githubusercontent.com/modoboa/modoboa/{modoboa_version}/dev-requirements.txt",
venv=self.venv_path)
# 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):
"""Deploy Modoboa."""
@@ -194,7 +177,7 @@ class Modoboa(base.Installer):
if self.upgrade and self.opendkim_enabled and self.dbengine == "postgres":
# Restore view previously deleted
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.backend.grant_right_on_table(
@@ -204,7 +187,7 @@ class Modoboa(base.Installer):
def setup_database(self):
"""Additional config."""
super().setup_database()
super(Modoboa, self).setup_database()
if not self.amavis_enabled:
return
self.backend.grant_access(
@@ -212,7 +195,7 @@ class Modoboa(base.Installer):
def get_packages(self):
"""Include extra packages if needed."""
packages = super().get_packages()
packages = super(Modoboa, self).get_packages()
condition = (
package.backend.FORMAT == "rpm" and
sys.version_info.major == 2 and
@@ -222,10 +205,6 @@ class Modoboa(base.Installer):
packages += ["openssl-devel"]
return packages
def setup_user(self):
super().setup_user()
self._setup_venv()
def get_config_files(self):
"""Return appropriate path."""
config_files = super().get_config_files()
@@ -234,13 +213,6 @@ class Modoboa(base.Installer):
else:
path = "supervisor=/etc/supervisord.d/policyd.ini"
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
def get_template_context(self):
@@ -249,8 +221,6 @@ class Modoboa(base.Installer):
extensions = self.config.get("modoboa", "extensions")
extensions = extensions.split()
random_hour = random.randint(0, 6)
self.dkim_cron_enabled = (not self.modoboa_2_2_or_greater and
self.opendkim_enabled)
context.update({
"sudo_user": (
"uwsgi" if package.backend.FORMAT == "rpm" else context["user"]
@@ -261,9 +231,7 @@ class Modoboa(base.Installer):
"" if "modoboa-radicale" in extensions else "#"),
"opendkim_user": self.config.get("opendkim", "user"),
"minutes": random.randint(1, 59),
"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 "#"
"hours" : f"{random_hour},{random_hour+12}"
})
return context
@@ -275,7 +243,7 @@ class Modoboa(base.Installer):
self.instance_path, "media", "webmail")
pw = pwd.getpwnam(self.user)
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 = {
"admin": {
"handle_mailboxes": True,
@@ -287,7 +255,7 @@ class Modoboa(base.Installer):
"maillog": {
"rrd_rootdir": rrd_root_dir,
},
"pdfcredentials": {
"modoboa_pdfcredentials": {
"storage_dir": pdf_storage_dir
},
"modoboa_radicale": {
@@ -313,18 +281,17 @@ class Modoboa(base.Installer):
def post_run(self):
"""Additional tasks."""
if 'centos' in utils.dist_name():
system.enable_and_start_service("redis")
else:
system.enable_and_start_service("redis-server")
self._setup_venv()
self._deploy_instance()
if not self.upgrade:
self.apply_settings()
if 'centos' in utils.dist_name():
supervisor = "supervisord"
system.enable_and_start_service("redis")
else:
supervisor = "supervisor"
system.enable_and_start_service("redis-server")
# Restart supervisor
system.enable_service(supervisor)
utils.exec_cmd("service {} stop".format(supervisor))

View File

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

View File

@@ -82,13 +82,11 @@ class Opendkim(base.Installer):
"""Additional tasks.
Check linux distribution (package deb, rpm), to adapt
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
"""
if package.backend.FORMAT == "deb":
params_file = "/etc/default/opendkim"
else:
params_file = "/etc/opendkim.conf"
pattern = r"s/^(SOCKET=.*)/#\1/"
utils.exec_cmd(
"perl -pi -e '{}' {}".format(pattern, params_file))
@@ -115,20 +113,19 @@ class Opendkim(base.Installer):
"""Restore keys."""
dkim_keys_backup = os.path.join(
self.archive_path, "custom/dkim")
keys_storage_dir = self.app_config["keys_storage_dir"]
if os.path.isdir(dkim_keys_backup):
for file in os.listdir(dkim_keys_backup):
file_path = os.path.join(dkim_keys_backup, file)
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")
# Setup permissions
user = self.config.get("opendkim", "user")
utils.exec_cmd(f"chown -R {user}:{user} {keys_storage_dir}")
def custom_backup(self, path):
"""Backup DKIM keys."""
if os.path.isdir(self.app_config["keys_storage_dir"]):
shutil.copytree(self.app_config["keys_storage_dir"], os.path.join(path, "dkim"))
storage_dir = self.config.get(
"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(
"DKIM keys saved!", utils.GREEN)

View File

@@ -14,6 +14,7 @@ from . import backup, install
class Postfix(base.Installer):
"""Postfix installer."""
appname = "postfix"
@@ -33,24 +34,13 @@ class Postfix(base.Installer):
def install_packages(self):
"""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(
"postfix", "main_mailer_type", "select", "No configuration")
super(Postfix, self).install_packages()
def get_template_context(self):
"""Additional variables."""
context = super().get_template_context()
context = super(Postfix, self).get_template_context()
context.update({
"db_driver": self.db_driver,
"dovecot_mailboxes_owner": self.config.get(
@@ -64,13 +54,6 @@ class Postfix(base.Installer):
})
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):
"""Additional tasks."""
venv_path = self.config.get("modoboa", "venv_path")
@@ -92,8 +75,10 @@ class Postfix(base.Installer):
if not os.path.exists(path):
utils.copy_file(os.path.join("/etc", f), path)
# Generate DHE group
self.check_dhe_group_file()
# Generate EDH parameters
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
aliases_file = "/etc/aliases"

View File

@@ -47,10 +47,8 @@ class Postwhite(base.Installer):
self.install_from_archive(SPF_TOOLS_REPOSITORY, install_dir)
self.postw_dir = self.install_from_archive(
POSTWHITE_REPOSITORY, install_dir)
utils.copy_file(
os.path.join(self.postw_dir, "postwhite.conf"), self.config_dir)
self.postw_bin = os.path.join(self.postw_dir, "postwhite")
utils.exec_cmd("{} /etc/postwhite.conf".format(self.postw_bin))
postw_bin = os.path.join(self.postw_dir, "postwhite")
utils.exec_cmd("{} /etc/postwhite.conf".format(postw_bin))
def custom_backup(self, path):
"""Backup custom configuration if any."""
@@ -67,3 +65,6 @@ class Postwhite(base.Installer):
if os.path.isfile(postwhite_backup_configuration):
utils.copy_file(postwhite_backup_configuration, self.config_dir)
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,7 +31,8 @@ class Radicale(base.Installer):
def _setup_venv(self):
"""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 = [
"Radicale", "radicale-dovecot-auth", "pytz"
]

View File

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

View File

@@ -57,7 +57,9 @@ class Spamassassin(base.Installer):
def post_run(self):
"""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"]:
utils.exec_cmd(
"perl -pi -e 's/^CRON=0/CRON=1/' /etc/cron.daily/spamassassin")

View File

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

View File

@@ -7,7 +7,7 @@ from . import package
from . import utils
class CertificateBackend:
class CertificateBackend(object):
"""Base class."""
def __init__(self, config):
@@ -24,44 +24,13 @@ class CertificateBackend:
return False
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):
"""Create a self signed certificate."""
def __init__(self, *args, **kwargs):
"""Sanity checks."""
super().__init__(*args, **kwargs)
super(SelfSignedCertificate, self).__init__(*args, **kwargs)
if self.config.has_option("general", "tls_key_file"):
# Compatibility
return
@@ -96,7 +65,7 @@ class LetsEncryptCertificate(CertificateBackend):
def __init__(self, *args, **kwargs):
"""Update config."""
super().__init__(*args, **kwargs)
super(LetsEncryptCertificate, self).__init__(*args, **kwargs)
self.hostname = self.config.get("general", "hostname")
self.config.set("general", "tls_cert_file", (
"/etc/letsencrypt/live/{}/fullchain.pem".format(self.hostname)))
@@ -121,14 +90,14 @@ class LetsEncryptCertificate(CertificateBackend):
elif "centos" in name:
package.backend.install("certbot")
else:
utils.printcolor("Failed to install certbot, aborting.")
utils.printcolor("Failed to install certbot, aborting.", utils.RED)
sys.exit(1)
# Nginx plugin certbot
if (
self.config.has_option("nginx", "enabled") and
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")
def generate_cert(self):
@@ -146,24 +115,12 @@ class LetsEncryptCertificate(CertificateBackend):
cfg_file = "/etc/letsencrypt/renewal/{}.conf".format(self.hostname)
pattern = "s/authenticator = standalone/authenticator = nginx/"
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):
"""Return the appropriate backend."""
cert_type = config.get("certificate", "type")
if cert_type == "letsencrypt":
if not config.getboolean("certificate", "generate"):
return None
if config.get("certificate", "type") == "letsencrypt":
return LetsEncryptCertificate(config)
if cert_type == "manual":
return ManualCertificate(config)
return SelfSignedCertificate(config)

View File

@@ -42,15 +42,13 @@ def user_input(message):
return answer
def exec_cmd(cmd, sudo_user=None, login=True, **kwargs):
"""
Execute a shell command.
def exec_cmd(cmd, sudo_user=None, pinput=None, login=True, **kwargs):
"""Execute a shell command.
Run a command using the current user. Set :keyword:`sudo_user` if
you need different privileges.
:param str cmd: the command to execute
:param str sudo_user: a valid system username
:param str pinput: data to send to process's stdin
:rtype: tuple
:return: return code, command output
"""
@@ -59,21 +57,23 @@ def exec_cmd(cmd, sudo_user=None, login=True, **kwargs):
cmd = "sudo {}-u {} {}".format("-i " if login else "", sudo_user, cmd)
if "shell" not in kwargs:
kwargs["shell"] = True
capture_output = True
if pinput is not None:
kwargs["stdin"] = subprocess.PIPE
capture_output = False
if "capture_output" in kwargs:
capture_output = kwargs.pop("capture_output")
elif not ENV.get("debug"):
capture_output = True
if capture_output:
kwargs.update(stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
kwargs["universal_newlines"] = True
output: str = ""
with subprocess.Popen(cmd, **kwargs) as process:
if capture_output:
for line in process.stdout:
output += line
if ENV.get("debug"):
sys.stdout.write(line)
return process.returncode, output.encode()
kwargs.update(stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output = None
process = subprocess.Popen(cmd, **kwargs)
if pinput or capture_output:
c_args = [pinput] if pinput is not None else []
output = process.communicate(*c_args)[0]
else:
process.wait()
return process.returncode, output
def dist_info():
@@ -135,6 +135,7 @@ def settings(**kwargs):
class ConfigFileTemplate(string.Template):
"""Custom class for configuration files."""
delimiter = "%"
@@ -176,7 +177,7 @@ def check_config_file(dest, interactive=False, upgrade=False, backup=False, rest
"""Create a new installer config file if needed."""
is_present = True
if os.path.exists(dest):
return is_present, update_config(dest, False)
return is_present
if upgrade:
printcolor(
"You cannot upgrade an existing installation without a "
@@ -197,7 +198,7 @@ def check_config_file(dest, interactive=False, upgrade=False, backup=False, rest
"Configuration file {} not found, creating new one."
.format(dest), YELLOW)
gen_config(dest, interactive)
return is_present, None
return is_present
def has_colours(stream):
@@ -316,22 +317,11 @@ def get_entry_value(entry, interactive):
if entry.get("values") and 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
def load_config_template(interactive):
"""Instantiate a configParser object with the predefined template."""
def gen_config(dest, interactive=False):
"""Create config file from dict template"""
tpl_dict = config_dict_template.ConfigDictTemplate
config = configparser.ConfigParser()
# only ask about options we need, else still generate default
@@ -347,82 +337,6 @@ def load_config_template(interactive):
for config_entry in section["values"]:
value = get_entry_value(config_entry, interactive_section)
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):
"""Create config file from dict template"""
config = load_config_template(interactive)
with open(dest, "w") as configfile:
config.write(configfile)

76
run.py
View File

@@ -11,7 +11,6 @@ except ImportError:
import ConfigParser as configparser
import sys
import checks
from modoboa_installer import compatibility_matrix
from modoboa_installer import constants
from modoboa_installer import package
@@ -23,7 +22,6 @@ from modoboa_installer import utils
PRIMARY_APPS = [
"amavis",
"fail2ban",
"modoboa",
"automx",
"radicale",
@@ -38,23 +36,17 @@ PRIMARY_APPS = [
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(
" IN MX {}.\n".format(
args.domain,
hostname.replace(".{}".format(args.domain), ""),
hostname
),
utils.YELLOW
utils.CYAN
)
utils.printcolor(
"Your mail server will be installed with the following components:",
@@ -120,21 +112,9 @@ def backup_system(config, args):
utils.copy_file(args.configfile, backup_path)
# Backup applications
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)
def config_file_update_complete(backup_location):
utils.printcolor("Update complete. It seems successful.",
utils.BLUE)
if backup_location is not None:
utils.printcolor("You will find your old config file "
f"here: {backup_location}",
utils.BLUE)
def main(input_args):
"""Install process."""
parser = argparse.ArgumentParser()
@@ -172,19 +152,12 @@ def main(input_args):
parser.add_argument(
"--silent-backup", action="store_true", default=False,
help="For script usage, do not require user interaction "
"backup will be saved at ./modoboa_backup/Backup_M_Y_d_H_M "
"if --backup-path is not provided")
parser.add_argument(
"--no-mail", action="store_true", default=False,
help="Disable mail backup (save space)")
"backup will be saved at ./modoboa_backup/Backup_M_Y_d_H_M if --backup-path is not provided")
parser.add_argument(
"--restore", type=str, metavar="path",
help="Restore a previously backup up modoboa instance on a NEW machine. "
"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,
help="The main domain of your future mail server")
args = parser.parse_args(input_args)
@@ -204,14 +177,7 @@ def main(input_args):
sys.exit(1)
utils.success("Welcome to Modoboa installer!\n")
# 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(
is_config_file_available = utils.check_config_file(
args.configfile, args.interactive, args.upgrade, args.backup, is_restoring)
if not is_config_file_available and (
@@ -219,20 +185,6 @@ def main(input_args):
utils.error("No config file found.")
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:
return
@@ -263,7 +215,7 @@ def main(input_args):
components = []
for section in config.sections():
if section in ["general", "database", "mysql", "postgres",
"certificate", "letsencrypt", "backup"]:
"certificate", "letsencrypt"]:
continue
if (config.has_option(section, "enabled") and
not config.getboolean(section, "enabled")):
@@ -286,7 +238,10 @@ def main(input_args):
ssl_backend.generate_cert()
for appname in PRIMARY_APPS:
scripts.install(appname, config, args.upgrade, args.restore)
if package.backend.FORMAT == "deb":
system.restart_service("cron")
else:
system.restart_service("crond")
package.backend.restore_system()
if not args.restore:
utils.success(
@@ -298,19 +253,6 @@ def main(input_args):
"Restore complete! You can enjoy Modoboa at https://{} (same credentials as before)"
.format(config.get("general", "hostname"))
)
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"
)
if __name__ == "__main__":

View File

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

View File

@@ -6,13 +6,8 @@ import sys
import tempfile
import unittest
from io import StringIO
from pathlib import Path
try:
import configparser
except ImportError:
import ConfigParser as configparser
from six import StringIO
from six.moves import configparser
try:
from unittest.mock import patch
except ImportError:
@@ -62,39 +57,6 @@ class ConfigFileTestCase(unittest.TestCase):
self.assertEqual(config.get("certificate", "type"), "self-signed")
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")
def test_interactive_mode_letsencrypt(self, mock_user_input):
"""Check interactive mode."""
@@ -130,9 +92,6 @@ class ConfigFileTestCase(unittest.TestCase):
" postwhite spamassassin uwsgi",
out.getvalue()
)
self.assertNotIn("It seems that your config file is outdated.",
out.getvalue()
)
@patch("modoboa_installer.utils.user_input")
def test_upgrade_mode(self, mock_user_input):

View File

@@ -1 +0,0 @@
c1abbe97925917d4ec62ff11a70b375d40be5147