162 Commits

Author SHA1 Message Date
Spitap
4e2e9b6ab9 Revert "Make pip quiet"
This reverts commit 7ccc871da7.
2024-02-05 12:27:17 +01:00
Spitap
7ccc871da7 Make pip quiet 2024-02-05 12:23:06 +01:00
Spitap
5f4817736f Increased verbosity 2024-02-05 11:52:26 +01:00
Spitfireap
804c20a18d fixed output not decoded 2024-02-01 13:13:06 +01:00
Spitfireap
dd32b21ce9 Improved version checking 2024-02-01 13:07:11 +01:00
Antoine Nguyen
0ccd81c92b Make sure to use int vars 2024-01-22 13:46:27 +01:00
Antoine Nguyen
715a5e3c8f Make redis available before we deploy modoboa 2024-01-12 17:33:29 +01:00
Antoine Nguyen
e7995ada3f Quickfix against SMTP smuggling
https://www.postfix.org/smtp-smuggling.html
2023-12-22 16:50:26 +01:00
Antoine Nguyen
7097e15ae9 Deploy supervisor config for new RQ worker 2023-12-22 16:43:33 +01:00
Antoine Nguyen
b7f378fc63 Disable all required lines when dovecot is not installed 2023-12-15 11:59:03 +01:00
Antoine Nguyen
8942836cfc Merge pull request #531 from modoboa/fix/postfix_dhe_group
Replace EDH key generation by DHE group file
2023-12-15 11:57:13 +01:00
Antoine Nguyen
7b990c9ff6 Replace EDH key generation by DHE group file 2023-12-15 11:55:11 +01:00
Antoine Nguyen
4a2e9f2ec6 Merge pull request #523 from modoboa/dynamic-requirements
Fetch requirements dynamically
2023-10-20 16:52:12 +02:00
Spitap
6f528c94c6 Moved block to _setup_venv() 2023-10-20 10:13:44 +02:00
Spitap
f77d6f07da Fetch requirements dynamically 2023-10-19 18:10:30 +02:00
Antoine Nguyen
960d1ad23d Merge pull request #516 from xBiei/patch-1
Typo~
2023-08-31 09:17:11 +02:00
_xB
821f72a989 Typo~ 2023-08-31 00:16:24 +03:00
Antoine Nguyen
d1e036b7b0 Merge pull request #514 from modoboa/rq
Updated for 2.2
2023-08-30 18:31:12 +02:00
Spitap
f658e5e85e Fixed escape character on dovecot config tpl 2023-08-30 17:13:59 +02:00
Spitap
9715fcc86e few fixes 2023-08-30 16:57:49 +02:00
Antoine Nguyen
23aabbfffc Updated exec_cmd to allow capturing while in debug mode 2023-08-30 14:17:04 +02:00
Spitap
4782000791 few fixes 2023-08-30 10:13:49 +02:00
Spitap
23a6101b7a fix 2023-08-30 09:58:18 +02:00
Spitap
8a0b3cda9e Added python module to base.py 2023-08-30 09:53:26 +02:00
Spitap
1a528282ce Removed duplicates 2023-08-30 09:47:10 +02:00
Spitap
b1da76cfbd Fixed venvpath 2023-08-30 09:05:42 +02:00
Spitap
941142f5f5 Fixed dkim user 2023-08-30 08:48:58 +02:00
Spitap
ef1bace29e Cleaning code the 2nd 2023-08-29 20:42:44 +02:00
Spitap
35fa19e47d Cleaning code 2023-08-29 20:41:01 +02:00
Spitap
0b0e2a4e6a Updated for 2.2 2023-08-29 20:07:26 +02:00
Antoine Nguyen
e537794af2 Merge pull request #508 from florealcab/master
Add support of Debian 12
2023-07-21 09:56:38 +02:00
Floréal Cabanettes
2cc34e9033 Merge branch 'modoboa:master' into master 2023-07-13 11:21:29 +02:00
Antoine Nguyen
393c433e9a Merge pull request #507 from samuraikid0/master
Fix http2 wrong port
2023-07-11 18:18:19 +02:00
Floréal Cabanettes
5704a0a236 Add amavis 2.13.X as a copy of 2.12.X for debian12, for postgresql too 2023-07-10 23:47:32 +02:00
Floréal Cabanettes
4b6ffa1630 Debian 12 is like debian 11 2023-07-08 07:57:47 +02:00
Floréal Cabanettes
987b43d9e9 Fix for debian12 2023-07-07 23:55:34 +02:00
Floréal Cabanettes
187790149d Add amavis 2.13.X as a copy of 2.12.X for debian12 2023-07-07 23:52:44 +02:00
Zzzz
9ad6c4db68 Fix http2 wrong port 2023-07-07 09:54:05 -11:00
Antoine Nguyen
dd668aca70 Merge pull request #500 from modoboa/http2
Added http2 for nginx
2023-06-12 16:35:12 +02:00
Spitap
6e3a232e83 Added http2 for nginx 2023-05-25 11:16:32 +02:00
Antoine Nguyen
ffb3356b46 Merge pull request #498 from modoboa/bug-fix
Fixed installation issue, Updated automx conf
2023-05-10 09:03:23 +02:00
Spitap
2873a5ae69 Updated automx config 2023-05-09 19:34:22 +02:00
Spitap
4e0b025477 added missing packages
Thanks to @ruslaan7
2023-05-09 18:30:16 +02:00
Antoine Nguyen
21435d885b Merge pull request #497 from modoboa/fix-pdf-storage
Fixed db query for pdf storage
2023-05-02 17:40:27 +02:00
Spitap
c8484406d2 Fixed db query for pdf storage 2023-05-02 08:42:02 +02:00
Antoine Nguyen
60bb5eadea Merge pull request #494 from modoboa/update-for-2.1
Updated for 2.1
2023-04-26 09:05:31 +02:00
Spitap
a6b1d9e5d8 Updated for 2.1 2023-04-26 08:21:33 +02:00
Antoine Nguyen
7752b860fa Merge pull request #492 from modoboa/fix-mysql-db-ubuntu
Added workaround for ubuntu 20 and 22
2023-04-25 17:30:17 +02:00
Antoine Nguyen
0040277380 Improved code 2023-04-25 17:28:04 +02:00
Antoine Nguyen
3767c056ca Create FUNDING.yml 2023-04-20 09:03:42 +02:00
Spitap
ff214ab8f9 Added workaround for ubuntu 20 and 22 2023-03-31 14:53:58 +02:00
Antoine Nguyen
58fc991722 Merge pull request #485 from modoboa/upgrade-config-file
Added ability to update configfile
2023-03-14 08:35:26 +01:00
Spitap
602405833c Better test 2023-03-13 14:59:30 +01:00
Spitap
85652320b6 Simplified return 2023-03-13 12:09:11 +01:00
Spitap
52bccf3393 Refactoring 2023-03-12 10:22:40 +01:00
Spitap
4cd3937fdd Updated tests 2023-03-12 00:50:34 +01:00
Spitap
6261066ccd Formating, force outdated config check 2023-03-12 00:30:04 +01:00
Spitfireap
0b29f74e08 typo, review fix 2023-03-11 12:41:16 +00:00
Spitfireap
29ff6d1933 Merge pull request #489 from softwarecreations/master
Fixed permissions of /etc/dovecot/conf.d/10-ssl-keys.try to resolve issue #2570
2023-03-10 15:19:53 +00:00
softwarecreations
9d24f17632 Fixed permissions of /etc/dovecot/conf.d/10-ssl-keys.try to resolve issue 2570
Resolves modoboa/modoboa#2570

When dovecot first starts up, root reads the conf and is able to read and load the keys in /etc/dovecot/conf.d/10-ssl-keys.try Inside that file, it can read the private key (that only root has permissions to read)

However when we try delete a user, doveconf tries to read the config (to find the user's mailbox) doveconf MUST fail to open 10-ssl-keys.try, which is fine, because 10-ssl.conf says

!include_try /etc/dovecot/conf.d/10-ssl-keys.try

So if doveconf can't open 10-ssl-keys.try it will will keep going. However if doveconf can read 10-ssl-keys.try then doveconf crashes saying something like:

Failed to retrieve mailbox location (b doveconf: Fatal: Error in configuration file /etc/dovecot/conf.d/10-ssl-keys.try line 11: ssl_key: Can't open file /etc/ssl/example.com/privkey.pem: Permission denied

And then the attempt to delete the user's mailbox fails.

According to @gsloop, "the API calls doveadm to return the directory that holds the users mailbox"

I did a new installation, the file /etc/dovecot/conf.d/10-ssl-keys.try was already owned by root:root but it had 644 permissions. So the line that I added corrects that.
2023-03-10 13:03:43 +02:00
Antoine Nguyen
b26905a97b Merge pull request #479 from n-tdi/patch-1
Make DNS understaind easier for users
2023-03-06 16:58:15 +01:00
Antoine Nguyen
a3f7b98104 Merge pull request #487 from mbaechtold/patch-1
Fix automx
2023-03-06 13:19:26 +01:00
Martin Bächtold
d547d37ece Fix automx
Relates to https://github.com/modoboa/modoboa-installer/issues/475
2023-03-05 09:27:40 +01:00
Spitap
dbfede6df1 Fixed typo, updated test 2023-03-03 09:33:32 +01:00
Spitap
335a676a1e Added ability to update configfile 2023-03-02 20:54:31 +01:00
Antoine Nguyen
06a81c7a80 Fix #481 2023-02-17 10:12:56 +01:00
Ntdi
cac6c1e7f7 Make DNS understaind easier for users
This simple change makes it easier for new users to add the DNS records they need for there Modoboa installation.
2023-02-12 17:13:35 -05:00
Antoine Nguyen
63d92b73f3 Merge pull request #474 from modoboa/security/fail2ban
Added fail2ban setup
2023-01-31 09:09:51 +01:00
Antoine Nguyen
76ec16cd45 Added missing files 2023-01-31 09:08:34 +01:00
Antoine Nguyen
5f02e1b8ed Added fail2ban setup 2023-01-30 18:02:09 +01:00
Antoine Nguyen
960f1429fd Removed temp. fix for django-webpack-loader. 2023-01-30 15:51:43 +01:00
Antoine Nguyen
8b376b0f69 Fixed typo
see #472
2023-01-24 13:02:13 +01:00
Antoine Nguyen
4fc540ddd8 Merge pull request #471 from Spitfireap/fix-dovecot-ownership
Fix dovecot ownership
2023-01-24 10:53:36 +01:00
Spitap
81129d2875 Removed globally set mail_uid and mail_gid
Co-Authored-By: Antoine Nguyen <tonio@ngyn.org>
2023-01-24 09:29:51 +01:00
Spitap
a6935bba89 Simplifeid setup_user
Co-Authored-By: Antoine Nguyen <tonio@ngyn.org>
2023-01-24 09:25:18 +01:00
Spitap
7cae12b32e Fix multiple hard-coded vmail 2023-01-23 19:24:28 +01:00
Antoine Nguyen
0fc15fc024 updated regexp
fix #312
2023-01-13 12:12:48 +01:00
Antoine Nguyen
7877de1abc Removed call to deprecated discover command
fix #403
2023-01-13 12:05:03 +01:00
Spitap
6144f7967c make use of mailbox_owner 2023-01-12 11:22:26 +01:00
Antoine Nguyen
a647edf5a5 Merge pull request #460 from Spitfireap/fix-dkim-perm
fixed dkim permissions
2023-01-10 14:07:22 +01:00
Antoine Nguyen
9f08964c59 Merge pull request #470 from Spitfireap/Fix-webmail-folder
Create subfolder on modoboas extensions install
2023-01-10 14:06:20 +01:00
Antoine Nguyen
99d229a693 Merge pull request #464 from Spitfireap/postwhite-conf-fix
Postwhite conf file not being copied
2023-01-10 14:05:23 +01:00
Spitap
cf6f34b257 Be sure to create webmail subfolder 2023-01-10 13:19:09 +01:00
Spitap
a94b5ac4b7 Refactoring 2022-12-27 20:27:28 +01:00
Spitap
4f9f433008 PEP 2022-12-27 19:56:12 +01:00
Spitap
2665e18c0a Fixed config file not copied on new install 2022-12-27 19:45:38 +01:00
Antoine Nguyen
5c22600d98 Merge pull request #462 from Spitfireap/randomize-api-call-time
randomize api call time
2022-11-29 16:54:28 +01:00
Spitap
bcdbb4a2ce fix typo 2022-11-29 14:53:05 +01:00
Spitap
bd1ddcef21 randomize api call time 2022-11-29 13:45:31 +01:00
Spitap
24f231bf1d fixed dkim permsissions 2022-11-27 13:57:35 +01:00
Antoine Nguyen
bc12ca7327 Merge pull request #458 from Spitfireap/fix-include_try
fix typo in dovecot configuration file
2022-11-14 15:49:41 +01:00
Spitap
bd0ecd0949 fix typo in dovecot configuration file 2022-11-10 14:57:43 +01:00
Antoine Nguyen
d364239348 Merge pull request #456 from modoboa/feature/improved_backup_restore
WIP: Improved backup/restore system.
2022-11-09 10:51:30 +01:00
Antoine Nguyen
37633008cb Fixed restore mode 2022-11-09 10:30:44 +01:00
Antoine Nguyen
d6f9a5b913 Few fixes. 2022-11-08 17:20:25 +01:00
Antoine Nguyen
8b1d60ee59 Few fixes 2022-11-08 17:19:23 +01:00
Antoine Nguyen
2b5edae5d5 WIP: Improved backup/restore system. 2022-11-06 10:30:24 +01:00
Antoine Nguyen
61838dbe4d Check if restore is defined before doing anything else.
fix #453
2022-11-05 09:30:50 +01:00
Antoine Nguyen
962cac3ad9 Merge pull request #450 from Spitfireap/fixed-super-call
fixed super call in modoboa's script
2022-11-04 09:41:20 +01:00
Spitap
ef2359a2a8 fixed super call 2022-11-03 23:10:21 +01:00
Antoine Nguyen
1b192c5fd5 Merge pull request #449 from Spitfireap/fixed-import-typo
fixed constants import
2022-11-03 15:34:48 +01:00
Spitap
b0b01465d9 fixed constants import 2022-11-03 15:00:07 +01:00
Antoine Nguyen
754d652fc2 Few fixes 2022-11-03 12:27:04 +01:00
Antoine Nguyen
cb5fa75693 Merge pull request #444 from Spitfireap/tighter-config-file-perm
tighter config file permission
2022-11-03 12:20:25 +01:00
Antoine Nguyen
1afb8e61fc Merge pull request #424 from Spitfireap/restore
Backup & restore system
2022-11-03 12:17:16 +01:00
Spitap
8dd0b7d497 Last camelCase 2022-11-03 10:57:03 +01:00
Spitap
554611b366 review fix 2022-11-03 10:54:06 +01:00
Antoine Nguyen
15c17796f2 Merge pull request #446 from Spitfireap/fix-ssl-min-protocol
fixed ssl_min_protocol setting
2022-10-28 09:43:30 +02:00
Spitap
84d13633a1 fixed ssl_min_protocol setting 2022-10-27 22:37:47 +02:00
Antoine Nguyen
ce8e7e6027 Merge pull request #445 from Spitfireap/dovecot-fixes
Fixes ssl permission error, updated ssl_protocol parameter
2022-10-27 17:56:37 +02:00
Spitap
e01265a4ee Merge branch 'tighter-config-file-perm' of https://github.com/Spitfireap/modoboa-installer into tighter-config-file-perm 2022-10-27 17:44:37 +02:00
Spitap
a5fba03264 tighter config file permission 2022-10-27 17:44:29 +02:00
Spitap
fe7df276fc Check dovecot version greater 2022-10-27 17:25:39 +02:00
Spitap
8f34f0af6f Fixes ssl permission error, updated ssl_protocol parameter 2022-10-27 17:00:58 +02:00
Antoine Nguyen
8e8ae5fb9c Merge pull request #439 from stefaweb/master
Update config_dict_template.py for default max_servers value
2022-10-27 16:49:20 +02:00
Spitap
235ef3befb thighter config file permission 2022-10-27 11:14:06 +02:00
Antoine Nguyen
67f6cee8ea Merge pull request #442 from Spitfireap/patch-1
Set $max_server to 2 to avoid amavis crash
2022-10-25 19:32:37 +02:00
Spitap
5c9d5c9a03 DKIM keys restore, Radicale backup/restore, fixes 2022-10-25 16:58:57 +02:00
Spitap
4c1f8710b5 Added dkim key backup 2022-10-25 16:04:55 +02:00
Spitap
e34eb4b337 fix database path 2022-10-25 13:59:28 +02:00
Spitfireap
53f7f8ef9d Update config_dict_template.py 2022-10-19 08:19:40 +00:00
Spitfireap
35778cd614 Merge branch 'modoboa:master' into restore 2022-10-18 17:17:48 +02:00
Stephane Leclerc
fefbf549a4 Update config_dict_template.py for default max_server value 2022-10-06 13:36:13 +02:00
Spitap
6726f5b1a2 Improved path generation, path mistake proofing 2022-09-26 13:39:28 +02:00
Spitap
a192cbcbd0 Updated doc, default path on conf file 2022-09-19 16:40:25 +02:00
Spitap
5bed9655ea fixed typo 2022-09-19 15:53:19 +02:00
Spitap
6b096a7470 Simplified db dumps restore 2022-09-19 15:50:03 +02:00
Spitap
e30add03fd Update from master 2022-09-19 15:39:05 +02:00
Spitap
d75d83f202 more refactoring 2022-09-19 15:13:44 +02:00
Spitap
f3811b4b39 refactoring 2022-09-19 15:00:26 +02:00
Spitap
b0d56b3989 PEP formating 2022-09-15 11:32:57 +02:00
Spitap
53e3e3ec58 Better UX, use of os to concatenate path 2022-08-05 15:20:11 +02:00
Spitap
e546d2cb23 Better UX 2022-07-27 16:32:59 +02:00
Spitap
70faa1c5cb Fixed backupdir index 2022-07-27 15:58:41 +02:00
Spitap
563979a7dd fixed mail backup/restore 2022-07-27 15:51:22 +02:00
Spitap
ee2ccf0647 Fixed postfix install, added restore to readme 2022-07-27 14:35:48 +02:00
Spitap
2077c94b52 Fix amavis config file not copied to right location 2022-07-26 17:05:00 +02:00
Spitap
4a7222bd24 Fixed nginx call to uwsgi 2022-07-26 16:53:24 +02:00
Spitap
e7b6104195 fixed install within class 2022-07-26 16:39:41 +02:00
Spitap
4a00590354 fixed restore disclamer 2022-07-26 16:20:03 +02:00
Spitap
15768c429e Restore workflow done 2022-07-26 12:07:42 +02:00
Spitap
439ffb94c4 initial commit 2022-07-26 10:37:38 +02:00
Spitap
37bc21dfd3 Backup postewhite.conf instead of custom whitelist
Postwhite.conf contains a custom host list
2022-07-26 10:36:08 +02:00
Spitap
26204143af Merge branch 'master' into backup 2022-07-25 22:10:26 +02:00
Spitap
20970557de Allow to disable mail backup 2022-07-25 22:05:35 +02:00
Spitap
632c26596e Update backup readme 2022-07-25 21:52:15 +02:00
Spitap
9e1c18cd6b Fix argument passed as list instead of string 2022-07-21 19:09:53 +02:00
Spitap
db6457c5f5 better path handling 2022-07-21 19:07:18 +02:00
Spitap
579faccfa5 added an automatic bash option (no path provided) or a path provided bash (for cron job) 2022-07-21 19:00:32 +02:00
Spitap
5318fa279b bash option 2022-07-21 18:00:50 +02:00
Spitap
74de6a9bb1 Reset pgpass before trying to backup secondary dbs 2022-07-21 17:31:56 +02:00
Spitap
54185a7c5a Fix database backup logic issue 2022-07-21 17:26:40 +02:00
Spitap
1f9d69c37c Fix copy issue 2022-07-21 17:21:59 +02:00
Spitap
8d02d2a9fb added safe mkdir in utils, use utils.mkdir_safe() in backup 2022-07-21 17:09:23 +02:00
Spitap
6f604a5fec Fix loop logic 2022-07-21 16:53:56 +02:00
Spitap
568c4a65a0 fix none-type passed to os.path 2022-07-21 16:51:32 +02:00
Spitap
dc84a79528 Note : capitalize affects only first letter 2022-07-21 14:12:35 +02:00
Spitap
304e25fa3c Fix getattr 2022-07-21 14:10:57 +02:00
Spitap
070efd61c4 Fix import 2022-07-21 14:08:39 +02:00
Spitap
9917d8023e Edited README, fix backup run process 2022-07-21 14:02:41 +02:00
Spitap
27b9de6755 database backup 2022-07-21 13:48:44 +02:00
Spitap
56ed214fb5 Starting work on backup system 2022-07-19 19:06:53 +02:00
42 changed files with 931 additions and 184 deletions

3
.github/FUNDING.yml vendored Normal file
View File

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

View File

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

View File

@@ -11,12 +11,11 @@ An installer which deploy a complete mail server based on Modoboa.
* Debian Buster (10) / Bullseye (11) * Debian Buster (10) / Bullseye (11)
* Ubuntu Bionic Beaver (18.04) and upper * Ubuntu Bionic Beaver (18.04) and upper
* CentOS 9 Stream * CentOS 7
.. warning:: .. warning::
``/tmp`` partition must be mounted without the ``noexec`` option. ``/tmp`` partition must be mounted without the ``noexec`` option.
Centos 7 support has been depreceated since modoboa requires python 3.7>=.
.. note:: .. note::

View File

@@ -20,7 +20,12 @@ COMPATIBILITY_MATRIX = {
"modoboa-pdfcredentials": ">=1.1.1", "modoboa-pdfcredentials": ">=1.1.1",
"modoboa-sievefilters": ">=1.1.1", "modoboa-sievefilters": ">=1.1.1",
"modoboa-webmail": ">=1.2.0", "modoboa-webmail": ">=1.2.0",
} },
"2.1.0": {
"modoboa-pdfcredentials": None,
"modoboa-dmarc": None,
"modoboa-imap-migration": None,
},
} }
EXTENSIONS_AVAILABILITY = { EXTENSIONS_AVAILABILITY = {

View File

@@ -118,6 +118,31 @@ ConfigDictTemplate = [
} }
] ]
}, },
{
"name": "fail2ban",
"values": [
{
"option": "enabled",
"default": "true",
},
{
"option": "config_dir",
"default": "/etc/fail2ban"
},
{
"option": "max_retry",
"default": "20"
},
{
"option": "ban_time",
"default": "3600"
},
{
"option": "find_time",
"default": "30"
},
]
},
{ {
"name": "modoboa", "name": "modoboa",
"values": [ "values": [
@@ -158,7 +183,7 @@ ConfigDictTemplate = [
{ {
"option": "extensions", "option": "extensions",
"default": ( "default": (
"modoboa-amavis modoboa-pdfcredentials " "modoboa-amavis "
"modoboa-postfix-autoreply modoboa-sievefilters " "modoboa-postfix-autoreply modoboa-sievefilters "
"modoboa-webmail modoboa-contacts " "modoboa-webmail modoboa-contacts "
"modoboa-radicale" "modoboa-radicale"
@@ -256,7 +281,7 @@ ConfigDictTemplate = [
}, },
{ {
"option": "user", "option": "user",
"default": "vmail", "default": "dovecot",
}, },
{ {
"option": "home_dir", "option": "home_dir",
@@ -321,6 +346,10 @@ ConfigDictTemplate = [
"option": "message_size_limit", "option": "message_size_limit",
"default": "11534336", "default": "11534336",
}, },
{
"option": "dhe_group",
"default": "4096"
}
] ]
}, },
{ {

View File

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

View File

@@ -46,7 +46,7 @@ class DEBPackage(Package):
"""Update local cache.""" """Update local cache."""
if self.index_updated: if self.index_updated:
return return
utils.exec_cmd("apt-get update --quiet") utils.exec_cmd("apt-get -o Dpkg::Progress-Fancy=0 update --quiet")
self.index_updated = True self.index_updated = True
def preconfigure(self, name, question, qtype, answer): def preconfigure(self, name, question, qtype, answer):
@@ -57,18 +57,18 @@ class DEBPackage(Package):
def install(self, name): def install(self, name):
"""Install a package.""" """Install a package."""
self.update() self.update()
utils.exec_cmd("apt-get install --quiet --assume-yes {}".format(name)) utils.exec_cmd("apt-get -o Dpkg::Progress-Fancy=0 install --quiet --assume-yes {}".format(name))
def install_many(self, names): def install_many(self, names):
"""Install many packages.""" """Install many packages."""
self.update() self.update()
return utils.exec_cmd("apt-get install --quiet --assume-yes {}".format( return utils.exec_cmd("apt-get -o Dpkg::Progress-Fancy=0 install --quiet --assume-yes {}".format(
" ".join(names))) " ".join(names)))
def get_installed_version(self, name): def get_installed_version(self, name):
"""Get installed package version.""" """Get installed package version."""
code, output = utils.exec_cmd( code, output = utils.exec_cmd(
"dpkg -s {} | grep Version".format(name), capture_output=True) "dpkg -s {} | grep Version".format(name))
match = re.match(r"Version: (\d:)?(.+)-\d", output.decode()) match = re.match(r"Version: (\d:)?(.+)-\d", output.decode())
if match: if match:
return match.group(2) return match.group(2)
@@ -82,32 +82,22 @@ class RPMPackage(Package):
def __init__(self, dist_name): def __init__(self, dist_name):
"""Initialize backend.""" """Initialize backend."""
self.dist_name = dist_name
super(RPMPackage, self).__init__(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.install("epel-release")
self.update()
def update(self):
"""Update the database repo."""
utils.exec_cmd("dnf update -y --quiet")
def install(self, name): def install(self, name):
"""Install a package.""" """Install a package."""
"""Need to add check for rrdtool, sendmail-milter, libmemcached and --enablerepo=crb""" utils.exec_cmd("yum install -y --quiet {}".format(name))
utils.exec_cmd("dnf install -y --quiet {}".format(name))
def install_many(self, names): def install_many(self, names):
"""Install many packages.""" """Install many packages."""
return utils.exec_cmd("dnf install -y --quiet {}".format(" ".join(names))) return utils.exec_cmd("yum install -y --quiet {}".format(" ".join(names)))
def get_installed_version(self, name): def get_installed_version(self, name):
"""Get installed package version.""" """Get installed package version."""
code, output = utils.exec_cmd( code, output = utils.exec_cmd(
"rpm -qi {} | grep Version".format(name), capture_output=True) "rpm -qi {} | grep Version".format(name))
match = re.match(r"Version\s+: (.+)", output.decode()) match = re.match(r"Version\s+: (.+)", output.decode())
if match: if match:
return match.group(1) return match.group(1)

View File

@@ -1,6 +1,8 @@
"""Python related tools.""" """Python related tools."""
import json
import os import os
import sys
from . import package from . import package
from . import utils from . import utils
@@ -45,6 +47,34 @@ def install_packages(names, venv=None, upgrade=False, **kwargs):
utils.exec_cmd(cmd, **kwargs) utils.exec_cmd(cmd, **kwargs)
def get_package_version(name, venv=None, **kwargs):
"""Returns the version of an installed package."""
cmd = f"{get_pip_path(venv)} list --format json"
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)
print(f"name: {name}, venv: {venv}, cmd: {cmd}, exit_code: {exit_code}, output: {output.decode()}")
list_dict = json.loads(output.decode())
version_list = []
for element in list_dict:
if element["name"] == name:
version_list = element["version"].split(".")
break
version_list_clean = []
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)
return version_list_clean
def install_package_from_repository(name, url, vcs="git", venv=None, **kwargs): def install_package_from_repository(name, url, vcs="git", venv=None, **kwargs):
"""Install a Python package from its repository.""" """Install a Python package from its repository."""
if vcs == "git": if vcs == "git":
@@ -54,6 +84,16 @@ def install_package_from_repository(name, url, vcs="git", venv=None, **kwargs):
utils.exec_cmd(cmd, **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, python_version=2): def setup_virtualenv(path, sudo_user=None, python_version=2):
"""Install a virtualenv if needed.""" """Install a virtualenv if needed."""
if os.path.exists(path): if os.path.exists(path):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -30,6 +30,12 @@ class Dovecot(base.Installer):
"conf.d/10-master.conf", "conf.d/20-lmtp.conf", "conf.d/10-ssl-keys.try"] "conf.d/10-master.conf", "conf.d/20-lmtp.conf", "conf.d/10-ssl-keys.try"]
with_user = True with_user = True
def setup_user(self):
"""Setup mailbox user."""
super().setup_user()
self.mailboxes_owner = self.app_config["mailboxes_owner"]
system.create_user(self.mailboxes_owner, self.home_dir)
def get_config_files(self): def get_config_files(self):
"""Additional config files.""" """Additional config files."""
return self.config_files + [ return self.config_files + [
@@ -58,7 +64,7 @@ class Dovecot(base.Installer):
def get_template_context(self): def get_template_context(self):
"""Additional variables.""" """Additional variables."""
context = super(Dovecot, self).get_template_context() context = super(Dovecot, self).get_template_context()
pw = pwd.getpwnam(self.user) pw_mailbox = pwd.getpwnam(self.mailboxes_owner)
dovecot_package = {"deb": "dovecot-core", "rpm": "dovecot"} dovecot_package = {"deb": "dovecot-core", "rpm": "dovecot"}
ssl_protocol_parameter = "ssl_protocols" ssl_protocol_parameter = "ssl_protocols"
if package.backend.get_installed_version(dovecot_package[package.backend.FORMAT]) > "2.3": if package.backend.get_installed_version(dovecot_package[package.backend.FORMAT]) > "2.3":
@@ -77,10 +83,12 @@ class Dovecot(base.Installer):
else: else:
# Protocols are automatically guessed on debian/ubuntu # Protocols are automatically guessed on debian/ubuntu
protocols = "" protocols = ""
context.update({ context.update({
"db_driver": self.db_driver, "db_driver": self.db_driver,
"mailboxes_owner_uid": pw[2], "mailboxes_owner_uid": pw_mailbox[2],
"mailboxes_owner_gid": pw[3], "mailboxes_owner_gid": pw_mailbox[3],
"mailbox_owner": self.mailboxes_owner,
"modoboa_user": self.config.get("modoboa", "user"), "modoboa_user": self.config.get("modoboa", "user"),
"modoboa_dbname": self.config.get("modoboa", "dbname"), "modoboa_dbname": self.config.get("modoboa", "dbname"),
"modoboa_dbuser": self.config.get("modoboa", "dbuser"), "modoboa_dbuser": self.config.get("modoboa", "dbuser"),
@@ -90,7 +98,9 @@ class Dovecot(base.Installer):
"ssl_protocol_parameter": ssl_protocol_parameter, "ssl_protocol_parameter": ssl_protocol_parameter,
"radicale_user": self.config.get("radicale", "user"), "radicale_user": self.config.get("radicale", "user"),
"radicale_auth_socket_path": os.path.basename( "radicale_auth_socket_path": os.path.basename(
self.config.get("dovecot", "radicale_auth_socket_path")) 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 "#"
}) })
return context return context
@@ -113,12 +123,12 @@ class Dovecot(base.Installer):
utils.copy_file(f, "{}/conf.d".format(self.config_dir)) utils.copy_file(f, "{}/conf.d".format(self.config_dir))
# Make postlogin script executable # Make postlogin script executable
utils.exec_cmd("chmod +x /usr/local/bin/postlogin.sh") utils.exec_cmd("chmod +x /usr/local/bin/postlogin.sh")
# Only root should have read access to the 10-ssl-keys.try
# See https://github.com/modoboa/modoboa/issues/2570
utils.exec_cmd("chmod 600 /etc/dovecot/conf.d/10-ssl-keys.try")
# Add mailboxes user to dovecot group for modoboa mailbox commands. # Add mailboxes user to dovecot group for modoboa mailbox commands.
# See https://github.com/modoboa/modoboa/issues/2157. # See https://github.com/modoboa/modoboa/issues/2157.
system.add_user_to_group( system.add_user_to_group(self.mailboxes_owner, 'dovecot')
self.config.get("dovecot", "mailboxes_owner"),
'dovecot'
)
def restart_daemon(self): def restart_daemon(self):
"""Restart daemon process. """Restart daemon process.
@@ -161,10 +171,10 @@ class Dovecot(base.Installer):
shutil.copytree(mail_dir, home_dir) shutil.copytree(mail_dir, home_dir)
# Resetting permission for vmail # Resetting permission for vmail
for dirpath, dirnames, filenames in os.walk(home_dir): for dirpath, dirnames, filenames in os.walk(home_dir):
shutil.chown(dirpath, self.user, self.user) shutil.chown(dirpath, self.mailboxes_owner, self.mailboxes_owner)
for filename in filenames: for filename in filenames:
shutil.chown(os.path.join(dirpath, filename), shutil.chown(os.path.join(dirpath, filename),
self.user, self.user) self.mailboxes_owner, self.mailboxes_owner)
else: else:
utils.printcolor( utils.printcolor(
"It seems that emails were not backed up, skipping restoration.", "It seems that emails were not backed up, skipping restoration.",

View File

@@ -0,0 +1,17 @@
"""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

@@ -0,0 +1,213 @@
-- 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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
[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 %{minutes} %{hours} * * * root $PYTHON $INSTANCE/manage.py communicate_with_public_api
# Generate DKIM keys (they will belong to the user running this job) # Generate DKIM keys (they will belong to the user running this job)
%{opendkim_enabled}* * * * * %{opendkim_user} umask 077 && $PYTHON $INSTANCE/manage.py modo manage_dkim_keys %{dkim_cron_enabled}* * * * * %{opendkim_user} umask 077 && $PYTHON $INSTANCE/manage.py modo manage_dkim_keys

View File

@@ -0,0 +1,9 @@
[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

@@ -0,0 +1,9 @@
[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,3 +6,4 @@ directory=%{home_dir}
redirect_stderr=true redirect_stderr=true
user=%{user} user=%{user}
numprocs=1 numprocs=1

View File

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

View File

@@ -41,7 +41,7 @@ smtpd_tls_auth_only = no
smtpd_tls_CApath = /etc/ssl/certs smtpd_tls_CApath = /etc/ssl/certs
smtpd_tls_key_file = %tls_key_file smtpd_tls_key_file = %tls_key_file
smtpd_tls_cert_file = %tls_cert_file smtpd_tls_cert_file = %tls_cert_file
smtpd_tls_dh1024_param_file = ${config_directory}/dh2048.pem smtpd_tls_dh1024_param_file = ${config_directory}/ffdhe%{dhe_group}.pem
smtpd_tls_loglevel = 1 smtpd_tls_loglevel = 1
smtpd_tls_session_cache_database = btree:$data_directory/smtpd_tls_session_cache smtpd_tls_session_cache_database = btree:$data_directory/smtpd_tls_session_cache
smtpd_tls_security_level = may smtpd_tls_security_level = may
@@ -57,6 +57,11 @@ smtpd_tls_exclude_ciphers = aNULL, MD5 , DES, ADH, RC4, PSD, SRP, 3DES, eNULL
# Enable elliptic curve cryptography # Enable elliptic curve cryptography
smtpd_tls_eecdh_grade = strong smtpd_tls_eecdh_grade = strong
# SMTP Smuggling prevention
# See https://www.postfix.org/smtp-smuggling.html
smtpd_data_restrictions = reject_unauth_pipelining
smtpd_forbid_unauth_pipelining = yes
# Use TLS if this is supported by the remote SMTP server, otherwise use plaintext. # Use TLS if this is supported by the remote SMTP server, otherwise use plaintext.
smtp_tls_CApath = /etc/ssl/certs smtp_tls_CApath = /etc/ssl/certs
smtp_tls_security_level = may smtp_tls_security_level = may
@@ -67,10 +72,10 @@ smtp_tls_exclude_ciphers = EXPORT, LOW
# #
%{dovecot_enabled}virtual_transport = lmtp:unix:private/dovecot-lmtp %{dovecot_enabled}virtual_transport = lmtp:unix:private/dovecot-lmtp
virtual_mailbox_domains = proxy:%{db_driver}:/etc/postfix/sql-domains.cf %{dovecot_enabled}virtual_mailbox_domains = proxy:%{db_driver}:/etc/postfix/sql-domains.cf
virtual_alias_domains = proxy:%{db_driver}:/etc/postfix/sql-domain-aliases.cf %{dovecot_enabled}virtual_alias_domains = proxy:%{db_driver}:/etc/postfix/sql-domain-aliases.cf
virtual_alias_maps = %{dovecot_enabled}virtual_alias_maps =
proxy:%{db_driver}:/etc/postfix/sql-aliases.cf %{dovecot_enabled} proxy:%{db_driver}:/etc/postfix/sql-aliases.cf
## Relay domains ## Relay domains
# #

View File

@@ -78,7 +78,7 @@ scache unix - - - - 1 scache
# Also specify in main.cf: maildrop_destination_recipient_limit=1 # Also specify in main.cf: maildrop_destination_recipient_limit=1
# #
maildrop unix - n n - - pipe maildrop unix - n n - - pipe
flags=DRhu user=vmail argv=/usr/bin/maildrop -d ${recipient} flags=DRhu user=%{dovecot_mailboxes_owner} argv=/usr/bin/maildrop -d ${recipient}
# #
# ==================================================================== # ====================================================================
# #

View File

@@ -26,7 +26,8 @@ class Modoboa(base.Installer):
"deb": [ "deb": [
"build-essential", "python3-dev", "libxml2-dev", "libxslt-dev", "build-essential", "python3-dev", "libxml2-dev", "libxslt-dev",
"libjpeg-dev", "librrd-dev", "rrdtool", "libffi-dev", "cron", "libjpeg-dev", "librrd-dev", "rrdtool", "libffi-dev", "cron",
"libssl-dev", "redis-server", "supervisor" "libssl-dev", "redis-server", "supervisor", "pkg-config",
"libcairo2-dev"
], ],
"rpm": [ "rpm": [
"gcc", "gcc-c++", "python3-devel", "libxml2-devel", "libxslt-devel", "gcc", "gcc-c++", "python3-devel", "libxml2-devel", "libxslt-devel",
@@ -43,7 +44,7 @@ class Modoboa(base.Installer):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Get configuration.""" """Get configuration."""
super(Modoboa, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.venv_path = self.config.get("modoboa", "venv_path") self.venv_path = self.config.get("modoboa", "venv_path")
self.instance_path = self.config.get("modoboa", "instance_path") self.instance_path = self.config.get("modoboa", "instance_path")
self.extensions = self.config.get("modoboa", "extensions").split() self.extensions = self.config.get("modoboa", "extensions").split()
@@ -60,6 +61,7 @@ class Modoboa(base.Installer):
self.extensions.remove("modoboa-radicale") self.extensions.remove("modoboa-radicale")
self.dovecot_enabled = self.config.getboolean("dovecot", "enabled") self.dovecot_enabled = self.config.getboolean("dovecot", "enabled")
self.opendkim_enabled = self.config.getboolean("opendkim", "enabled") self.opendkim_enabled = self.config.getboolean("opendkim", "enabled")
self.dkim_cron_enabled = False
def is_extension_ok_for_version(self, extension, version): def is_extension_ok_for_version(self, extension, version):
"""Check if extension can be installed with this modo version.""" """Check if extension can be installed with this modo version."""
@@ -87,36 +89,41 @@ class Modoboa(base.Installer):
continue continue
if extension in matrix: if extension in matrix:
req_version = matrix[extension] req_version = matrix[extension]
if req_version is None:
continue
req_version = req_version.replace("<", "\<") req_version = req_version.replace("<", "\<")
req_version = req_version.replace(">", "\>") req_version = req_version.replace(">", "\>")
packages.append("{}{}".format(extension, req_version)) packages.append("{}{}".format(extension, req_version))
else: else:
packages.append(extension) packages.append(extension)
# Temp fix for django-braces
python.install_package(
"django-braces", self.venv_path, upgrade=self.upgrade,
sudo_user=self.user
)
if self.dbengine == "postgres":
packages.append("psycopg2-binary\<2.9")
else:
packages.append("mysqlclient")
if sys.version_info.major == 2 and sys.version_info.micro < 9: if sys.version_info.major == 2 and sys.version_info.micro < 9:
# Add extra packages to fix the SNI issue # Add extra packages to fix the SNI issue
packages += ["pyOpenSSL"] packages += ["pyOpenSSL"]
# Temp fix for https://github.com/modoboa/modoboa/issues/2247
packages.append("django-webpack-loader==0.7.0")
python.install_packages( python.install_packages(
packages, self.venv_path, packages, self.venv_path,
upgrade=self.upgrade, upgrade=self.upgrade,
sudo_user=self.user, sudo_user=self.user,
beta=self.config.getboolean("modoboa", "install_beta") beta=self.config.getboolean("modoboa", "install_beta")
) )
# 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: if self.devmode:
# FIXME: use dev-requirements instead python.install_package_from_remote_requirements(
python.install_packages( f"https://raw.githubusercontent.com/modoboa/modoboa/{modoboa_version}/dev-requirements.txt",
["django-bower", "django-debug-toolbar"], self.venv_path, venv=self.venv_path)
upgrade=self.upgrade, sudo_user=self.user)
def _deploy_instance(self): def _deploy_instance(self):
"""Deploy Modoboa.""" """Deploy Modoboa."""
@@ -177,7 +184,7 @@ class Modoboa(base.Installer):
if self.upgrade and self.opendkim_enabled and self.dbengine == "postgres": if self.upgrade and self.opendkim_enabled and self.dbengine == "postgres":
# Restore view previously deleted # Restore view previously deleted
self.backend.load_sql_file( self.backend.load_sql_file(
self.dbname, self.dbuser, self.dbpassword, self.dbname, self.dbuser, self.dbpasswd,
self.get_file_path("dkim_view_{}.sql".format(self.dbengine)) self.get_file_path("dkim_view_{}.sql".format(self.dbengine))
) )
self.backend.grant_right_on_table( self.backend.grant_right_on_table(
@@ -187,7 +194,7 @@ class Modoboa(base.Installer):
def setup_database(self): def setup_database(self):
"""Additional config.""" """Additional config."""
super(Modoboa, self).setup_database() super().setup_database()
if not self.amavis_enabled: if not self.amavis_enabled:
return return
self.backend.grant_access( self.backend.grant_access(
@@ -195,7 +202,7 @@ class Modoboa(base.Installer):
def get_packages(self): def get_packages(self):
"""Include extra packages if needed.""" """Include extra packages if needed."""
packages = super(Modoboa, self).get_packages() packages = super().get_packages()
condition = ( condition = (
package.backend.FORMAT == "rpm" and package.backend.FORMAT == "rpm" and
sys.version_info.major == 2 and sys.version_info.major == 2 and
@@ -205,6 +212,10 @@ class Modoboa(base.Installer):
packages += ["openssl-devel"] packages += ["openssl-devel"]
return packages return packages
def setup_user(self):
super().setup_user()
self._setup_venv()
def get_config_files(self): def get_config_files(self):
"""Return appropriate path.""" """Return appropriate path."""
config_files = super().get_config_files() config_files = super().get_config_files()
@@ -213,6 +224,13 @@ class Modoboa(base.Installer):
else: else:
path = "supervisor=/etc/supervisord.d/policyd.ini" path = "supervisor=/etc/supervisord.d/policyd.ini"
config_files.append(path) config_files.append(path)
# Add worker for dkim if needed
if self.modoboa_2_2_or_greater:
config_files.append(
"supervisor-rq-dkim=/etc/supervisor/conf.d/modoboa-dkim-worker.conf")
config_files.append(
"supervisor-rq-base=/etc/supervisor/conf.d/modoboa-base-worker.conf")
return config_files return config_files
def get_template_context(self): def get_template_context(self):
@@ -221,6 +239,8 @@ class Modoboa(base.Installer):
extensions = self.config.get("modoboa", "extensions") extensions = self.config.get("modoboa", "extensions")
extensions = extensions.split() extensions = extensions.split()
random_hour = random.randint(0, 6) random_hour = random.randint(0, 6)
self.dkim_cron_enabled = (not self.modoboa_2_2_or_greater and
self.opendkim_enabled)
context.update({ context.update({
"sudo_user": ( "sudo_user": (
"uwsgi" if package.backend.FORMAT == "rpm" else context["user"] "uwsgi" if package.backend.FORMAT == "rpm" else context["user"]
@@ -231,7 +251,9 @@ class Modoboa(base.Installer):
"" if "modoboa-radicale" in extensions else "#"), "" if "modoboa-radicale" in extensions else "#"),
"opendkim_user": self.config.get("opendkim", "user"), "opendkim_user": self.config.get("opendkim", "user"),
"minutes": random.randint(1, 59), "minutes": random.randint(1, 59),
"hours" : f"{random_hour},{random_hour+12}" "hours": f"{random_hour},{random_hour+12}",
"modoboa_2_2_or_greater": "" if self.modoboa_2_2_or_greater else "#",
"dkim_cron_enabled": "" if self.dkim_cron_enabled else "#"
}) })
return context return context
@@ -243,7 +265,7 @@ class Modoboa(base.Installer):
self.instance_path, "media", "webmail") self.instance_path, "media", "webmail")
pw = pwd.getpwnam(self.user) pw = pwd.getpwnam(self.user)
for d in [rrd_root_dir, pdf_storage_dir, webmail_media_dir]: for d in [rrd_root_dir, pdf_storage_dir, webmail_media_dir]:
utils.mkdir(d, stat.S_IRWXU | stat.S_IRWXG, pw[2], pw[3]) utils.mkdir_safe(d, stat.S_IRWXU | stat.S_IRWXG, pw[2], pw[3])
settings = { settings = {
"admin": { "admin": {
"handle_mailboxes": True, "handle_mailboxes": True,
@@ -255,7 +277,7 @@ class Modoboa(base.Installer):
"maillog": { "maillog": {
"rrd_rootdir": rrd_root_dir, "rrd_rootdir": rrd_root_dir,
}, },
"modoboa_pdfcredentials": { "pdfcredentials": {
"storage_dir": pdf_storage_dir "storage_dir": pdf_storage_dir
}, },
"modoboa_radicale": { "modoboa_radicale": {
@@ -281,17 +303,18 @@ class Modoboa(base.Installer):
def post_run(self): def post_run(self):
"""Additional tasks.""" """Additional tasks."""
self._setup_venv() if 'centos' in utils.dist_name():
system.enable_and_start_service("redis")
else:
system.enable_and_start_service("redis-server")
self._deploy_instance() self._deploy_instance()
if not self.upgrade: if not self.upgrade:
self.apply_settings() self.apply_settings()
if 'centos' in utils.dist_name(): if 'centos' in utils.dist_name():
supervisor = "supervisord" supervisor = "supervisord"
system.enable_and_start_service("redis")
else: else:
supervisor = "supervisor" supervisor = "supervisor"
system.enable_and_start_service("redis-server")
# Restart supervisor # Restart supervisor
system.enable_service(supervisor) system.enable_service(supervisor)
utils.exec_cmd("service {} stop".format(supervisor)) utils.exec_cmd("service {} stop".format(supervisor))

View File

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

View File

@@ -14,7 +14,6 @@ from . import backup, install
class Postfix(base.Installer): class Postfix(base.Installer):
"""Postfix installer.""" """Postfix installer."""
appname = "postfix" appname = "postfix"
@@ -34,13 +33,24 @@ class Postfix(base.Installer):
def install_packages(self): def install_packages(self):
"""Preconfigure postfix package installation.""" """Preconfigure postfix package installation."""
if "centos" in utils.dist_name():
config = configparser.ConfigParser()
with open("/etc/yum.repos.d/CentOS-Base.repo") as fp:
config.read_file(fp)
config.set("centosplus", "enabled", "1")
config.set("centosplus", "includepkgs", "postfix-*")
config.set("base", "exclude", "postfix-*")
config.set("updates", "exclude", "postfix-*")
with open("/etc/yum.repos.d/CentOS-Base.repo", "w") as fp:
config.write(fp)
package.backend.preconfigure( package.backend.preconfigure(
"postfix", "main_mailer_type", "select", "No configuration") "postfix", "main_mailer_type", "select", "No configuration")
super(Postfix, self).install_packages() super(Postfix, self).install_packages()
def get_template_context(self): def get_template_context(self):
"""Additional variables.""" """Additional variables."""
context = super(Postfix, self).get_template_context() context = super().get_template_context()
context.update({ context.update({
"db_driver": self.db_driver, "db_driver": self.db_driver,
"dovecot_mailboxes_owner": self.config.get( "dovecot_mailboxes_owner": self.config.get(
@@ -54,6 +64,13 @@ class Postfix(base.Installer):
}) })
return context return context
def check_dhe_group_file(self):
group = self.config.get(self.appname, "dhe_group")
file_name = f"ffdhe{group}.pem"
if not os.path.exists(f"{self.config_dir}/{file_name}"):
url = f"https://raw.githubusercontent.com/internetstandards/dhe_groups/main/{file_name}"
utils.exec_cmd(f"wget {url}", cwd=self.config_dir)
def post_run(self): def post_run(self):
"""Additional tasks.""" """Additional tasks."""
venv_path = self.config.get("modoboa", "venv_path") venv_path = self.config.get("modoboa", "venv_path")
@@ -75,10 +92,8 @@ class Postfix(base.Installer):
if not os.path.exists(path): if not os.path.exists(path):
utils.copy_file(os.path.join("/etc", f), path) utils.copy_file(os.path.join("/etc", f), path)
# Generate EDH parameters # Generate DHE group
if not os.path.exists("{}/dh2048.pem".format(self.config_dir)): self.check_dhe_group_file()
cmd = "openssl dhparam -dsaparam -out dh2048.pem 2048"
utils.exec_cmd(cmd, cwd=self.config_dir)
# Generate /etc/aliases.db file to avoid warnings # Generate /etc/aliases.db file to avoid warnings
aliases_file = "/etc/aliases" aliases_file = "/etc/aliases"

View File

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

View File

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

View File

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

View File

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

View File

@@ -90,14 +90,14 @@ class LetsEncryptCertificate(CertificateBackend):
elif "centos" in name: elif "centos" in name:
package.backend.install("certbot") package.backend.install("certbot")
else: else:
utils.printcolor("Failed to install certbot, aborting.", utils.RED) utils.printcolor("Failed to install certbot, aborting.")
sys.exit(1) sys.exit(1)
# Nginx plugin certbot # Nginx plugin certbot
if ( if (
self.config.has_option("nginx", "enabled") and self.config.has_option("nginx", "enabled") and
self.config.getboolean("nginx", "enabled") self.config.getboolean("nginx", "enabled")
): ):
if name == "ubuntu" or name.startswith("debian") or ("Centos" in name and version.startswith("9")): if name == "ubuntu" or name.startswith("debian"):
package.backend.install("python3-certbot-nginx") package.backend.install("python3-certbot-nginx")
def generate_cert(self): def generate_cert(self):

View File

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

35
run.py
View File

@@ -22,6 +22,7 @@ from modoboa_installer import utils
PRIMARY_APPS = [ PRIMARY_APPS = [
"amavis", "amavis",
"fail2ban",
"modoboa", "modoboa",
"automx", "automx",
"radicale", "radicale",
@@ -41,7 +42,7 @@ def installation_disclaimer(args, config):
"Before you start the installation, please make sure the following " "Before you start the installation, please make sure the following "
"DNS records exist for domain '{}':\n" "DNS records exist for domain '{}':\n"
" {} IN A <IP ADDRESS OF YOUR SERVER>\n" " {} IN A <IP ADDRESS OF YOUR SERVER>\n"
" IN MX {}.\n".format( " @ IN MX {}.\n".format(
args.domain, args.domain,
hostname.replace(".{}".format(args.domain), ""), hostname.replace(".{}".format(args.domain), ""),
hostname hostname
@@ -115,6 +116,15 @@ def backup_system(config, args):
scripts.backup(app, config, backup_path) 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): def main(input_args):
"""Install process.""" """Install process."""
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
@@ -152,7 +162,8 @@ def main(input_args):
parser.add_argument( parser.add_argument(
"--silent-backup", action="store_true", default=False, "--silent-backup", action="store_true", default=False,
help="For script usage, do not require user interaction " help="For script usage, do not require user interaction "
"backup will be saved at ./modoboa_backup/Backup_M_Y_d_H_M if --backup-path is not provided") "backup will be saved at ./modoboa_backup/Backup_M_Y_d_H_M "
"if --backup-path is not provided")
parser.add_argument( parser.add_argument(
"--restore", type=str, metavar="path", "--restore", type=str, metavar="path",
help="Restore a previously backup up modoboa instance on a NEW machine. " help="Restore a previously backup up modoboa instance on a NEW machine. "
@@ -177,7 +188,8 @@ def main(input_args):
sys.exit(1) sys.exit(1)
utils.success("Welcome to Modoboa installer!\n") utils.success("Welcome to Modoboa installer!\n")
is_config_file_available = utils.check_config_file(
is_config_file_available, outdate_config = utils.check_config_file(
args.configfile, args.interactive, args.upgrade, args.backup, is_restoring) args.configfile, args.interactive, args.upgrade, args.backup, is_restoring)
if not is_config_file_available and ( if not is_config_file_available and (
@@ -185,6 +197,20 @@ def main(input_args):
utils.error("No config file found.") utils.error("No config file found.")
return return
# Check if config is outdated and ask user if it needs to be updated
if is_config_file_available and outdate_config:
answer = utils.user_input("It seems that your config file is outdated. "
"Would you like to update it? (Y/n) ")
if 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 answer.lower().startswith("y"):
return
else:
utils.error("You might encounter unexpected errors ! "
"Make sure to update your config before opening an issue!")
if args.stop_after_configfile_check: if args.stop_after_configfile_check:
return return
@@ -238,10 +264,7 @@ def main(input_args):
ssl_backend.generate_cert() ssl_backend.generate_cert()
for appname in PRIMARY_APPS: for appname in PRIMARY_APPS:
scripts.install(appname, config, args.upgrade, args.restore) scripts.install(appname, config, args.upgrade, args.restore)
if package.backend.FORMAT == "deb":
system.restart_service("cron") system.restart_service("cron")
else:
system.restart_service("crond")
package.backend.restore_system() package.backend.restore_system()
if not args.restore: if not args.restore:
utils.success( utils.success(

View File

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

View File

@@ -6,8 +6,13 @@ import sys
import tempfile import tempfile
import unittest import unittest
from six import StringIO from io import StringIO
from six.moves import configparser from pathlib import Path
try:
import configparser
except ImportError:
import ConfigParser as configparser
try: try:
from unittest.mock import patch from unittest.mock import patch
except ImportError: except ImportError:
@@ -57,6 +62,39 @@ class ConfigFileTestCase(unittest.TestCase):
self.assertEqual(config.get("certificate", "type"), "self-signed") self.assertEqual(config.get("certificate", "type"), "self-signed")
self.assertEqual(config.get("database", "engine"), "postgres") self.assertEqual(config.get("database", "engine"), "postgres")
@patch("modoboa_installer.utils.user_input")
def test_updating_configfile(self, mock_user_input):
"""Check configfile update mechanism."""
cfgfile_temp = os.path.join(self.workdir, "installer_old.cfg")
out = StringIO()
sys.stdout = out
run.main([
"--stop-after-configfile-check",
"--configfile", cfgfile_temp,
"example.test"])
self.assertTrue(os.path.exists(cfgfile_temp))
# Adding a dummy section
with open(cfgfile_temp, "a") as fp:
fp.write(
"""
[dummy]
weird_old_option = "hey
""")
mock_user_input.side_effect = ["y"]
out = StringIO()
sys.stdout = out
run.main([
"--stop-after-configfile-check",
"--configfile", cfgfile_temp,
"example.test"])
self.assertIn("dummy", out.getvalue())
self.assertTrue(Path(self.workdir).glob("*.old"))
self.assertIn("Update complete",
out.getvalue()
)
@patch("modoboa_installer.utils.user_input") @patch("modoboa_installer.utils.user_input")
def test_interactive_mode_letsencrypt(self, mock_user_input): def test_interactive_mode_letsencrypt(self, mock_user_input):
"""Check interactive mode.""" """Check interactive mode."""
@@ -92,6 +130,9 @@ class ConfigFileTestCase(unittest.TestCase):
" postwhite spamassassin uwsgi", " postwhite spamassassin uwsgi",
out.getvalue() out.getvalue()
) )
self.assertNotIn("It seems that your config file is outdated.",
out.getvalue()
)
@patch("modoboa_installer.utils.user_input") @patch("modoboa_installer.utils.user_input")
def test_upgrade_mode(self, mock_user_input): def test_upgrade_mode(self, mock_user_input):