├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml ├── stale.yml └── workflows │ ├── installer.yml │ └── versioning.yml ├── .gitignore ├── LICENSE ├── README.rst ├── checks.py ├── modoboa_installer ├── __init__.py ├── compatibility_matrix.py ├── config_dict_template.py ├── constants.py ├── database.py ├── package.py ├── python.py ├── scripts │ ├── __init__.py │ ├── amavis.py │ ├── automx.py │ ├── backup.py │ ├── base.py │ ├── clamav.py │ ├── dovecot.py │ ├── fail2ban.py │ ├── files │ │ ├── amavis │ │ │ ├── amavis_mysql_2.10.1.sql │ │ │ ├── amavis_mysql_2.11.X.sql │ │ │ ├── amavis_mysql_2.12.X.sql │ │ │ ├── amavis_mysql_2.13.X.sql │ │ │ ├── amavis_mysql_2.7.1.sql │ │ │ ├── amavis_postgres_2.10.1.sql │ │ │ ├── amavis_postgres_2.11.X.sql │ │ │ ├── amavis_postgres_2.12.X.sql │ │ │ ├── amavis_postgres_2.13.X.sql │ │ │ ├── amavis_postgres_2.7.0.sql │ │ │ ├── amavis_postgres_2.7.1.sql │ │ │ ├── amavisd.conf.tpl │ │ │ └── conf.d │ │ │ │ ├── 05-node_id.tpl │ │ │ │ ├── 15-content_filter_mode.tpl │ │ │ │ └── 50-user.tpl │ │ ├── automx │ │ │ └── automx.conf.tpl │ │ ├── clamav │ │ │ ├── sysconfig │ │ │ │ └── clamd.amavisd.tpl │ │ │ └── tmpfiles.d │ │ │ │ └── clamd.amavisd.conf.tpl │ │ ├── dovecot │ │ │ ├── conf.d │ │ │ │ ├── 10-auth.conf │ │ │ │ ├── 10-mail.conf │ │ │ │ ├── 10-master.conf.tpl │ │ │ │ ├── 10-ssl-keys.try.tpl │ │ │ │ ├── 10-ssl.conf.tpl │ │ │ │ ├── 15-mailboxes.conf │ │ │ │ ├── 20-imap.conf │ │ │ │ ├── 20-lmtp.conf.tpl │ │ │ │ ├── 20-managesieve.conf │ │ │ │ ├── 90-quota.conf │ │ │ │ ├── 90-sieve.conf │ │ │ │ ├── auth-oauth2.conf.ext │ │ │ │ ├── auth-sql.conf.ext │ │ │ │ └── dovecot-oauth2.conf.ext.tpl │ │ │ ├── dovecot-dict-sql.conf.ext.tpl │ │ │ ├── dovecot-sql-master-mysql.conf.ext.tpl │ │ │ ├── dovecot-sql-master-postgres.conf.ext.tpl │ │ │ ├── dovecot-sql-mysql.conf.ext.tpl │ │ │ ├── dovecot-sql-postgres.conf.ext.tpl │ │ │ ├── dovecot.conf.tpl │ │ │ ├── fix_modoboa_postgres_schema.sql │ │ │ ├── install_modoboa_postgres_trigger.sql │ │ │ ├── postlogin-mysql.sh.tpl │ │ │ └── postlogin-postgres.sh.tpl │ │ ├── fail2ban │ │ │ ├── filter.d │ │ │ │ └── modoboa-auth.conf.tpl │ │ │ └── jail.d │ │ │ │ └── modoboa.conf.tpl │ │ ├── modoboa │ │ │ ├── crontab.tpl │ │ │ ├── sudoers.tpl │ │ │ ├── supervisor-rq-base.tpl │ │ │ ├── supervisor-rq-dkim.tpl │ │ │ └── supervisor.tpl │ │ ├── nginx │ │ │ ├── automx.conf.tpl │ │ │ └── modoboa.conf.tpl │ │ ├── opendkim │ │ │ ├── dkim_view_mysql.sql │ │ │ ├── dkim_view_postgres.sql │ │ │ ├── opendkim.conf.tpl │ │ │ └── opendkim.hosts.tpl │ │ ├── postfix │ │ │ ├── main.cf.tpl │ │ │ └── master.cf.tpl │ │ ├── postwhite │ │ │ └── crontab.tpl │ │ ├── radicale │ │ │ ├── config.tpl │ │ │ └── supervisor.tpl │ │ ├── spamassassin │ │ │ ├── local.cf.tpl │ │ │ └── v310.pre.tpl │ │ └── uwsgi │ │ │ ├── automx.ini.tpl │ │ │ └── modoboa.ini.tpl │ ├── modoboa.py │ ├── nginx.py │ ├── opendkim.py │ ├── postfix.py │ ├── postwhite.py │ ├── radicale.py │ ├── razor.py │ ├── restore.py │ ├── spamassassin.py │ └── uwsgi.py ├── ssl.py ├── system.py └── utils.py ├── run.py ├── test-requirements.txt ├── tests.py └── version.txt /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [modoboa] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Impacted versions 2 | 3 | * Distribution: Debian / Ubuntu / Centos 4 | * Codename: Jessie / Trusty / Centos 7 / ... 5 | * Arch: 32 Bits / 64 Bits 6 | * Database: PostgreSQL / MySQL 7 | 8 | # Steps to reproduce 9 | 10 | # Full trace using --debug option or current behaviour 11 | 12 | # Expected behavior 13 | 14 | # Video/Screenshot link (optional) 15 | 16 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Description of the issue/feature this PR addresses: 2 | 3 | Current behavior before PR: 4 | 5 | Desired behavior after PR is merged: 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 14 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - bug 10 | - dependencies 11 | - enhancement 12 | - looking-for-sponsors 13 | - documentation 14 | # Label to use when marking an issue as stale 15 | staleLabel: stale 16 | # Set to true to ignore issues in a milestone (defaults to false) 17 | exemptMilestones: true 18 | # Set to true to ignore issues with an assignee (defaults to false) 19 | exemptAssignees: true 20 | # Limit to only `issues` or `pulls` 21 | only: issues 22 | # Comment to post when marking an issue as stale. Set to `false` to disable 23 | markComment: > 24 | This issue has been automatically marked as stale because it has not had 25 | recent activity. It will be closed if no further activity occurs. Thank you 26 | for your contributions. 27 | # Comment to post when closing a stale issue. Set to `false` to disable 28 | closeComment: false 29 | -------------------------------------------------------------------------------- /.github/workflows/installer.yml: -------------------------------------------------------------------------------- 1 | name: Modoboa installer 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: [3.9, '3.10', '3.11', '3.12'] 15 | fail-fast: false 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | - name: Install dependencies 24 | run: | 25 | pip install -r test-requirements.txt 26 | - name: Run tests 27 | if: ${{ matrix.python-version != '3.12' }} 28 | run: | 29 | python tests.py 30 | - name: Run tests and coverage 31 | if: ${{ matrix.python-version == '3.12' }} 32 | run: | 33 | coverage run tests.py 34 | - name: Upload coverage result 35 | if: ${{ matrix.python-version == '3.12' }} 36 | uses: actions/upload-artifact@v4 37 | with: 38 | name: coverage-results 39 | path: .coverage 40 | include-hidden-files: true 41 | 42 | coverage: 43 | needs: test 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v4 47 | - name: Set up Python 48 | uses: actions/setup-python@v5 49 | with: 50 | python-version: '3.12' 51 | - name: Install dependencies 52 | run: | 53 | pip install codecov 54 | - name: Download coverage results 55 | uses: actions/download-artifact@v4 56 | with: 57 | name: coverage-results 58 | - name: Report coverage 59 | run: | 60 | coverage report 61 | codecov 62 | -------------------------------------------------------------------------------- /.github/workflows/versioning.yml: -------------------------------------------------------------------------------- 1 | name: Update version file 2 | 3 | on: 4 | workflow_run: 5 | branches: [ master ] 6 | workflows: [Modoboa installer] 7 | types: 8 | - completed 9 | 10 | jobs: 11 | update-version: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository. 18 | ref: ${{ github.head_ref }} 19 | - name: Overwrite file 20 | uses: "DamianReeves/write-file-action@master" 21 | with: 22 | path: version.txt 23 | write-mode: overwrite 24 | contents: ${{ github.sha }} 25 | 26 | - name: Commit & Push 27 | uses: Andro999b/push@v1.3 28 | with: 29 | github_token: ${{ secrets.GITHUB_TOKEN }} 30 | branch: ${{ github.ref_name }} 31 | force: true 32 | message: '[GitHub Action] Updated version file' 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | # PyCharm 60 | .idea/ 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Modoboa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | **modoboa-installer** 2 | ===================== 3 | 4 | |workflow| |codecov| 5 | 6 | An installer which deploy a complete mail server based on Modoboa. 7 | 8 | .. warning:: 9 | 10 | This tool is still in beta stage, it has been tested on: 11 | 12 | * Debian 10 and upper 13 | * Ubuntu Bionic Beaver (18.04) and upper 14 | 15 | .. warning:: 16 | 17 | ``/tmp`` partition must be mounted without the ``noexec`` option. 18 | 19 | .. note:: 20 | 21 | The server (physical or virtual) running Modoboa needs at least 2GB 22 | of RAM in order to compile the required dependencies during the 23 | installation process. Passwords should not contain any special characters 24 | as they may cause the installation to fail. It's important to set a FQDN 25 | before, otherwise the installation will break. 26 | 27 | Usage:: 28 | 29 | $ git clone https://github.com/modoboa/modoboa-installer 30 | $ cd modoboa-installer 31 | $ sudo python3 run.py 32 | 33 | 34 | If ``python3`` is not installed on your system, please install it. 35 | 36 | A configuration file will be automatically generated the first time 37 | you run the installer, please don't copy the 38 | ``installer.cfg.template`` file manually. 39 | 40 | The following components are installed by the installer: 41 | 42 | * Database server (PostgreSQL or MySQL) 43 | * Nginx and uWSGI 44 | * Postfix 45 | * Dovecot 46 | * Amavis (with SpamAssassin and ClamAV) 47 | * automx (autoconfiguration service) 48 | * OpenDKIM 49 | * Radicale (CalDAV and CardDAV server) 50 | 51 | If you want to customize configuration before running the installer, 52 | run the following command:: 53 | 54 | $ ./run.py --stop-after-configfile-check 55 | 56 | An interactive mode is also available:: 57 | 58 | $ ./run.py --interactive 59 | 60 | Make your modifications and run the installer as usual. 61 | 62 | By default, the latest Modoboa version is installed but you can select 63 | a previous one using the ``--version`` option:: 64 | 65 | $ sudo ./run.py --version=X.X.X 66 | 67 | .. note:: 68 | 69 | Version selection is available only for Modoboa >= 1.8.1. 70 | 71 | You can also install beta releases using the ``--beta`` flag:: 72 | 73 | $ sudo ./run.py --beta 74 | 75 | If you want more information about the installation process, add the 76 | ``--debug`` option to your command line. 77 | 78 | Upgrade mode 79 | ============ 80 | 81 | An experimental upgrade mode is available. 82 | 83 | .. note:: 84 | 85 | You must keep the original configuration file, ie the one used for 86 | the installation. Otherwise, you won't be able to use this mode. 87 | 88 | You can activate it as follows:: 89 | 90 | $ sudo ./run.py --upgrade 91 | 92 | It will automatically install latest versions of modoboa and its plugins. 93 | 94 | Backup mode 95 | =========== 96 | 97 | An experimental backup mode is available. 98 | 99 | .. warning:: 100 | 101 | You must keep the original configuration file, i.e. the one used for 102 | the installation. Otherwise, you will need to recreate it manually with the right information! 103 | 104 | You can start the process as follows:: 105 | 106 | $ sudo ./run.py --backup 107 | 108 | Then follow the step on the console. 109 | 110 | There is also a non-interactive mode:: 111 | 112 | $ sudo ./run.py --silent-backup 113 | 114 | You can also add a path, else it will be saved in ./modoboa_backup/Backup_M_Y_d_H_M:: 115 | 116 | $ sudo ./run.py --silent-backup --backup-path "/My_Backup_Path" 117 | 118 | if you want to disable mail backup:: 119 | 120 | $ sudo ./run.py --backup --no-mail 121 | 122 | This can be useful for larger instance 123 | 124 | 1. Silent mode 125 | 126 | Command:: 127 | 128 | $ sudo ./run.py --silent-backup 129 | 130 | This mode will run silently. When executed, it will create 131 | /modoboa_backup/ and each time you execute it, it will create a new 132 | backup directory with current date and time. 133 | 134 | You can supply a custom path if needed:: 135 | 136 | $ sudo ./run.py --silent-backup --backup-path /path/of/backup/directory 137 | 138 | If you want to disable emails backup, disable dovecot in the 139 | configuration file (set enabled to False). 140 | 141 | This can be useful for larger instance. 142 | 143 | Restore mode 144 | ============ 145 | 146 | An experimental restore mode is available. 147 | 148 | You can start the process as follows:: 149 | 150 | $ sudo ./run.py --restore /path/to/backup/directory/ 151 | 152 | Then wait for the process to finish. 153 | 154 | Change the generated hostname 155 | ============================= 156 | 157 | By default, the installer will setup your email server using the 158 | following hostname: ``mail.``. If you want a different 159 | value, generate the configuration file like this:: 160 | 161 | $ ./run.py --stop-after-configfile-check 162 | 163 | Then edit ``installer.cfg`` and look for the following section:: 164 | 165 | [general] 166 | hostname = mail.%(domain)s 167 | 168 | Replace ``mail`` by the value you want to use and save your 169 | modifications. 170 | 171 | Finally, run the installer without the 172 | ``--stop-after-configfile-check`` option. 173 | 174 | Certificate 175 | =========== 176 | 177 | Self-signed 178 | ----------- 179 | 180 | It is the default type of certificate the installer will generate, it 181 | is however not recommended for production use. 182 | 183 | Letsencrypt 184 | ----------- 185 | 186 | .. warning:: 187 | 188 | Please note that by using this option, you agree to the `ToS 189 | `_ of 190 | letsencrypt and that your IP will be logged (see ToS). 191 | Please also note this option requires the hostname you're using to be 192 | valid (ie. it can be resolved with a DNS query) and to match the 193 | server you're installing Modoboa on. 194 | 195 | If you want to generate a valid certificate using `Let's Encrypt 196 | `_, edit the ``installer.cfg`` file and 197 | modify the following settings:: 198 | 199 | [certificate] 200 | generate = true 201 | type = letsencrypt 202 | tls_cert_file_path = 203 | tls_key_file_path = 204 | 205 | [letsencrypt] 206 | email = admin@example.com 207 | 208 | Change the ``email`` setting to a valid value since it will be used 209 | for account recovery. 210 | 211 | Manual 212 | ------ 213 | 214 | .. warning:: 215 | 216 | It is not possible to configure manual certs interactively, so 217 | you'll have to do it in 2 steps. Please run ``run.py`` with 218 | `--stop-after-configfile-check` first, configure your file as 219 | desired and apply the configuration as written bellow. Then run 220 | ``run.py`` again but without `--stop-after-configfile-check` or 221 | `--interactive`. 222 | 223 | If you want to use already generated certs, simply edit the 224 | ``installer.cfg`` file and modify the following settings:: 225 | 226 | [certificate] 227 | generate = true 228 | type = manual 229 | tls_cert_file_path = *path to tls fullchain file* 230 | tls_key_file_path = *path to tls key file* 231 | 232 | .. |workflow| image:: https://github.com/modoboa/modoboa-installer/workflows/Modoboa%20installer/badge.svg 233 | .. |codecov| image:: https://codecov.io/gh/modoboa/modoboa-installer/graph/badge.svg?token=Fo2o1GdHZq 234 | :target: https://codecov.io/gh/modoboa/modoboa-installer 235 | -------------------------------------------------------------------------------- /checks.py: -------------------------------------------------------------------------------- 1 | """Checks to be performed before any install or upgrade""" 2 | 3 | import sys 4 | from urllib.request import urlopen 5 | 6 | from modoboa_installer import utils 7 | 8 | 9 | def check_version(): 10 | local_version = "" 11 | with open("version.txt", "r") as version: 12 | local_version = version.readline() 13 | remote_version = "" 14 | with urlopen("https://raw.githubusercontent.com/modoboa/modoboa-installer/master/version.txt") as r_version: 15 | remote_version = r_version.read().decode() 16 | if local_version == "" or remote_version == "": 17 | utils.printcolor( 18 | "Could not check that your installer is up-to-date: " 19 | f"local version: {local_version}, " 20 | f"remote version: {remote_version}", 21 | utils.YELLOW 22 | ) 23 | if remote_version != local_version: 24 | utils.error( 25 | "Your installer seems outdated.\n" 26 | "Check README file for instructions about how to update.\n" 27 | "No support will be provided without an up-to-date installer!" 28 | ) 29 | answer = utils.user_input("Continue anyway? (Y/n) ") 30 | if not answer.lower().startswith("y"): 31 | sys.exit(0) 32 | else: 33 | utils.success("Installer seems up to date!") 34 | 35 | 36 | def handle(): 37 | check_version() 38 | -------------------------------------------------------------------------------- /modoboa_installer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modoboa/modoboa-installer/1e4ba0676419f71aab791d95e00b46de05a78513/modoboa_installer/__init__.py -------------------------------------------------------------------------------- /modoboa_installer/compatibility_matrix.py: -------------------------------------------------------------------------------- 1 | """Modoboa compatibility matrix.""" 2 | 3 | COMPATIBILITY_MATRIX = { 4 | "1.8.1": { 5 | "modoboa-pdfcredentials": "<=1.1.0", 6 | "modoboa-sievefilters": "<=1.1.0", 7 | "modoboa-webmail": "<=1.1.5", 8 | }, 9 | "1.8.2": { 10 | "modoboa-pdfcredentials": ">=1.1.1", 11 | "modoboa-sievefilters": ">=1.1.1", 12 | "modoboa-webmail": ">=1.2.0", 13 | }, 14 | "1.8.3": { 15 | "modoboa-pdfcredentials": ">=1.1.1", 16 | "modoboa-sievefilters": ">=1.1.1", 17 | "modoboa-webmail": ">=1.2.0", 18 | }, 19 | "1.9.0": { 20 | "modoboa-pdfcredentials": ">=1.1.1", 21 | "modoboa-sievefilters": ">=1.1.1", 22 | "modoboa-webmail": ">=1.2.0", 23 | }, 24 | } 25 | 26 | EXTENSIONS_AVAILABILITY = { 27 | "modoboa-contacts": "1.7.4", 28 | } 29 | 30 | REMOVED_EXTENSIONS = { 31 | "modoboa-pdfcredentials": "2.1.0", 32 | "modoboa-dmarc": "2.1.0", 33 | "modoboa-imap-migration": "2.1.0", 34 | "modoboa-sievefilters": "2.3.0", 35 | "modoboa-postfix-autoreply": "2.3.0" 36 | } 37 | -------------------------------------------------------------------------------- /modoboa_installer/constants.py: -------------------------------------------------------------------------------- 1 | DEFAULT_BACKUP_DIRECTORY = "./modoboa_backup/" 2 | -------------------------------------------------------------------------------- /modoboa_installer/package.py: -------------------------------------------------------------------------------- 1 | """Package management related tools.""" 2 | 3 | import re 4 | 5 | from . import utils 6 | 7 | 8 | class Package: 9 | """Base classe.""" 10 | 11 | def __init__(self, dist_name): 12 | """Constructor.""" 13 | self.dist_name = dist_name 14 | 15 | def preconfigure(self, name, question, qtype, answer): 16 | """Empty method.""" 17 | pass 18 | 19 | def prepare_system(self): 20 | pass 21 | 22 | def restore_system(self): 23 | pass 24 | 25 | 26 | class DEBPackage(Package): 27 | """DEB based operations.""" 28 | 29 | FORMAT = "deb" 30 | 31 | def __init__(self, dist_name): 32 | super().__init__(dist_name) 33 | self.index_updated = False 34 | self.policy_file = "/usr/sbin/policy-rc.d" 35 | 36 | def enable_backports(self, codename): 37 | code, output = utils.exec_cmd(f"grep {codename}-backports /etc/apt/sources.list") 38 | if code: 39 | with open(f"/etc/apt/sources.list.d/backports.list", "w") as fp: 40 | fp.write(f"deb http://deb.debian.org/debian {codename}-backports main\n") 41 | self.update(force=True) 42 | 43 | def prepare_system(self): 44 | """Make sure services don't start at installation.""" 45 | with open(self.policy_file, "w") as fp: 46 | fp.write("exit 101\n") 47 | utils.exec_cmd("chmod +x {}".format(self.policy_file)) 48 | 49 | def restore_system(self): 50 | utils.exec_cmd("rm -f {}".format(self.policy_file)) 51 | 52 | def update(self, force=False): 53 | """Update local cache.""" 54 | if self.index_updated and not force: 55 | return 56 | utils.exec_cmd("apt-get -o Dpkg::Progress-Fancy=0 update --quiet") 57 | self.index_updated = True 58 | 59 | def preconfigure(self, name, question, qtype, answer): 60 | """Pre-configure a package before installation.""" 61 | line = "{0} {0}/{1} {2} {3}".format(name, question, qtype, answer) 62 | utils.exec_cmd("echo '{}' | debconf-set-selections".format(line)) 63 | 64 | def install(self, name): 65 | """Install a package.""" 66 | self.update() 67 | utils.exec_cmd("apt-get -o Dpkg::Progress-Fancy=0 install --quiet --assume-yes -o DPkg::options::=--force-confold {}".format(name)) 68 | 69 | def install_many(self, names): 70 | """Install many packages.""" 71 | self.update() 72 | return utils.exec_cmd("apt-get -o Dpkg::Progress-Fancy=0 install --quiet --assume-yes -o DPkg::options::=--force-confold {}".format( 73 | " ".join(names))) 74 | 75 | def get_installed_version(self, name): 76 | """Get installed package version.""" 77 | code, output = utils.exec_cmd( 78 | "dpkg -s {} | grep Version".format(name)) 79 | match = re.match(r"Version: (\d:)?(.+)-\d", output.decode()) 80 | if match: 81 | return match.group(2) 82 | return None 83 | 84 | 85 | class RPMPackage(Package): 86 | """RPM based operations.""" 87 | 88 | FORMAT = "rpm" 89 | 90 | def __init__(self, dist_name): 91 | """Initialize backend.""" 92 | super(RPMPackage, self).__init__(dist_name) 93 | if "centos" in dist_name: 94 | self.install("epel-release") 95 | 96 | def install(self, name): 97 | """Install a package.""" 98 | utils.exec_cmd("yum install -y --quiet {}".format(name)) 99 | 100 | def install_many(self, names): 101 | """Install many packages.""" 102 | return utils.exec_cmd("yum install -y --quiet {}".format(" ".join(names))) 103 | 104 | def get_installed_version(self, name): 105 | """Get installed package version.""" 106 | code, output = utils.exec_cmd( 107 | "rpm -qi {} | grep Version".format(name)) 108 | match = re.match(r"Version\s+: (.+)", output.decode()) 109 | if match: 110 | return match.group(1) 111 | return None 112 | 113 | 114 | def get_backend(): 115 | """Return the appropriate package backend.""" 116 | distname = utils.dist_name() 117 | backend = None 118 | if distname in ["debian", "debian gnu/linux", "ubuntu", "linuxmint"]: 119 | backend = DEBPackage 120 | elif "centos" in distname: 121 | backend = RPMPackage 122 | else: 123 | raise NotImplementedError( 124 | "Sorry, this distribution is not supported yet.") 125 | return backend(distname) 126 | 127 | 128 | backend = get_backend() 129 | -------------------------------------------------------------------------------- /modoboa_installer/python.py: -------------------------------------------------------------------------------- 1 | """Python related tools.""" 2 | 3 | import os 4 | import sys 5 | 6 | from . import package 7 | from . import utils 8 | 9 | 10 | def get_path(cmd, venv=None): 11 | """Return path to cmd.""" 12 | path = cmd 13 | if venv: 14 | path = os.path.join(venv, "bin", path) 15 | return path 16 | 17 | 18 | def get_pip_path(venv): 19 | """Return the full path to pip command.""" 20 | binpath = "pip" 21 | if venv: 22 | binpath = os.path.join(venv, "bin", binpath) 23 | return binpath 24 | 25 | 26 | def install_package(name, venv=None, upgrade=False, binary=True, **kwargs): 27 | """Install a Python package using pip.""" 28 | cmd = "{} install{}{}{} {}".format( 29 | get_pip_path(venv), 30 | " -U" if upgrade else "", 31 | " --no-binary :all:" if not binary else "", 32 | " --pre" if kwargs.pop("beta", False) else "", 33 | name 34 | ) 35 | utils.exec_cmd(cmd, **kwargs) 36 | 37 | 38 | def install_packages(names, venv=None, upgrade=False, **kwargs): 39 | """Install a Python package using pip.""" 40 | cmd = "{} install{}{} {}".format( 41 | get_pip_path(venv), 42 | " -U " if upgrade else "", 43 | " --pre" if kwargs.pop("beta", False) else "", 44 | " ".join(names) 45 | ) 46 | utils.exec_cmd(cmd, **kwargs) 47 | 48 | 49 | def get_package_version(name, venv=None, **kwargs): 50 | """Returns the version of an installed package.""" 51 | cmd = "{} show {}".format( 52 | get_pip_path(venv), 53 | name 54 | ) 55 | exit_code, output = utils.exec_cmd(cmd, **kwargs) 56 | if exit_code != 0: 57 | utils.error(f"Failed to get version of {name}. " 58 | f"Output is: {output}") 59 | sys.exit(1) 60 | 61 | version_list_clean = [] 62 | for line in output.decode().split("\n"): 63 | if not line.startswith("Version:"): 64 | continue 65 | version_item_list = line.split(":") 66 | version_list = version_item_list[1].split(".") 67 | for element in version_list: 68 | try: 69 | version_list_clean.append(int(element)) 70 | except ValueError: 71 | utils.printcolor( 72 | f"Failed to decode some part of the version of {name}", 73 | utils.YELLOW) 74 | version_list_clean.append(element) 75 | if len(version_list_clean) == 0: 76 | utils.printcolor( 77 | f"Failed to find the version of {name}", 78 | utils.RED) 79 | sys.exit(1) 80 | return version_list_clean 81 | 82 | 83 | def install_package_from_repository(name, url, vcs="git", venv=None, **kwargs): 84 | """Install a Python package from its repository.""" 85 | if vcs == "git": 86 | package.backend.install("git") 87 | cmd = "{} install -e {}+{}#egg={}".format( 88 | get_pip_path(venv), vcs, url, name) 89 | utils.exec_cmd(cmd, **kwargs) 90 | 91 | 92 | def setup_virtualenv(path, sudo_user=None): 93 | """Install a virtualenv if needed.""" 94 | if os.path.exists(path): 95 | return 96 | if utils.dist_name().startswith("centos"): 97 | python_binary = "python3" 98 | packages = ["python3"] 99 | else: 100 | python_binary = "python3" 101 | packages = ["python3-venv"] 102 | package.backend.install_many(packages) 103 | with utils.settings(sudo_user=sudo_user): 104 | utils.exec_cmd("{} -m venv {}".format(python_binary, path)) 105 | install_packages(["pip", "setuptools"], venv=path, upgrade=True) 106 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | """Installation scripts management.""" 2 | 3 | import importlib 4 | import sys 5 | 6 | from .. import utils 7 | 8 | 9 | def load_app_script(appname): 10 | """Load module corresponding to the given appname.""" 11 | try: 12 | script = importlib.import_module( 13 | "modoboa_installer.scripts.{}".format(appname)) 14 | except ImportError: 15 | print("Unknown application {}".format(appname)) 16 | sys.exit(1) 17 | return script 18 | 19 | 20 | def install(appname: str, config, upgrade: bool, archive_path: str): 21 | """Install an application.""" 22 | if (config.has_option(appname, "enabled") and 23 | not config.getboolean(appname, "enabled")): 24 | return 25 | 26 | utils.printcolor("Installing {}".format(appname), utils.MAGENTA) 27 | script = load_app_script(appname) 28 | try: 29 | getattr(script, appname.capitalize())(config, upgrade, archive_path).run() 30 | except utils.FatalError as inst: 31 | utils.error("{}".format(inst)) 32 | sys.exit(1) 33 | 34 | 35 | def backup(appname, config, path): 36 | """Backup an application.""" 37 | if (config.has_option(appname, "enabled") and 38 | not config.getboolean(appname, "enabled")): 39 | return 40 | 41 | utils.printcolor("Backing up {}".format(appname), utils.MAGENTA) 42 | script = load_app_script(appname) 43 | try: 44 | getattr(script, appname.capitalize())(config, False, False).backup(path) 45 | except utils.FatalError as inst: 46 | utils.error("{}".format(inst)) 47 | sys.exit(1) 48 | 49 | 50 | def restore_prep(restore): 51 | """Restore instance""" 52 | script = importlib.import_module( 53 | "modoboa_installer.scripts.restore") 54 | getattr(script, "Restore")(restore) 55 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/amavis.py: -------------------------------------------------------------------------------- 1 | """Amavis related functions.""" 2 | 3 | import os 4 | 5 | from .. import package 6 | from .. import utils 7 | 8 | from . import base 9 | from . import backup, install 10 | 11 | 12 | class Amavis(base.Installer): 13 | 14 | """Amavis installer.""" 15 | 16 | appname = "amavis" 17 | packages = { 18 | "deb": [ 19 | "libdbi-perl", "amavisd-new", "arc", "arj", "cabextract", 20 | "liblz4-tool", "lrzip", "lzop", "p7zip-full", "rpm2cpio", 21 | "unrar-free", 22 | ], 23 | "rpm": [ 24 | "amavisd-new", "arj", "lz4", "lzop", "p7zip", 25 | ], 26 | } 27 | with_db = True 28 | 29 | @property 30 | def config_dir(self): 31 | """Return appropriate config dir.""" 32 | if package.backend.FORMAT == "rpm": 33 | return "/etc/amavisd" 34 | return "/etc/amavis" 35 | 36 | def get_daemon_name(self): 37 | """Return appropriate daemon name.""" 38 | if package.backend.FORMAT == "rpm": 39 | return "amavisd" 40 | return "amavis" 41 | 42 | def get_config_files(self): 43 | """Return appropriate config files.""" 44 | if package.backend.FORMAT == "deb": 45 | return [ 46 | "conf.d/05-node_id", "conf.d/15-content_filter_mode", 47 | "conf.d/50-user"] 48 | return ["amavisd.conf"] 49 | 50 | def get_packages(self): 51 | """Additional packages.""" 52 | packages = super(Amavis, self).get_packages() 53 | if package.backend.FORMAT == "deb": 54 | db_driver = "pg" if self.db_driver == "pgsql" else self.db_driver 55 | return packages + ["libdbd-{}-perl".format(db_driver)] 56 | if self.db_driver == "pgsql": 57 | db_driver = "Pg" 58 | elif self.db_driver == "mysql": 59 | db_driver = "MySQL" 60 | else: 61 | raise NotImplementedError("DB driver not supported") 62 | packages += ["perl-DBD-{}".format(db_driver)] 63 | name, version = utils.dist_info() 64 | if version.startswith('7'): 65 | packages += ["cabextract", "lrzip", "unar", "unzoo"] 66 | elif version.startswith('8'): 67 | packages += ["perl-IO-stringy"] 68 | return packages 69 | 70 | def get_sql_schema_path(self): 71 | """Return schema path.""" 72 | version = package.backend.get_installed_version("amavisd-new") 73 | if version is None: 74 | # Fallback to amavis... 75 | version = package.backend.get_installed_version("amavis") 76 | if version is None: 77 | raise utils.FatalError("Amavis is not installed") 78 | path = self.get_file_path( 79 | "amavis_{}_{}.sql".format(self.dbengine, version)) 80 | if not os.path.exists(path): 81 | version = ".".join(version.split(".")[:-1]) + ".X" 82 | path = self.get_file_path( 83 | "amavis_{}_{}.sql".format(self.dbengine, version)) 84 | if not os.path.exists(path): 85 | raise utils.FatalError("Failed to find amavis database schema") 86 | return path 87 | 88 | def pre_run(self): 89 | """Tasks to run first.""" 90 | with open("/etc/mailname", "w") as fp: 91 | fp.write("{}\n".format(self.config.get("general", "hostname"))) 92 | 93 | def post_run(self): 94 | """Additional tasks.""" 95 | install("spamassassin", self.config, self.upgrade, self.archive_path) 96 | install("clamav", self.config, self.upgrade, self.archive_path) 97 | 98 | def custom_backup(self, path): 99 | """Backup custom configuration if any.""" 100 | if package.backend.FORMAT == "deb": 101 | amavis_custom = f"{self.config_dir}/conf.d/99-custom" 102 | if os.path.isfile(amavis_custom): 103 | utils.copy_file(amavis_custom, path) 104 | utils.success("Amavis custom configuration saved!") 105 | backup("spamassassin", self.config, os.path.dirname(path)) 106 | 107 | def restore(self): 108 | """Restore custom config files.""" 109 | if package.backend.FORMAT != "deb": 110 | return 111 | amavis_custom_configuration = os.path.join( 112 | self.archive_path, "custom/99-custom") 113 | if os.path.isfile(amavis_custom_configuration): 114 | utils.copy_file(amavis_custom_configuration, os.path.join( 115 | self.config_dir, "conf.d")) 116 | utils.success("Custom amavis configuration restored.") 117 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/automx.py: -------------------------------------------------------------------------------- 1 | """Automx related tasks.""" 2 | 3 | import os 4 | import pwd 5 | import shutil 6 | import stat 7 | 8 | from .. import python 9 | from .. import system 10 | from .. import utils 11 | 12 | from . import base 13 | 14 | 15 | class Automx(base.Installer): 16 | """Automx installation.""" 17 | 18 | appname = "automx" 19 | config_files = ["automx.conf"] 20 | no_daemon = True 21 | packages = { 22 | "deb": ["memcached", "unzip"], 23 | "rpm": ["memcached", "unzip"] 24 | } 25 | with_user = True 26 | 27 | def __init__(self, *args, **kwargs): 28 | """Get configuration.""" 29 | super(Automx, self).__init__(*args, **kwargs) 30 | self.venv_path = self.config.get("automx", "venv_path") 31 | self.instance_path = self.config.get("automx", "instance_path") 32 | 33 | def get_template_context(self): 34 | """Additional variables.""" 35 | context = super(Automx, self).get_template_context() 36 | sql_dsn = "{}://{}:{}@{}:{}/{}".format( 37 | "postgresql" if self.dbengine == "postgres" else self.dbengine, 38 | self.config.get("modoboa", "dbuser"), 39 | self.config.get("modoboa", "dbpassword"), 40 | self.dbhost, 41 | self.dbport, 42 | self.config.get("modoboa", "dbname")) 43 | if self.db_driver == "pgsql": 44 | sql_query = ( 45 | "SELECT first_name || ' ' || last_name AS display_name, email" 46 | ", SPLIT_PART(email, '@', 2) AS domain " 47 | "FROM core_user WHERE email='%s' AND is_active;") 48 | else: 49 | sql_query = ( 50 | "SELECT concat(first_name, ' ', last_name) AS display_name, " 51 | "email, SUBSTRING_INDEX(email, '@', -1) AS domain " 52 | "FROM core_user WHERE email='%s' AND is_active=1;" 53 | ) 54 | context.update({"sql_dsn": sql_dsn, "sql_query": sql_query}) 55 | return context 56 | 57 | def _setup_venv(self): 58 | """Prepare a python virtualenv.""" 59 | python.setup_virtualenv(self.venv_path, sudo_user=self.user) 60 | packages = [ 61 | "future", "lxml", "ipaddress", "sqlalchemy < 2.0", "python-memcached", 62 | "python-dateutil", "configparser" 63 | ] 64 | if self.dbengine == "postgres": 65 | packages.append("psycopg2-binary") 66 | else: 67 | packages.append("mysqlclient") 68 | python.install_packages(packages, self.venv_path, sudo_user=self.user) 69 | target = "{}/master.zip".format(self.home_dir) 70 | if os.path.exists(target): 71 | os.unlink(target) 72 | utils.exec_cmd( 73 | "wget https://github.com/sys4/automx/archive/master.zip", 74 | sudo_user=self.user, cwd=self.home_dir) 75 | self.repo_dir = "{}/automx-master".format(self.home_dir) 76 | if os.path.exists(self.repo_dir): 77 | shutil.rmtree(self.repo_dir) 78 | utils.exec_cmd( 79 | "unzip master.zip", sudo_user=self.user, cwd=self.home_dir) 80 | utils.exec_cmd( 81 | "{} setup.py install".format( 82 | python.get_path("python", self.venv_path)), 83 | cwd=self.repo_dir) 84 | 85 | def _deploy_instance(self): 86 | """Copy files to instance dir.""" 87 | if not os.path.exists(self.instance_path): 88 | pw = pwd.getpwnam(self.user) 89 | mode = ( 90 | stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | 91 | stat.S_IROTH | stat.S_IXOTH) 92 | utils.mkdir(self.instance_path, mode, pw[2], pw[3]) 93 | path = "{}/src/automx_wsgi.py".format(self.repo_dir) 94 | utils.exec_cmd("cp {} {}".format(path, self.instance_path), 95 | sudo_user=self.user, cwd=self.home_dir) 96 | 97 | def post_run(self): 98 | """Additional tasks.""" 99 | self._setup_venv() 100 | self._deploy_instance() 101 | system.enable_and_start_service("memcached") 102 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/backup.py: -------------------------------------------------------------------------------- 1 | """Backup script for pre-installed instance.""" 2 | 3 | import os 4 | import pwd 5 | import shutil 6 | import stat 7 | import sys 8 | import datetime 9 | 10 | from .. import database 11 | from .. import utils 12 | from ..constants import DEFAULT_BACKUP_DIRECTORY 13 | 14 | 15 | class Backup: 16 | """ 17 | Backup structure ( {optional} ): 18 | {{backup_directory}} 19 | || 20 | ||--> installer.cfg 21 | ||--> custom 22 | |--> { (copy of) /etc/amavis/conf.d/99-custom } 23 | |--> { (copy of) /etc/postfix/custom_whitelist.cidr } 24 | |--> { (copy of) dkim directory } 25 | |--> {dkim.pem}... 26 | |--> { (copy of) radicale home_dir } 27 | ||--> databases 28 | |--> modoboa.sql 29 | |--> { amavis.sql } 30 | |--> { spamassassin.sql } 31 | ||--> mails 32 | |--> vmails 33 | """ 34 | 35 | def __init__(self, config, silent_backup, backup_path, nomail): 36 | self.config = config 37 | self.backup_path = backup_path 38 | self.nomail = nomail 39 | self.silent_backup = silent_backup 40 | 41 | def validate_path(self, path): 42 | """Check basic condition for backup directory.""" 43 | 44 | path_exists = os.path.exists(path) 45 | 46 | if path_exists and os.path.isfile(path): 47 | utils.error("Error, you provided a file instead of a directory!") 48 | return False 49 | 50 | if not path_exists: 51 | if not self.silent_backup: 52 | create_dir = input( 53 | f"\"{path}\" doesn't exist, would you like to create it? [Y/n]\n").lower() 54 | 55 | if self.silent_backup or (not self.silent_backup and create_dir.startswith("y")): 56 | pw = pwd.getpwnam("root") 57 | utils.mkdir_safe(path, stat.S_IRWXU | 58 | stat.S_IRWXG, pw[2], pw[3]) 59 | else: 60 | utils.error("Error, backup directory not present.") 61 | return False 62 | 63 | if len(os.listdir(path)) != 0: 64 | if not self.silent_backup: 65 | delete_dir = input( 66 | "Warning: backup directory is not empty, it will be purged if you continue... [Y/n]\n").lower() 67 | 68 | if self.silent_backup or (not self.silent_backup and delete_dir.startswith("y")): 69 | try: 70 | os.remove(os.path.join(path, "installer.cfg")) 71 | except FileNotFoundError: 72 | pass 73 | 74 | shutil.rmtree(os.path.join(path, "custom"), 75 | ignore_errors=False) 76 | shutil.rmtree(os.path.join(path, "mails"), ignore_errors=False) 77 | shutil.rmtree(os.path.join(path, "databases"), 78 | ignore_errors=False) 79 | else: 80 | utils.error("Error: backup directory not clean.") 81 | return False 82 | 83 | self.backup_path = path 84 | 85 | pw = pwd.getpwnam("root") 86 | for dir in ["custom/", "databases/"]: 87 | utils.mkdir_safe(os.path.join(self.backup_path, dir), 88 | stat.S_IRWXU | stat.S_IRWXG, pw[2], pw[3]) 89 | return True 90 | 91 | def set_path(self): 92 | """Setup backup directory.""" 93 | if self.silent_backup: 94 | if self.backup_path is None: 95 | if self.config.has_option("backup", "default_path"): 96 | path = self.config.get("backup", "default_path") 97 | else: 98 | path = DEFAULT_BACKUP_DIRECTORY 99 | date = datetime.datetime.now().strftime("%m_%d_%Y_%H_%M") 100 | path = os.path.join(path, f"backup_{date}") 101 | self.validate_path(path) 102 | else: 103 | if not self.validate_path(self.backup_path): 104 | utils.printcolor( 105 | f"Path provided: {self.backup_path}", utils.BLUE) 106 | sys.exit(1) 107 | else: 108 | user_value = None 109 | while user_value == "" or user_value is None or not self.validate_path(user_value): 110 | utils.printcolor( 111 | "Enter backup path (it must be an empty directory)", utils.MAGENTA) 112 | utils.printcolor("CTRL+C to cancel", utils.MAGENTA) 113 | user_value = utils.user_input("-> ") 114 | 115 | def config_file_backup(self): 116 | utils.copy_file("installer.cfg", self.backup_path) 117 | 118 | def mail_backup(self): 119 | if self.nomail: 120 | utils.printcolor( 121 | "Skipping mail backup, no-mail argument provided", utils.MAGENTA) 122 | return 123 | 124 | utils.printcolor("Backing up mails", utils.MAGENTA) 125 | 126 | home_path = self.config.get("dovecot", "home_dir") 127 | 128 | if not os.path.exists(home_path) or os.path.isfile(home_path): 129 | utils.error("Error backing up Email, provided path " 130 | f" ({home_path}) seems not right...") 131 | 132 | else: 133 | dst = os.path.join(self.backup_path, "mails/") 134 | 135 | if os.path.exists(dst): 136 | shutil.rmtree(dst) 137 | 138 | shutil.copytree(home_path, dst) 139 | utils.printcolor("Mail backup complete!", utils.GREEN) 140 | 141 | def custom_config_backup(self): 142 | """ 143 | Custom config : 144 | - DKIM keys: {{keys_storage_dir}} 145 | - Radicale collection (calendat, contacts): {{home_dir}} 146 | - Amavis : /etc/amavis/conf.d/99-custom 147 | - Postwhite : /etc/postwhite.conf 148 | Feel free to suggest to add others! 149 | """ 150 | utils.printcolor( 151 | "Backing up some custom configuration...", utils.MAGENTA) 152 | 153 | custom_path = os.path.join( 154 | self.backup_path, "custom") 155 | 156 | # DKIM Key 157 | if (self.config.has_option("opendkim", "enabled") and 158 | self.config.getboolean("opendkim", "enabled")): 159 | dkim_keys = self.config.get( 160 | "opendkim", "keys_storage_dir", fallback="/var/lib/dkim") 161 | if os.path.isdir(dkim_keys): 162 | shutil.copytree(dkim_keys, os.path.join(custom_path, "dkim")) 163 | utils.printcolor( 164 | "DKIM keys saved!", utils.GREEN) 165 | 166 | # Radicale Collections 167 | if (self.config.has_option("radicale", "enabled") and 168 | self.config.getboolean("radicale", "enabled")): 169 | radicale_backup = os.path.join(self.config.get( 170 | "radicale", "home_dir", fallback="/srv/radicale"), "collections") 171 | if os.path.isdir(radicale_backup): 172 | shutil.copytree(radicale_backup, os.path.join( 173 | custom_path, "radicale")) 174 | utils.printcolor("Radicale files saved", utils.GREEN) 175 | 176 | # AMAVIS 177 | if (self.config.has_option("amavis", "enabled") and 178 | self.config.getboolean("amavis", "enabled")): 179 | amavis_custom = "/etc/amavis/conf.d/99-custom" 180 | if os.path.isfile(amavis_custom): 181 | utils.copy_file(amavis_custom, custom_path) 182 | utils.printcolor( 183 | "Amavis custom configuration saved!", utils.GREEN) 184 | 185 | # POSTWHITE 186 | if (self.config.has_option("postwhite", "enabled") and 187 | self.config.getboolean("postwhite", "enabled")): 188 | postswhite_custom = "/etc/postwhite.conf" 189 | if os.path.isfile(postswhite_custom): 190 | utils.copy_file(postswhite_custom, custom_path) 191 | utils.printcolor( 192 | "Postwhite configuration saved!", utils.GREEN) 193 | 194 | def database_backup(self): 195 | """Backing up databases""" 196 | 197 | utils.printcolor("Backing up databases...", utils.MAGENTA) 198 | 199 | self.database_dump("modoboa") 200 | self.database_dump("amavis") 201 | self.database_dump("spamassassin") 202 | 203 | def database_dump(self, app_name): 204 | 205 | dump_path = os.path.join(self.backup_path, "databases") 206 | backend = database.get_backend(self.config) 207 | 208 | if app_name == "modoboa" or (self.config.has_option(app_name, "enabled") and 209 | self.config.getboolean(app_name, "enabled")): 210 | dbname = self.config.get(app_name, "dbname") 211 | dbuser = self.config.get(app_name, "dbuser") 212 | dbpasswd = self.config.get(app_name, "dbpassword") 213 | backend.dump_database(dbname, dbuser, dbpasswd, 214 | os.path.join(dump_path, f"{app_name}.sql")) 215 | 216 | def backup_completed(self): 217 | utils.printcolor("Backup process done, your backup is available here:" 218 | f"--> {self.backup_path}", utils.GREEN) 219 | 220 | def run(self): 221 | self.set_path() 222 | self.config_file_backup() 223 | self.mail_backup() 224 | self.custom_config_backup() 225 | self.database_backup() 226 | self.backup_completed() 227 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/base.py: -------------------------------------------------------------------------------- 1 | """Base classes.""" 2 | 3 | import os 4 | import sys 5 | 6 | from .. import database 7 | from .. import package 8 | from .. import python 9 | from .. import system 10 | from .. import utils 11 | 12 | 13 | class Installer: 14 | """Simple installer for one application.""" 15 | 16 | appname = None 17 | no_daemon = False 18 | daemon_name = None 19 | packages = {} 20 | with_user = False 21 | with_db = False 22 | config_files = [] 23 | 24 | def __init__(self, config, upgrade: bool, archive_path: str): 25 | """Get configuration.""" 26 | self.config = config 27 | self.upgrade = upgrade 28 | self.archive_path = archive_path 29 | if self.config.has_section(self.appname): 30 | self.app_config = dict(self.config.items(self.appname)) 31 | self.dbengine = self.config.get("database", "engine") 32 | # Used to install system packages 33 | self.db_driver = ( 34 | "pgsql" if self.dbengine == "postgres" else self.dbengine) 35 | self.backend = database.get_backend(self.config) 36 | self.dbhost = self.config.get("database", "host") 37 | self.dbport = self.config.get( 38 | "database", "port", fallback=self.backend.default_port) 39 | self._config_dir = None 40 | if not self.with_db: 41 | return 42 | self.dbname = self.config.get(self.appname, "dbname") 43 | self.dbuser = self.config.get(self.appname, "dbuser") 44 | self.dbpasswd = self.config.get(self.appname, "dbpassword") 45 | 46 | @property 47 | def modoboa_2_2_or_greater(self): 48 | # Check if modoboa version > 2.2 49 | modoboa_version = python.get_package_version( 50 | "modoboa", 51 | self.config.get("modoboa", "venv_path"), 52 | sudo_user=self.config.get("modoboa", "user") 53 | ) 54 | condition = ( 55 | (int(modoboa_version[0]) == 2 and int(modoboa_version[1]) >= 2) or 56 | int(modoboa_version[0]) > 2 57 | ) 58 | return condition 59 | 60 | @property 61 | def config_dir(self): 62 | """Return main configuration directory.""" 63 | if self._config_dir is None and self.config.has_option( 64 | self.appname, "config_dir"): 65 | self._config_dir = self.config.get(self.appname, "config_dir") 66 | return self._config_dir 67 | 68 | def get_sql_schema_path(self): 69 | """Return a schema to install.""" 70 | return None 71 | 72 | def get_sql_schema_from_backup(self): 73 | """Retrieve a dump path from a previous backup.""" 74 | utils.printcolor( 75 | f"Trying to restore {self.appname} database from backup.", 76 | utils.MAGENTA 77 | ) 78 | database_backup_path = os.path.join( 79 | self.archive_path, f"databases/{self.appname}.sql") 80 | if os.path.isfile(database_backup_path): 81 | utils.success(f"SQL dump found in backup for {self.appname}!") 82 | return database_backup_path 83 | return None 84 | 85 | def get_file_path(self, fname): 86 | """Return the absolute path of this file.""" 87 | return os.path.abspath( 88 | os.path.join( 89 | os.path.dirname(__file__), "files", self.appname, fname) 90 | ) 91 | 92 | def setup_database(self): 93 | """Setup a database.""" 94 | if not self.with_db: 95 | return 96 | self.backend.create_user(self.dbuser, self.dbpasswd) 97 | self.backend.create_database(self.dbname, self.dbuser) 98 | schema = None 99 | if self.archive_path: 100 | schema = self.get_sql_schema_from_backup() 101 | if not schema: 102 | schema = self.get_sql_schema_path() 103 | if schema: 104 | self.backend.load_sql_file( 105 | self.dbname, self.dbuser, self.dbpasswd, schema) 106 | 107 | def setup_user(self): 108 | """Setup a system user.""" 109 | if not self.with_user: 110 | return 111 | self.user = self.config.get(self.appname, "user") 112 | if self.config.has_option(self.appname, "home_dir"): 113 | self.home_dir = self.config.get(self.appname, "home_dir") 114 | else: 115 | self.home_dir = None 116 | system.create_user(self.user, self.home_dir) 117 | 118 | def get_template_context(self): 119 | """Return context used for template rendering.""" 120 | context = { 121 | "dbengine": ( 122 | "Pg" if self.dbengine == "postgres" else self.dbengine), 123 | "dbhost": self.dbhost, 124 | "dbport": self.dbport, 125 | } 126 | for option, value in self.config.items("general"): 127 | context[option] = value 128 | for option, value in self.config.items(self.appname): 129 | context[option] = value 130 | for section in self.config.sections(): 131 | if section == self.appname: 132 | continue 133 | if self.config.has_option(section, "enabled"): 134 | val = "" if self.config.getboolean(section, "enabled") else "#" 135 | context["{}_enabled".format(section)] = val 136 | return context 137 | 138 | def get_packages(self): 139 | """Return the list of packages to install.""" 140 | return self.packages.get(package.backend.FORMAT, {}) 141 | 142 | def install_packages(self): 143 | """Install required packages.""" 144 | packages = self.get_packages() 145 | if not packages: 146 | return 147 | exitcode, output = package.backend.install_many(packages) 148 | if exitcode: 149 | utils.error("Failed to install dependencies") 150 | sys.exit(1) 151 | 152 | def get_config_files(self): 153 | """Return the list of configuration files to copy.""" 154 | return self.config_files 155 | 156 | def install_config_files(self): 157 | """Install configuration files.""" 158 | config_files = self.get_config_files() 159 | if not config_files: 160 | return 161 | context = self.get_template_context() 162 | for ftpl in config_files: 163 | if "=" in ftpl: 164 | ftpl, dstname = ftpl.split("=") 165 | else: 166 | dstname = ftpl 167 | src = self.get_file_path("{}.tpl".format(ftpl)) 168 | dst = dstname 169 | if not dst.startswith("/"): 170 | dst = os.path.join(self.config_dir, dst) 171 | utils.copy_from_template(src, dst, context) 172 | 173 | def backup(self, path): 174 | if self.with_db: 175 | self._dump_database(path) 176 | custom_backup_path = os.path.join(path, "custom") 177 | self.custom_backup(custom_backup_path) 178 | 179 | def custom_backup(self, path): 180 | """Override this method in subscripts to add custom backup content.""" 181 | pass 182 | 183 | def restore(self): 184 | """Restore from a previous backup.""" 185 | pass 186 | 187 | def get_daemon_name(self): 188 | """Return daemon name if defined.""" 189 | return self.daemon_name if self.daemon_name else self.appname 190 | 191 | def restart_daemon(self): 192 | """Restart daemon process.""" 193 | if self.no_daemon: 194 | return 195 | name = self.get_daemon_name() 196 | system.enable_and_start_service(name) 197 | 198 | def run(self): 199 | """Run the installer.""" 200 | self.pre_run() 201 | self.install_packages() 202 | self.setup_user() 203 | if not self.upgrade: 204 | self.setup_database() 205 | self.install_config_files() 206 | self.post_run() 207 | if self.archive_path: 208 | self.restore() 209 | self.restart_daemon() 210 | 211 | def _dump_database(self, backup_path: str): 212 | """Create a new database dump for this app.""" 213 | target_dir = os.path.join(backup_path, "databases") 214 | target_file = os.path.join(target_dir, f"{self.appname}.sql") 215 | self.backend.dump_database( 216 | self.dbname, self.dbuser, self.dbpasswd, target_file) 217 | 218 | def pre_run(self): 219 | """Tasks to execute before the installer starts.""" 220 | pass 221 | 222 | def post_run(self): 223 | """Additionnal tasks.""" 224 | pass 225 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/clamav.py: -------------------------------------------------------------------------------- 1 | """ClamAV related tools.""" 2 | 3 | from .. import package 4 | from .. import utils 5 | from .. import system 6 | 7 | from . import base 8 | 9 | 10 | class Clamav(base.Installer): 11 | 12 | """ClamAV installer.""" 13 | 14 | appname = "clamav" 15 | packages = { 16 | "deb": ["clamav-daemon"], 17 | "rpm": [ 18 | "clamav", "clamav-update", "clamav-server", "clamav-server-systemd" 19 | ], 20 | } 21 | 22 | def get_daemon_name(self): 23 | """Return appropriate daemon name.""" 24 | if package.backend.FORMAT == "rpm": 25 | return "clamd@amavisd" 26 | return "clamav-daemon" 27 | 28 | @property 29 | def config_dir(self): 30 | """Return appropriate config dir.""" 31 | if package.backend.FORMAT == "rpm": 32 | return "/etc" 33 | return "" 34 | 35 | def get_config_files(self): 36 | """Return appropriate config files.""" 37 | if package.backend.FORMAT == "rpm": 38 | return ["sysconfig/clamd.amavisd", "tmpfiles.d/clamd.amavisd.conf"] 39 | return [] 40 | 41 | def post_run(self): 42 | """Additional tasks.""" 43 | if package.backend.FORMAT == "deb": 44 | user = self.config.get(self.appname, "user") 45 | system.add_user_to_group( 46 | user, self.config.get("amavis", "user") 47 | ) 48 | pattern = ( 49 | "s/^AllowSupplementaryGroups false/" 50 | "AllowSupplementaryGroups true/") 51 | utils.exec_cmd( 52 | "perl -pi -e '{}' /etc/clamav/clamd.conf".format(pattern)) 53 | else: 54 | user = "clamupdate" 55 | utils.exec_cmd( 56 | "perl -pi -e 's/^Example/#Example/' /etc/freshclam.conf") 57 | # Check if not present before 58 | path = "/usr/lib/systemd/system/clamd@.service" 59 | code, output = utils.exec_cmd( 60 | r"grep 'WantedBy\s*=\s*multi-user.target' {}".format(path)) 61 | if code: 62 | utils.exec_cmd( 63 | """cat <> {} 64 | 65 | [Install] 66 | WantedBy=multi-user.target 67 | EOM 68 | """.format(path)) 69 | 70 | if utils.dist_name() in ["debian", "ubuntu"]: 71 | # Stop freshclam daemon to allow manual download 72 | utils.exec_cmd("service clamav-freshclam stop") 73 | utils.exec_cmd("freshclam", sudo_user=user, login=False) 74 | utils.exec_cmd("service clamav-freshclam start") 75 | else: 76 | utils.exec_cmd("freshclam", sudo_user=user, login=False) 77 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/dovecot.py: -------------------------------------------------------------------------------- 1 | """Dovecot related tools.""" 2 | 3 | import glob 4 | import os 5 | import pwd 6 | import shutil 7 | import uuid 8 | 9 | from .. import database 10 | from .. import package 11 | from .. import system 12 | from .. import utils 13 | 14 | from . import base 15 | 16 | 17 | class Dovecot(base.Installer): 18 | """Dovecot installer.""" 19 | 20 | appname = "dovecot" 21 | packages = { 22 | "deb": [ 23 | "dovecot-imapd", "dovecot-lmtpd", "dovecot-managesieved", 24 | "dovecot-sieve"], 25 | "rpm": [ 26 | "dovecot", "dovecot-pigeonhole"] 27 | } 28 | config_files = [ 29 | "dovecot.conf", "dovecot-dict-sql.conf.ext", "conf.d/10-ssl.conf", 30 | "conf.d/10-master.conf", "conf.d/20-lmtp.conf", "conf.d/10-ssl-keys.try", 31 | "conf.d/dovecot-oauth2.conf.ext" 32 | ] 33 | with_user = True 34 | 35 | def setup_user(self): 36 | """Setup mailbox user.""" 37 | super().setup_user() 38 | self.mailboxes_owner = self.app_config["mailboxes_owner"] 39 | system.create_user(self.mailboxes_owner, self.home_dir) 40 | 41 | def get_config_files(self): 42 | """Additional config files.""" 43 | return self.config_files + [ 44 | "dovecot-sql-{}.conf.ext=dovecot-sql.conf.ext" 45 | .format(self.dbengine), 46 | "dovecot-sql-master-{}.conf.ext=dovecot-sql-master.conf.ext" 47 | .format(self.dbengine), 48 | "postlogin-{}.sh=/usr/local/bin/postlogin.sh" 49 | .format(self.dbengine), 50 | ] 51 | 52 | def get_packages(self): 53 | """Additional packages.""" 54 | packages = ["dovecot-{}".format(self.db_driver)] 55 | if package.backend.FORMAT == "deb": 56 | if "pop3" in self.config.get("dovecot", "extra_protocols"): 57 | packages += ["dovecot-pop3d"] 58 | packages += super().get_packages() 59 | backports_codename = getattr(self, "backports_codename", None) 60 | if backports_codename: 61 | packages = [f"{package}/{backports_codename}-backports" for package in packages] 62 | return packages 63 | 64 | def install_packages(self): 65 | """Preconfigure Dovecot if needed.""" 66 | name, version = utils.dist_info() 67 | name = name.lower() 68 | if name.startswith("debian") and version.startswith("12"): 69 | package.backend.enable_backports("bookworm") 70 | self.backports_codename = "bookworm" 71 | package.backend.preconfigure( 72 | "dovecot-core", "create-ssl-cert", "boolean", "false") 73 | super().install_packages() 74 | 75 | def create_oauth2_app(self): 76 | """Create a application for Oauth2 authentication.""" 77 | # FIXME: how can we check that application already exists ? 78 | venv_path = self.config.get("modoboa", "venv_path") 79 | python_path = os.path.join(venv_path, "bin", "python") 80 | instance_path = self.config.get("modoboa", "instance_path") 81 | script_path = os.path.join(instance_path, "manage.py") 82 | client_id = "dovecot" 83 | client_secret = str(uuid.uuid4()) 84 | cmd = ( 85 | f"{python_path} {script_path} createapplication " 86 | f"--name=Dovecot --skip-authorization " 87 | f"--client-id={client_id} --client-secret={client_secret} " 88 | f"confidential client-credentials" 89 | ) 90 | utils.exec_cmd(cmd) 91 | return client_id, client_secret 92 | 93 | def get_template_context(self): 94 | """Additional variables.""" 95 | context = super().get_template_context() 96 | pw_mailbox = pwd.getpwnam(self.mailboxes_owner) 97 | dovecot_package = {"deb": "dovecot-core", "rpm": "dovecot"} 98 | ssl_protocol_parameter = "ssl_protocols" 99 | if package.backend.get_installed_version(dovecot_package[package.backend.FORMAT]) > "2.3": 100 | ssl_protocol_parameter = "ssl_min_protocol" 101 | ssl_protocols = "!SSLv2 !SSLv3" 102 | if package.backend.get_installed_version("openssl").startswith("1.1") \ 103 | or package.backend.get_installed_version("openssl").startswith("3"): 104 | ssl_protocols = "!SSLv3" 105 | if ssl_protocol_parameter == "ssl_min_protocol": 106 | ssl_protocols = "TLSv1" 107 | if "centos" in utils.dist_name(): 108 | protocols = "protocols = imap lmtp sieve" 109 | extra_protocols = self.config.get("dovecot", "extra_protocols") 110 | if extra_protocols: 111 | protocols += " {}".format(extra_protocols) 112 | else: 113 | # Protocols are automatically guessed on debian/ubuntu 114 | protocols = "" 115 | 116 | oauth2_client_id, oauth2_client_secret = self.create_oauth2_app() 117 | hostname = self.config.get("general", "hostname") 118 | oauth2_introspection_url = ( 119 | f"https://{oauth2_client_id}:{oauth2_client_secret}" 120 | f"@{hostname}/api/o/introspect/" 121 | ) 122 | 123 | context.update({ 124 | "db_driver": self.db_driver, 125 | "mailboxes_owner_uid": pw_mailbox[2], 126 | "mailboxes_owner_gid": pw_mailbox[3], 127 | "mailbox_owner": self.mailboxes_owner, 128 | "modoboa_user": self.config.get("modoboa", "user"), 129 | "modoboa_dbname": self.config.get("modoboa", "dbname"), 130 | "modoboa_dbuser": self.config.get("modoboa", "dbuser"), 131 | "modoboa_dbpassword": self.config.get("modoboa", "dbpassword"), 132 | "protocols": protocols, 133 | "ssl_protocols": ssl_protocols, 134 | "ssl_protocol_parameter": ssl_protocol_parameter, 135 | "radicale_user": self.config.get("radicale", "user"), 136 | "radicale_auth_socket_path": os.path.basename( 137 | self.config.get("dovecot", "radicale_auth_socket_path")), 138 | "modoboa_2_2_or_greater": "" if self.modoboa_2_2_or_greater else "#", 139 | "not_modoboa_2_2_or_greater": "" if not self.modoboa_2_2_or_greater else "#", 140 | "oauth2_introspection_url": oauth2_introspection_url 141 | }) 142 | return context 143 | 144 | def post_run(self): 145 | """Additional tasks.""" 146 | if self.dbengine == "postgres": 147 | dbname = self.config.get("modoboa", "dbname") 148 | dbuser = self.config.get("modoboa", "dbuser") 149 | dbpassword = self.config.get("modoboa", "dbpassword") 150 | backend = database.get_backend(self.config) 151 | backend.load_sql_file( 152 | dbname, dbuser, dbpassword, 153 | self.get_file_path("install_modoboa_postgres_trigger.sql") 154 | ) 155 | backend.load_sql_file( 156 | dbname, dbuser, dbpassword, 157 | self.get_file_path("fix_modoboa_postgres_schema.sql") 158 | ) 159 | for f in glob.glob("{}/*".format(self.get_file_path("conf.d"))): 160 | utils.copy_file(f, "{}/conf.d".format(self.config_dir)) 161 | # Make postlogin script executable 162 | utils.exec_cmd("chmod +x /usr/local/bin/postlogin.sh") 163 | # Only root should have read access to the 10-ssl-keys.try 164 | # See https://github.com/modoboa/modoboa/issues/2570 165 | utils.exec_cmd("chmod 600 /etc/dovecot/conf.d/10-ssl-keys.try") 166 | # Add mailboxes user to dovecot group for modoboa mailbox commands. 167 | # See https://github.com/modoboa/modoboa/issues/2157. 168 | system.add_user_to_group(self.mailboxes_owner, 'dovecot') 169 | 170 | def restart_daemon(self): 171 | """Restart daemon process. 172 | 173 | Note: we don't capture output and manually redirect stdout to 174 | /dev/null since this command may hang depending on the process 175 | being restarted (dovecot for example)... 176 | 177 | """ 178 | code, output = utils.exec_cmd("service dovecot status") 179 | action = "start" if code else "restart" 180 | utils.exec_cmd( 181 | "service {} {} > /dev/null 2>&1".format(self.appname, action), 182 | capture_output=False) 183 | system.enable_service(self.get_daemon_name()) 184 | 185 | def backup(self, path): 186 | """Backup emails.""" 187 | home_dir = self.config.get("dovecot", "home_dir") 188 | utils.printcolor("Backing up mails", utils.MAGENTA) 189 | if not os.path.exists(home_dir) or os.path.isfile(home_dir): 190 | utils.error("Error backing up emails, provided path " 191 | f" ({home_dir}) seems not right...") 192 | return 193 | 194 | dst = os.path.join(path, "mails/") 195 | if os.path.exists(dst): 196 | shutil.rmtree(dst) 197 | shutil.copytree(home_dir, dst) 198 | utils.success("Mail backup complete!") 199 | 200 | def restore(self): 201 | """Restore emails.""" 202 | home_dir = self.config.get("dovecot", "home_dir") 203 | mail_dir = os.path.join(self.archive_path, "mails/") 204 | if len(os.listdir(mail_dir)) > 0: 205 | utils.success("Copying mail backup over dovecot directory.") 206 | if os.path.exists(home_dir): 207 | shutil.rmtree(home_dir) 208 | shutil.copytree(mail_dir, home_dir) 209 | # Resetting permission for vmail 210 | for dirpath, dirnames, filenames in os.walk(home_dir): 211 | shutil.chown(dirpath, self.mailboxes_owner, self.mailboxes_owner) 212 | for filename in filenames: 213 | shutil.chown(os.path.join(dirpath, filename), 214 | self.mailboxes_owner, self.mailboxes_owner) 215 | else: 216 | utils.printcolor( 217 | "It seems that emails were not backed up, skipping restoration.", 218 | utils.MAGENTA 219 | ) 220 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/fail2ban.py: -------------------------------------------------------------------------------- 1 | """fail2ban related functions.""" 2 | 3 | from . import base 4 | 5 | 6 | class Fail2ban(base.Installer): 7 | """Fail2ban installer.""" 8 | 9 | appname = "fail2ban" 10 | packages = { 11 | "deb": ["fail2ban"], 12 | "rpm": ["fail2ban"] 13 | } 14 | config_files = [ 15 | "jail.d/modoboa.conf", 16 | "filter.d/modoboa-auth.conf", 17 | ] 18 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/amavis/conf.d/05-node_id.tpl: -------------------------------------------------------------------------------- 1 | use strict; 2 | 3 | # $myhostname is used by amavisd-new for node identification, and it is 4 | # important to get it right (e.g. for ESMTP EHLO, loop detection, and so on). 5 | 6 | # chomp($myhostname = `hostname --fqdn`); 7 | 8 | # To manually set $myhostname, edit the following line with the correct Fully 9 | # Qualified Domain Name (FQDN) and remove the # at the beginning of the line. 10 | # 11 | $myhostname = "%hostname"; 12 | 13 | 1; # ensure a defined return 14 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/amavis/conf.d/15-content_filter_mode.tpl: -------------------------------------------------------------------------------- 1 | use strict; 2 | 3 | # You can modify this file to re-enable SPAM checking through spamassassin 4 | # and to re-enable antivirus checking. 5 | 6 | # 7 | # Default antivirus checking mode 8 | # Please note, that anti-virus checking is DISABLED by 9 | # default. 10 | # If You wish to enable it, please uncomment the following lines: 11 | 12 | %{clamav_enabled}@bypass_virus_checks_maps = ( 13 | %{clamav_enabled} \%%bypass_virus_checks, \@bypass_virus_checks_acl, \$bypass_virus_checks_re); 14 | 15 | # 16 | # Default SPAM checking mode 17 | # Please note, that anti-spam checking is DISABLED by 18 | # default. 19 | # If You wish to enable it, please uncomment the following lines: 20 | 21 | 22 | @bypass_spam_checks_maps = ( 23 | \%%bypass_spam_checks, \@bypass_spam_checks_acl, \$bypass_spam_checks_re); 24 | 25 | 1; # ensure a defined return 26 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/amavis/conf.d/50-user.tpl: -------------------------------------------------------------------------------- 1 | use strict; 2 | 3 | # General settings 4 | # 5 | $inet_socket_port = [9998, 10024, 10026]; 6 | $max_servers = %max_servers; 7 | 8 | # SQL configuration 9 | # 10 | @lookup_sql_dsn = ( [ 'DBI:%dbengine:database=%dbname;host=%dbhost;port=%dbport', '%dbuser', '%dbpassword' ]); 11 | @storage_sql_dsn = @lookup_sql_dsn; 12 | $sql_allow_8bit_address = 1; 13 | 14 | # Quarantine methods 15 | # 16 | $virus_quarantine_method = 'sql:'; 17 | $spam_quarantine_method = 'sql:'; 18 | $banned_files_quarantine_method = 'sql:'; 19 | $bad_header_quarantine_method = 'sql:'; 20 | 21 | # Discard spam 22 | $final_spam_destiny = D_DISCARD; 23 | 24 | # Policy banks 25 | # 26 | $interface_policy{'9998'} = 'AM.PDP-INET'; 27 | 28 | $policy_bank{'AM.PDP-INET'} = { 29 | protocol => 'AM.PDP', 30 | inet_acl => [qw( 127.0.0.1 )], 31 | }; 32 | 33 | # switch policy bank to 'ORIGINATING' for mail received on port 10026: 34 | $interface_policy{'10026'} = 'ORIGINATING'; 35 | 36 | $policy_bank{'ORIGINATING'} = { # mail originating from our users 37 | originating => 1, # indicates client is ours, allows signing 38 | # force MTA to convert mail to 7-bit before DKIM signing 39 | # to avoid later conversions which could destroy signature: 40 | smtpd_discard_ehlo_keywords => ['8BITMIME'], 41 | }; 42 | 43 | 1; # ensure a defined return; 44 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/automx/automx.conf.tpl: -------------------------------------------------------------------------------- 1 | [automx] 2 | provider = %domain 3 | domains = * 4 | 5 | #debug=yes 6 | #logfile = /srv/automx/automx.log 7 | 8 | # Protect against DoS 9 | memcache = 127.0.0.1:11211 10 | memcache_ttl = 600 11 | client_error_limit = 20 12 | rate_limit_exception_networks = 127.0.0.0/8, ::1/128 13 | 14 | [global] 15 | backend = sql 16 | action = settings 17 | account_type = email 18 | host = %sql_dsn 19 | query = %sql_query 20 | result_attrs = display_name, email 21 | 22 | display_name = ${display_name} 23 | 24 | smtp = yes 25 | smtp_server = %hostname 26 | smtp_port = 587 27 | smtp_encryption = starttls 28 | smtp_auth = plaintext 29 | smtp_auth_identity = ${email} 30 | smtp_refresh_ttl = 6 31 | smtp_default = yes 32 | 33 | imap = yes 34 | imap_server = %hostname 35 | imap_port = 143 36 | imap_encryption = starttls 37 | imap_auth = plaintext 38 | imap_auth_identity = ${email} 39 | imap_refresh_ttl = 6 40 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/clamav/sysconfig/clamd.amavisd.tpl: -------------------------------------------------------------------------------- 1 | CLAMD_CONFIGFILE=/etc/clamd.d/amavisd.conf 2 | CLAMD_SOCKET=/var/run/clamd.amavisd/clamd.sock 3 | #CLAMD_OPTIONS= 4 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/clamav/tmpfiles.d/clamd.amavisd.conf.tpl: -------------------------------------------------------------------------------- 1 | d /var/run/clamd.amavisd 0755 amavis amavis - 2 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/dovecot/conf.d/10-auth.conf: -------------------------------------------------------------------------------- 1 | ## 2 | ## Authentication processes 3 | ## 4 | 5 | # Disable LOGIN command and all other plaintext authentications unless 6 | # SSL/TLS is used (LOGINDISABLED capability). Note that if the remote IP 7 | # matches the local IP (ie. you're connecting from the same computer), the 8 | # connection is considered secure and plaintext authentication is allowed. 9 | #disable_plaintext_auth = yes 10 | 11 | # Authentication cache size (e.g. 10M). 0 means it's disabled. Note that 12 | # bsdauth, PAM and vpopmail require cache_key to be set for caching to be used. 13 | #auth_cache_size = 0 14 | # Time to live for cached data. After TTL expires the cached record is no 15 | # longer used, *except* if the main database lookup returns internal failure. 16 | # We also try to handle password changes automatically: If user's previous 17 | # authentication was successful, but this one wasn't, the cache isn't used. 18 | # For now this works only with plaintext authentication. 19 | #auth_cache_ttl = 1 hour 20 | # TTL for negative hits (user not found, password mismatch). 21 | # 0 disables caching them completely. 22 | #auth_cache_negative_ttl = 1 hour 23 | 24 | # Space separated list of realms for SASL authentication mechanisms that need 25 | # them. You can leave it empty if you don't want to support multiple realms. 26 | # Many clients simply use the first one listed here, so keep the default realm 27 | # first. 28 | #auth_realms = 29 | 30 | # Default realm/domain to use if none was specified. This is used for both 31 | # SASL realms and appending @domain to username in plaintext logins. 32 | #auth_default_realm = 33 | 34 | # List of allowed characters in username. If the user-given username contains 35 | # a character not listed in here, the login automatically fails. This is just 36 | # an extra check to make sure user can't exploit any potential quote escaping 37 | # vulnerabilities with SQL/LDAP databases. If you want to allow all characters, 38 | # set this value to empty. 39 | #auth_username_chars = abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-_@ 40 | 41 | # Username character translations before it's looked up from databases. The 42 | # value contains series of from -> to characters. For example "#@/@" means 43 | # that '#' and '/' characters are translated to '@'. 44 | #auth_username_translation = 45 | 46 | # Username formatting before it's looked up from databases. You can use 47 | # the standard variables here, eg. %Lu would lowercase the username, %n would 48 | # drop away the domain if it was given, or "%n-AT-%d" would change the '@' into 49 | # "-AT-". This translation is done after auth_username_translation changes. 50 | auth_username_format = %Lu 51 | 52 | # If you want to allow master users to log in by specifying the master 53 | # username within the normal username string (ie. not using SASL mechanism's 54 | # support for it), you can specify the separator character here. The format 55 | # is then . UW-IMAP uses "*" as the 56 | # separator, so that could be a good choice. 57 | auth_master_user_separator = * 58 | 59 | # Username to use for users logging in with ANONYMOUS SASL mechanism 60 | #auth_anonymous_username = anonymous 61 | 62 | # Maximum number of dovecot-auth worker processes. They're used to execute 63 | # blocking passdb and userdb queries (eg. MySQL and PAM). They're 64 | # automatically created and destroyed as needed. 65 | #auth_worker_max_count = 30 66 | 67 | # Host name to use in GSSAPI principal names. The default is to use the 68 | # name returned by gethostname(). Use "$ALL" (with quotes) to allow all keytab 69 | # entries. 70 | #auth_gssapi_hostname = 71 | 72 | # Kerberos keytab to use for the GSSAPI mechanism. Will use the system 73 | # default (usually /etc/krb5.keytab) if not specified. You may need to change 74 | # the auth service to run as root to be able to read this file. 75 | #auth_krb5_keytab = 76 | 77 | # Do NTLM and GSS-SPNEGO authentication using Samba's winbind daemon and 78 | # ntlm_auth helper. 79 | #auth_use_winbind = no 80 | 81 | # Path for Samba's ntlm_auth helper binary. 82 | #auth_winbind_helper_path = /usr/bin/ntlm_auth 83 | 84 | # Time to delay before replying to failed authentications. 85 | #auth_failure_delay = 2 secs 86 | 87 | # Require a valid SSL client certificate or the authentication fails. 88 | #auth_ssl_require_client_cert = no 89 | 90 | # Take the username from client's SSL certificate, using 91 | # X509_NAME_get_text_by_NID() which returns the subject's DN's 92 | # CommonName. 93 | #auth_ssl_username_from_cert = no 94 | 95 | # Space separated list of wanted authentication mechanisms: 96 | # plain login digest-md5 cram-md5 ntlm rpa apop anonymous gssapi otp skey 97 | # gss-spnego 98 | # NOTE: See also disable_plaintext_auth setting. 99 | auth_mechanisms = plain login oauthbearer xoauth2 100 | 101 | ## 102 | ## Password and user databases 103 | ## 104 | 105 | # 106 | # Password database is used to verify user's password (and nothing more). 107 | # You can have multiple passdbs and userdbs. This is useful if you want to 108 | # allow both system users (/etc/passwd) and virtual users to login without 109 | # duplicating the system users into virtual database. 110 | # 111 | # 112 | # 113 | # User database specifies where mails are located and what user/group IDs 114 | # own them. For single-UID configuration use "static" userdb. 115 | # 116 | # 117 | 118 | #!include auth-deny.conf.ext 119 | #!include auth-master.conf.ext 120 | 121 | #!include auth-system.conf.ext 122 | !include auth-sql.conf.ext 123 | !include auth-oauth2.conf.ext 124 | #!include auth-ldap.conf.ext 125 | #!include auth-passwdfile.conf.ext 126 | #!include auth-checkpassword.conf.ext 127 | #!include auth-vpopmail.conf.ext 128 | #!include auth-static.conf.ext 129 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/dovecot/conf.d/10-master.conf.tpl: -------------------------------------------------------------------------------- 1 | #default_process_limit = 100 2 | #default_client_limit = 1000 3 | 4 | # Default VSZ (virtual memory size) limit for service processes. This is mainly 5 | # intended to catch and kill processes that leak memory before they eat up 6 | # everything. 7 | #default_vsz_limit = 256M 8 | 9 | # Login user is internally used by login processes. This is the most untrusted 10 | # user in Dovecot system. It shouldn't have access to anything at all. 11 | #default_login_user = dovenull 12 | 13 | # Internal user is used by unprivileged processes. It should be separate from 14 | # login user, so that login processes can't disturb other processes. 15 | #default_internal_user = dovecot 16 | 17 | service imap-login { 18 | inet_listener imap { 19 | #port = 143 20 | } 21 | inet_listener imaps { 22 | #port = 993 23 | #ssl = yes 24 | } 25 | 26 | # Number of connections to handle before starting a new process. Typically 27 | # the only useful values are 0 (unlimited) or 1. 1 is more secure, but 0 28 | # is faster. 29 | #service_count = 1 30 | 31 | # Number of processes to always keep waiting for more connections. 32 | #process_min_avail = 0 33 | 34 | # If you set service_count=0, you probably need to grow this. 35 | #vsz_limit = $default_vsz_limit 36 | } 37 | 38 | service pop3-login { 39 | inet_listener pop3 { 40 | #port = 110 41 | } 42 | inet_listener pop3s { 43 | #port = 995 44 | #ssl = yes 45 | } 46 | } 47 | 48 | service lmtp { 49 | unix_listener lmtp { 50 | #mode = 0666 51 | } 52 | 53 | # Create inet listener only if you can't use the above UNIX socket 54 | #inet_listener lmtp { 55 | # Avoid making LMTP visible for the entire internet 56 | #address = 57 | #port = 58 | #} 59 | 60 | unix_listener /var/spool/postfix/private/dovecot-lmtp { 61 | mode = 0600 62 | user = postfix 63 | group = postfix 64 | } 65 | } 66 | 67 | service imap { 68 | # Most of the memory goes to mmap()ing files. You may need to increase this 69 | # limit if you have huge mailboxes. 70 | #vsz_limit = $default_vsz_limit 71 | 72 | # Max. number of IMAP processes (connections) 73 | #process_limit = 1024 74 | 75 | executable = imap postlogin 76 | } 77 | 78 | service pop3 { 79 | # Max. number of POP3 processes (connections) 80 | #process_limit = 1024 81 | 82 | executable = pop3 postlogin 83 | } 84 | 85 | service postlogin { 86 | executable = script-login /usr/local/bin/postlogin.sh 87 | user = %modoboa_user 88 | unix_listener postlogin { 89 | } 90 | } 91 | 92 | service stats { 93 | # To allow modoboa to access available cipher list. 94 | unix_listener stats-reader { 95 | user = %{mailboxes_owner} 96 | group = %{mailboxes_owner} 97 | mode = 0660 98 | } 99 | 100 | unix_listener stats-writer { 101 | user = %{mailboxes_owner} 102 | group = %{mailboxes_owner} 103 | mode = 0660 104 | } 105 | } 106 | 107 | service auth { 108 | # auth_socket_path points to this userdb socket by default. It's typically 109 | # used by dovecot-lda, doveadm, possibly imap process, etc. Users that have 110 | # full permissions to this socket are able to get a list of all usernames and 111 | # get the results of everyone's userdb lookups. 112 | # 113 | # The default 0666 mode allows anyone to connect to the socket, but the 114 | # userdb lookups will succeed only if the userdb returns an "uid" field that 115 | # matches the caller process's UID. Also if caller's uid or gid matches the 116 | # socket's uid or gid the lookup succeeds. Anything else causes a failure. 117 | # 118 | # To give the caller full permissions to lookup all users, set the mode to 119 | # something else than 0666 and Dovecot lets the kernel enforce the 120 | # permissions (e.g. 0777 allows everyone full permissions). 121 | unix_listener auth-userdb { 122 | #mode = 0666 123 | user = %{mailboxes_owner} 124 | #group = 125 | } 126 | 127 | # Postfix smtp-auth 128 | unix_listener /var/spool/postfix/private/auth { 129 | mode = 0666 130 | user = postfix 131 | group = postfix 132 | } 133 | 134 | # Radicale auth 135 | %{radicale_enabled}unix_listener %{radicale_auth_socket_path} { 136 | %{radicale_enabled} mode = 0666 137 | %{radicale_enabled} user = %{radicale_user} 138 | %{radicale_enabled} group = %{radicale_user} 139 | %{radicale_enabled}} 140 | 141 | # Auth process is run as this user. 142 | #user = $default_internal_user 143 | } 144 | 145 | service auth-worker { 146 | # Auth worker process is run as root by default, so that it can access 147 | # /etc/shadow. If this isn't necessary, the user should be changed to 148 | # $default_internal_user. 149 | #user = root 150 | } 151 | 152 | service dict { 153 | # If dict proxy is used, mail processes should have access to its socket. 154 | # For example: mode=0660, group=vmail and global mail_access_groups=vmail 155 | unix_listener dict { 156 | mode = 0600 157 | user = %{mailboxes_owner} 158 | #group = 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/dovecot/conf.d/10-ssl-keys.try.tpl: -------------------------------------------------------------------------------- 1 | # PEM encoded X.509 SSL/TLS certificate and private key. They're opened before 2 | # dropping root privileges, so keep the key file unreadable by anyone but 3 | # root. Included doc/mkcert.sh can be used to easily generate self-signed 4 | # certificate, just make sure to update the domains in dovecot-openssl.cnf 5 | ssl_cert = <%tls_cert_file 6 | ssl_key = <%tls_key_file -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/dovecot/conf.d/10-ssl.conf.tpl: -------------------------------------------------------------------------------- 1 | ## 2 | ## SSL settings 3 | ## 4 | 5 | # SSL/TLS support: yes, no, required. 6 | #ssl = yes 7 | 8 | # Workarround https://github.com/modoboa/modoboa/issues/2570 9 | # We try to load the key and pass if it fails 10 | # Keys require root permissions, standard commands would be blocked 11 | # because dovecot can't load these cert 12 | !include_try /etc/dovecot/conf.d/10-ssl-keys.try 13 | 14 | # If key file is password protected, give the password here. Alternatively 15 | # give it when starting dovecot with -p parameter. Since this file is often 16 | # world-readable, you may want to place this setting instead to a different 17 | # root owned 0600 file by using ssl_key_password = 19 | #service_count = 1 20 | 21 | # Number of processes to always keep waiting for more connections. 22 | #process_min_avail = 0 23 | 24 | # If you set service_count=0, you probably need to grow this. 25 | #vsz_limit = 64M 26 | } 27 | 28 | service managesieve { 29 | # Max. number of ManageSieve processes (connections) 30 | #process_limit = 1024 31 | } 32 | 33 | # Service configuration 34 | 35 | protocol sieve { 36 | # Maximum ManageSieve command line length in bytes. ManageSieve usually does 37 | # not involve overly long command lines, so this setting will not normally 38 | # need adjustment 39 | #managesieve_max_line_length = 65536 40 | 41 | # Maximum number of ManageSieve connections allowed for a user from each IP 42 | # address. 43 | # NOTE: The username is compared case-sensitively. 44 | #mail_max_userip_connections = 10 45 | 46 | # Space separated list of plugins to load (none known to be useful so far). 47 | # Do NOT try to load IMAP plugins here. 48 | #mail_plugins = 49 | 50 | # MANAGESIEVE logout format string: 51 | # %i - total number of bytes read from client 52 | # %o - total number of bytes sent to client 53 | #managesieve_logout_format = bytes=%i/%o 54 | 55 | # To fool ManageSieve clients that are focused on CMU's timesieved you can 56 | # specify the IMPLEMENTATION capability that Dovecot reports to clients. 57 | # For example: 'Cyrus timsieved v2.2.13' 58 | #managesieve_implementation_string = Dovecot Pigeonhole 59 | 60 | # Explicitly specify the SIEVE and NOTIFY capability reported by the server 61 | # before login. If left unassigned these will be reported dynamically 62 | # according to what the Sieve interpreter supports by default (after login 63 | # this may differ depending on the user). 64 | #managesieve_sieve_capability = 65 | #managesieve_notify_capability = 66 | 67 | # The maximum number of compile errors that are returned to the client upon 68 | # script upload or script verification. 69 | #managesieve_max_compile_errors = 5 70 | 71 | # Refer to 90-sieve.conf for script quota configuration and configuration of 72 | # Sieve execution limits. 73 | } 74 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/dovecot/conf.d/90-quota.conf: -------------------------------------------------------------------------------- 1 | ## 2 | ## Quota configuration. 3 | ## 4 | 5 | # Note that you also have to enable quota plugin in mail_plugins setting. 6 | # 7 | 8 | ## 9 | ## Quota limits 10 | ## 11 | 12 | # Quota limits are set using "quota_rule" parameters. To get per-user quota 13 | # limits, you can set/override them by returning "quota_rule" extra field 14 | # from userdb. It's also possible to give mailbox-specific limits, for example 15 | # to give additional 100 MB when saving to Trash: 16 | 17 | plugin { 18 | #quota_rule = *:storage=1G 19 | #quota_rule2 = Trash:storage=+100M 20 | } 21 | 22 | ## 23 | ## Quota warnings 24 | ## 25 | 26 | # You can execute a given command when user exceeds a specified quota limit. 27 | # Each quota root has separate limits. Only the command for the first 28 | # exceeded limit is excecuted, so put the highest limit first. 29 | # The commands are executed via script service by connecting to the named 30 | # UNIX socket (quota-warning below). 31 | # Note that % needs to be escaped as %%, otherwise "% " expands to empty. 32 | 33 | plugin { 34 | #quota_warning = storage=95%% quota-warning 95 %u 35 | #quota_warning2 = storage=80%% quota-warning 80 %u 36 | } 37 | 38 | # Example quota-warning service. The unix listener's permissions should be 39 | # set in a way that mail processes can connect to it. Below example assumes 40 | # that mail processes run as vmail user. If you use mode=0666, all system users 41 | # can generate quota warnings to anyone. 42 | #service quota-warning { 43 | # executable = script /usr/local/bin/quota-warning.sh 44 | # user = dovecot 45 | # unix_listener quota-warning { 46 | # user = vmail 47 | # } 48 | #} 49 | 50 | ## 51 | ## Quota backends 52 | ## 53 | 54 | # Multiple backends are supported: 55 | # dirsize: Find and sum all the files found from mail directory. 56 | # Extremely SLOW with Maildir. It'll eat your CPU and disk I/O. 57 | # dict: Keep quota stored in dictionary (eg. SQL) 58 | # maildir: Maildir++ quota 59 | # fs: Read-only support for filesystem quota 60 | 61 | plugin { 62 | #quota = dirsize:User quota 63 | #quota = maildir:User quota 64 | quota = dict:User quota::proxy::quota 65 | #quota = fs:User quota 66 | } 67 | 68 | # Multiple quota roots are also possible, for example this gives each user 69 | # their own 100MB quota and one shared 1GB quota within the domain: 70 | plugin { 71 | #quota = dict:user::proxy::quota 72 | #quota2 = dict:domain:%d:proxy::quota_domain 73 | #quota_rule = *:storage=102400 74 | #quota2_rule = *:storage=1048576 75 | } 76 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/dovecot/conf.d/90-sieve.conf: -------------------------------------------------------------------------------- 1 | ## 2 | ## Settings for the Sieve interpreter 3 | ## 4 | 5 | # Do not forget to enable the Sieve plugin in 15-lda.conf and 20-lmtp.conf 6 | # by adding it to the respective mail_plugins= settings. 7 | 8 | plugin { 9 | # The path to the user's main active script. If ManageSieve is used, this the 10 | # location of the symbolic link controlled by ManageSieve. 11 | sieve = ~/.dovecot.sieve 12 | 13 | # The default Sieve script when the user has none. This is a path to a global 14 | # sieve script file, which gets executed ONLY if user's private Sieve script 15 | # doesn't exist. Be sure to pre-compile this script manually using the sievec 16 | # command line tool. 17 | # --> See sieve_before fore executing scripts before the user's personal 18 | # script. 19 | #sieve_default = /var/lib/dovecot/sieve/default.sieve 20 | 21 | # Directory for :personal include scripts for the include extension. This 22 | # is also where the ManageSieve service stores the user's scripts. 23 | sieve_dir = ~/sieve 24 | 25 | # Directory for :global include scripts for the include extension. 26 | #sieve_global_dir = 27 | 28 | # Path to a script file or a directory containing script files that need to be 29 | # executed before the user's script. If the path points to a directory, all 30 | # the Sieve scripts contained therein (with the proper .sieve extension) are 31 | # executed. The order of execution within a directory is determined by the 32 | # file names, using a normal 8bit per-character comparison. Multiple script 33 | # file or directory paths can be specified by appending an increasing number. 34 | #sieve_before = 35 | #sieve_before2 = 36 | #sieve_before3 = (etc...) 37 | 38 | # Identical to sieve_before, only the specified scripts are executed after the 39 | # user's script (only when keep is still in effect!). Multiple script file or 40 | # directory paths can be specified by appending an increasing number. 41 | #sieve_after = 42 | #sieve_after2 = 43 | #sieve_after2 = (etc...) 44 | 45 | # Which Sieve language extensions are available to users. By default, all 46 | # supported extensions are available, except for deprecated extensions or 47 | # those that are still under development. Some system administrators may want 48 | # to disable certain Sieve extensions or enable those that are not available 49 | # by default. This setting can use '+' and '-' to specify differences relative 50 | # to the default. For example `sieve_extensions = +imapflags' will enable the 51 | # deprecated imapflags extension in addition to all extensions were already 52 | # enabled by default. 53 | #sieve_extensions = +notify +imapflags 54 | 55 | # Which Sieve language extensions are ONLY available in global scripts. This 56 | # can be used to restrict the use of certain Sieve extensions to administrator 57 | # control, for instance when these extensions can cause security concerns. 58 | # This setting has higher precedence than the `sieve_extensions' setting 59 | # (above), meaning that the extensions enabled with this setting are never 60 | # available to the user's personal script no matter what is specified for the 61 | # `sieve_extensions' setting. The syntax of this setting is similar to the 62 | # `sieve_extensions' setting, with the difference that extensions are 63 | # enabled or disabled for exclusive use in global scripts. Currently, no 64 | # extensions are marked as such by default. 65 | #sieve_global_extensions = 66 | 67 | # The Pigeonhole Sieve interpreter can have plugins of its own. Using this 68 | # setting, the used plugins can be specified. Check the Dovecot wiki 69 | # (wiki2.dovecot.org) or the pigeonhole website 70 | # (http://pigeonhole.dovecot.org) for available plugins. 71 | #sieve_plugins = 72 | 73 | # The separator that is expected between the :user and :detail 74 | # address parts introduced by the subaddress extension. This may 75 | # also be a sequence of characters (e.g. '--'). The current 76 | # implementation looks for the separator from the left of the 77 | # localpart and uses the first one encountered. The :user part is 78 | # left of the separator and the :detail part is right. This setting 79 | # is also used by Dovecot's LMTP service. 80 | #recipient_delimiter = + 81 | 82 | # The maximum size of a Sieve script. The compiler will refuse to compile any 83 | # script larger than this limit. If set to 0, no limit on the script size is 84 | # enforced. 85 | #sieve_max_script_size = 1M 86 | 87 | # The maximum number of actions that can be performed during a single script 88 | # execution. If set to 0, no limit on the total number of actions is enforced. 89 | #sieve_max_actions = 32 90 | 91 | # The maximum number of redirect actions that can be performed during a single 92 | # script execution. If set to 0, no redirect actions are allowed. 93 | #sieve_max_redirects = 4 94 | 95 | # The maximum number of personal Sieve scripts a single user can have. If set 96 | # to 0, no limit on the number of scripts is enforced. 97 | # (Currently only relevant for ManageSieve) 98 | #sieve_quota_max_scripts = 0 99 | 100 | # The maximum amount of disk storage a single user's scripts may occupy. If 101 | # set to 0, no limit on the used amount of disk storage is enforced. 102 | # (Currently only relevant for ManageSieve) 103 | #sieve_quota_max_storage = 0 104 | } 105 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/dovecot/conf.d/auth-oauth2.conf.ext: -------------------------------------------------------------------------------- 1 | passdb { 2 | driver = oauth2 3 | mechanisms = xoauth2 oauthbearer 4 | args = /etc/dovecot/conf.d/dovecot-oauth2.conf.ext 5 | } 6 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/dovecot/conf.d/auth-sql.conf.ext: -------------------------------------------------------------------------------- 1 | # Authentication for SQL users. Included from auth.conf. 2 | # 3 | # 4 | 5 | passdb { 6 | driver = sql 7 | 8 | # Path for SQL configuration file, see example-config/dovecot-sql.conf.ext 9 | args = /etc/dovecot/dovecot-sql.conf.ext 10 | } 11 | 12 | passdb { 13 | driver = sql 14 | args = /etc/dovecot/dovecot-sql-master.conf.ext 15 | master = yes 16 | pass = yes 17 | } 18 | 19 | # "prefetch" user database means that the passdb already provided the 20 | # needed information and there's no need to do a separate userdb lookup. 21 | # 22 | userdb { 23 | driver = prefetch 24 | } 25 | 26 | userdb { 27 | driver = sql 28 | args = /etc/dovecot/dovecot-sql.conf.ext 29 | } 30 | 31 | # If you don't have any user-specific settings, you can avoid the user_query 32 | # by using userdb static instead of userdb sql, for example: 33 | # 34 | #userdb { 35 | #driver = static 36 | #args = uid=vmail gid=vmail home=/var/vmail/%u 37 | #} 38 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/dovecot/conf.d/dovecot-oauth2.conf.ext.tpl: -------------------------------------------------------------------------------- 1 | introspection_mode = post 2 | introspection_url = %{oauth2_introspection_url} 3 | username_attribute = username 4 | tls_ca_cert_file = /etc/ssl/certs/ca-certificates.crt 5 | active_attribute = active 6 | active_value = true 7 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/dovecot/dovecot-dict-sql.conf.ext.tpl: -------------------------------------------------------------------------------- 1 | connect = host=%dbhost port=%dbport dbname=%modoboa_dbname user=%modoboa_dbuser password=%modoboa_dbpassword 2 | 3 | # CREATE TABLE quota ( 4 | # username varchar(100) not null, 5 | # bytes bigint not null default 0, 6 | # messages integer not null default 0, 7 | # primary key (username) 8 | # ); 9 | 10 | map { 11 | pattern = priv/quota/storage 12 | table = admin_quota 13 | username_field = username 14 | value_field = bytes 15 | } 16 | map { 17 | pattern = priv/quota/messages 18 | table = admin_quota 19 | username_field = username 20 | value_field = messages 21 | } 22 | 23 | # CREATE TABLE expires ( 24 | # username varchar(100) not null, 25 | # mailbox varchar(255) not null, 26 | # expire_stamp integer not null, 27 | # primary key (username, mailbox) 28 | # ); 29 | 30 | map { 31 | pattern = shared/expire/$user/$mailbox 32 | table = expires 33 | value_field = expire_stamp 34 | 35 | fields { 36 | username = $user 37 | mailbox = $mailbox 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/dovecot/dovecot-sql-master-mysql.conf.ext.tpl: -------------------------------------------------------------------------------- 1 | # This file is opened as root, so it should be owned by root and mode 0600. 2 | # 3 | # http://wiki2.dovecot.org/AuthDatabase/SQL 4 | # 5 | # For the sql passdb module, you'll need a database with a table that 6 | # contains fields for at least the username and password. If you want to 7 | # use the user@domain syntax, you might want to have a separate domain 8 | # field as well. 9 | # 10 | # If your users all have the same uig/gid, and have predictable home 11 | # directories, you can use the static userdb module to generate the home 12 | # dir based on the username and domain. In this case, you won't need fields 13 | # for home, uid, or gid in the database. 14 | # 15 | # If you prefer to use the sql userdb module, you'll want to add fields 16 | # for home, uid, and gid. Here is an example table: 17 | # 18 | # CREATE TABLE users ( 19 | # username VARCHAR(128) NOT NULL, 20 | # domain VARCHAR(128) NOT NULL, 21 | # password VARCHAR(64) NOT NULL, 22 | # home VARCHAR(255) NOT NULL, 23 | # uid INTEGER NOT NULL, 24 | # gid INTEGER NOT NULL, 25 | # active CHAR(1) DEFAULT 'Y' NOT NULL 26 | # ); 27 | 28 | # Database driver: mysql, pgsql, sqlite 29 | driver = %db_driver 30 | 31 | # Database connection string. This is driver-specific setting. 32 | # 33 | # HA / round-robin load-balancing is supported by giving multiple host 34 | # settings, like: host=sql1.host.org host=sql2.host.org 35 | # 36 | # pgsql: 37 | # For available options, see the PostgreSQL documention for the 38 | # PQconnectdb function of libpq. 39 | # Use maxconns=n (default 5) to change how many connections Dovecot can 40 | # create to pgsql. 41 | # 42 | # mysql: 43 | # Basic options emulate PostgreSQL option names: 44 | # host, port, user, password, dbname 45 | # 46 | # But also adds some new settings: 47 | # client_flags - See MySQL manual 48 | # ssl_ca, ssl_ca_path - Set either one or both to enable SSL 49 | # ssl_cert, ssl_key - For sending client-side certificates to server 50 | # ssl_cipher - Set minimum allowed cipher security (default: HIGH) 51 | # option_file - Read options from the given file instead of 52 | # the default my.cnf location 53 | # option_group - Read options from the given group (default: client) 54 | # 55 | # You can connect to UNIX sockets by using host: host=/var/run/mysql.sock 56 | # Note that currently you can't use spaces in parameters. 57 | # 58 | # sqlite: 59 | # The path to the database file. 60 | # 61 | # Examples: 62 | # connect = host=192.168.1.1 dbname=users 63 | # connect = host=sql.example.com dbname=virtual user=virtual password=blarg 64 | # connect = /etc/dovecot/authdb.sqlite 65 | # 66 | #connect = 67 | connect = host=%dbhost port=%dbport dbname=%modoboa_dbname user=%modoboa_dbuser password=%modoboa_dbpassword 68 | 69 | # Default password scheme. 70 | # 71 | # List of supported schemes is in 72 | # http://wiki2.dovecot.org/Authentication/PasswordSchemes 73 | # 74 | #default_pass_scheme = MD5 75 | 76 | # passdb query to retrieve the password. It can return fields: 77 | # password - The user's password. This field must be returned. 78 | # user - user@domain from the database. Needed with case-insensitive lookups. 79 | # username and domain - An alternative way to represent the "user" field. 80 | # 81 | # The "user" field is often necessary with case-insensitive lookups to avoid 82 | # e.g. "name" and "nAme" logins creating two different mail directories. If 83 | # your user and domain names are in separate fields, you can return "username" 84 | # and "domain" fields instead of "user". 85 | # 86 | # The query can also return other fields which have a special meaning, see 87 | # http://wiki2.dovecot.org/PasswordDatabase/ExtraFields 88 | # 89 | # Commonly used available substitutions (see http://wiki2.dovecot.org/Variables 90 | # for full list): 91 | # %%u = entire user@domain 92 | # %%n = user part of user@domain 93 | # %%d = domain part of user@domain 94 | # 95 | # Note that these can be used only as input to SQL query. If the query outputs 96 | # any of these substitutions, they're not touched. Otherwise it would be 97 | # difficult to have eg. usernames containing '%%' characters. 98 | # 99 | # Example: 100 | # password_query = SELECT userid AS user, pw AS password \ 101 | # FROM users WHERE userid = '%%u' AND active = 'Y' 102 | # 103 | #password_query = \ 104 | # SELECT username, domain, password \ 105 | # FROM users WHERE username = '%%n' AND domain = '%%d' 106 | 107 | # userdb query to retrieve the user information. It can return fields: 108 | # uid - System UID (overrides mail_uid setting) 109 | # gid - System GID (overrides mail_gid setting) 110 | # home - Home directory 111 | # mail - Mail location (overrides mail_location setting) 112 | # 113 | # None of these are strictly required. If you use a single UID and GID, and 114 | # home or mail directory fits to a template string, you could use userdb static 115 | # instead. For a list of all fields that can be returned, see 116 | # http://wiki2.dovecot.org/UserDatabase/ExtraFields 117 | # 118 | # Examples: 119 | # user_query = SELECT home, uid, gid FROM users WHERE userid = '%%u' 120 | # user_query = SELECT dir AS home, user AS uid, group AS gid FROM users where userid = '%%u' 121 | # user_query = SELECT home, 501 AS uid, 501 AS gid FROM users WHERE userid = '%%u' 122 | # 123 | #user_query = \ 124 | # SELECT home, uid, gid \ 125 | # FROM users WHERE username = '%%n' AND domain = '%%d' 126 | 127 | # If you wish to avoid two SQL lookups (passdb + userdb), you can use 128 | # userdb prefetch instead of userdb sql in dovecot.conf. In that case you'll 129 | # also have to return userdb fields in password_query prefixed with "userdb_" 130 | # string. For example: 131 | #password_query = \ 132 | # SELECT userid AS user, password, \ 133 | # home AS userdb_home, uid AS userdb_uid, gid AS userdb_gid \ 134 | # FROM users WHERE userid = '%%u' 135 | password_query = SELECT email AS user, password FROM core_user WHERE email='%%u' and is_active=1 and master_user=1 136 | 137 | # Query to get a list of all usernames. 138 | #iterate_query = SELECT username AS user FROM users 139 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/dovecot/dovecot-sql-master-postgres.conf.ext.tpl: -------------------------------------------------------------------------------- 1 | # This file is opened as root, so it should be owned by root and mode 0600. 2 | # 3 | # http://wiki2.dovecot.org/AuthDatabase/SQL 4 | # 5 | # For the sql passdb module, you'll need a database with a table that 6 | # contains fields for at least the username and password. If you want to 7 | # use the user@domain syntax, you might want to have a separate domain 8 | # field as well. 9 | # 10 | # If your users all have the same uig/gid, and have predictable home 11 | # directories, you can use the static userdb module to generate the home 12 | # dir based on the username and domain. In this case, you won't need fields 13 | # for home, uid, or gid in the database. 14 | # 15 | # If you prefer to use the sql userdb module, you'll want to add fields 16 | # for home, uid, and gid. Here is an example table: 17 | # 18 | # CREATE TABLE users ( 19 | # username VARCHAR(128) NOT NULL, 20 | # domain VARCHAR(128) NOT NULL, 21 | # password VARCHAR(64) NOT NULL, 22 | # home VARCHAR(255) NOT NULL, 23 | # uid INTEGER NOT NULL, 24 | # gid INTEGER NOT NULL, 25 | # active CHAR(1) DEFAULT 'Y' NOT NULL 26 | # ); 27 | 28 | # Database driver: mysql, pgsql, sqlite 29 | driver = %db_driver 30 | 31 | # Database connection string. This is driver-specific setting. 32 | # 33 | # HA / round-robin load-balancing is supported by giving multiple host 34 | # settings, like: host=sql1.host.org host=sql2.host.org 35 | # 36 | # pgsql: 37 | # For available options, see the PostgreSQL documention for the 38 | # PQconnectdb function of libpq. 39 | # Use maxconns=n (default 5) to change how many connections Dovecot can 40 | # create to pgsql. 41 | # 42 | # mysql: 43 | # Basic options emulate PostgreSQL option names: 44 | # host, port, user, password, dbname 45 | # 46 | # But also adds some new settings: 47 | # client_flags - See MySQL manual 48 | # ssl_ca, ssl_ca_path - Set either one or both to enable SSL 49 | # ssl_cert, ssl_key - For sending client-side certificates to server 50 | # ssl_cipher - Set minimum allowed cipher security (default: HIGH) 51 | # option_file - Read options from the given file instead of 52 | # the default my.cnf location 53 | # option_group - Read options from the given group (default: client) 54 | # 55 | # You can connect to UNIX sockets by using host: host=/var/run/mysql.sock 56 | # Note that currently you can't use spaces in parameters. 57 | # 58 | # sqlite: 59 | # The path to the database file. 60 | # 61 | # Examples: 62 | # connect = host=192.168.1.1 dbname=users 63 | # connect = host=sql.example.com dbname=virtual user=virtual password=blarg 64 | # connect = /etc/dovecot/authdb.sqlite 65 | # 66 | #connect = 67 | connect = host=%dbhost port=%dbport dbname=%modoboa_dbname user=%modoboa_dbuser password=%modoboa_dbpassword 68 | 69 | # Default password scheme. 70 | # 71 | # List of supported schemes is in 72 | # http://wiki2.dovecot.org/Authentication/PasswordSchemes 73 | # 74 | #default_pass_scheme = MD5 75 | 76 | # passdb query to retrieve the password. It can return fields: 77 | # password - The user's password. This field must be returned. 78 | # user - user@domain from the database. Needed with case-insensitive lookups. 79 | # username and domain - An alternative way to represent the "user" field. 80 | # 81 | # The "user" field is often necessary with case-insensitive lookups to avoid 82 | # e.g. "name" and "nAme" logins creating two different mail directories. If 83 | # your user and domain names are in separate fields, you can return "username" 84 | # and "domain" fields instead of "user". 85 | # 86 | # The query can also return other fields which have a special meaning, see 87 | # http://wiki2.dovecot.org/PasswordDatabase/ExtraFields 88 | # 89 | # Commonly used available substitutions (see http://wiki2.dovecot.org/Variables 90 | # for full list): 91 | # %%u = entire user@domain 92 | # %%n = user part of user@domain 93 | # %%d = domain part of user@domain 94 | # 95 | # Note that these can be used only as input to SQL query. If the query outputs 96 | # any of these substitutions, they're not touched. Otherwise it would be 97 | # difficult to have eg. usernames containing '%%' characters. 98 | # 99 | # Example: 100 | # password_query = SELECT userid AS user, pw AS password \ 101 | # FROM users WHERE userid = '%%u' AND active = 'Y' 102 | # 103 | #password_query = \ 104 | # SELECT username, domain, password \ 105 | # FROM users WHERE username = '%%n' AND domain = '%%d' 106 | 107 | # userdb query to retrieve the user information. It can return fields: 108 | # uid - System UID (overrides mail_uid setting) 109 | # gid - System GID (overrides mail_gid setting) 110 | # home - Home directory 111 | # mail - Mail location (overrides mail_location setting) 112 | # 113 | # None of these are strictly required. If you use a single UID and GID, and 114 | # home or mail directory fits to a template string, you could use userdb static 115 | # instead. For a list of all fields that can be returned, see 116 | # http://wiki2.dovecot.org/UserDatabase/ExtraFields 117 | # 118 | # Examples: 119 | # user_query = SELECT home, uid, gid FROM users WHERE userid = '%%u' 120 | # user_query = SELECT dir AS home, user AS uid, group AS gid FROM users where userid = '%%u' 121 | # user_query = SELECT home, 501 AS uid, 501 AS gid FROM users WHERE userid = '%%u' 122 | # 123 | #user_query = \ 124 | # SELECT home, uid, gid \ 125 | # FROM users WHERE username = '%%n' AND domain = '%%d' 126 | 127 | # If you wish to avoid two SQL lookups (passdb + userdb), you can use 128 | # userdb prefetch instead of userdb sql in dovecot.conf. In that case you'll 129 | # also have to return userdb fields in password_query prefixed with "userdb_" 130 | # string. For example: 131 | #password_query = \ 132 | # SELECT userid AS user, password, \ 133 | # home AS userdb_home, uid AS userdb_uid, gid AS userdb_gid \ 134 | # FROM users WHERE userid = '%%u' 135 | password_query = SELECT email AS user, password FROM core_user WHERE email='%%u' and is_active and master_user 136 | 137 | # Query to get a list of all usernames. 138 | #iterate_query = SELECT username AS user FROM users 139 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/dovecot/dovecot-sql-mysql.conf.ext.tpl: -------------------------------------------------------------------------------- 1 | # This file is opened as root, so it should be owned by root and mode 0600. 2 | # 3 | # http://wiki2.dovecot.org/AuthDatabase/SQL 4 | # 5 | # For the sql passdb module, you'll need a database with a table that 6 | # contains fields for at least the username and password. If you want to 7 | # use the user@domain syntax, you might want to have a separate domain 8 | # field as well. 9 | # 10 | # If your users all have the same uig/gid, and have predictable home 11 | # directories, you can use the static userdb module to generate the home 12 | # dir based on the username and domain. In this case, you won't need fields 13 | # for home, uid, or gid in the database. 14 | # 15 | # If you prefer to use the sql userdb module, you'll want to add fields 16 | # for home, uid, and gid. Here is an example table: 17 | # 18 | # CREATE TABLE users ( 19 | # username VARCHAR(128) NOT NULL, 20 | # domain VARCHAR(128) NOT NULL, 21 | # password VARCHAR(64) NOT NULL, 22 | # home VARCHAR(255) NOT NULL, 23 | # uid INTEGER NOT NULL, 24 | # gid INTEGER NOT NULL, 25 | # active CHAR(1) DEFAULT 'Y' NOT NULL 26 | # ); 27 | 28 | # Database driver: mysql, pgsql, sqlite 29 | driver = %db_driver 30 | 31 | # Database connection string. This is driver-specific setting. 32 | # 33 | # HA / round-robin load-balancing is supported by giving multiple host 34 | # settings, like: host=sql1.host.org host=sql2.host.org 35 | # 36 | # pgsql: 37 | # For available options, see the PostgreSQL documention for the 38 | # PQconnectdb function of libpq. 39 | # Use maxconns=n (default 5) to change how many connections Dovecot can 40 | # create to pgsql. 41 | # 42 | # mysql: 43 | # Basic options emulate PostgreSQL option names: 44 | # host, port, user, password, dbname 45 | # 46 | # But also adds some new settings: 47 | # client_flags - See MySQL manual 48 | # ssl_ca, ssl_ca_path - Set either one or both to enable SSL 49 | # ssl_cert, ssl_key - For sending client-side certificates to server 50 | # ssl_cipher - Set minimum allowed cipher security (default: HIGH) 51 | # option_file - Read options from the given file instead of 52 | # the default my.cnf location 53 | # option_group - Read options from the given group (default: client) 54 | # 55 | # You can connect to UNIX sockets by using host: host=/var/run/mysql.sock 56 | # Note that currently you can't use spaces in parameters. 57 | # 58 | # sqlite: 59 | # The path to the database file. 60 | # 61 | # Examples: 62 | # connect = host=192.168.1.1 dbname=users 63 | # connect = host=sql.example.com dbname=virtual user=virtual password=blarg 64 | # connect = /etc/dovecot/authdb.sqlite 65 | # 66 | #connect = 67 | connect = host=%dbhost port=%dbport dbname=%modoboa_dbname user=%modoboa_dbuser password=%modoboa_dbpassword 68 | 69 | # Default password scheme. 70 | # 71 | # List of supported schemes is in 72 | # http://wiki2.dovecot.org/Authentication/PasswordSchemes 73 | # 74 | #default_pass_scheme = MD5 75 | 76 | # passdb query to retrieve the password. It can return fields: 77 | # password - The user's password. This field must be returned. 78 | # user - user@domain from the database. Needed with case-insensitive lookups. 79 | # username and domain - An alternative way to represent the "user" field. 80 | # 81 | # The "user" field is often necessary with case-insensitive lookups to avoid 82 | # e.g. "name" and "nAme" logins creating two different mail directories. If 83 | # your user and domain names are in separate fields, you can return "username" 84 | # and "domain" fields instead of "user". 85 | # 86 | # The query can also return other fields which have a special meaning, see 87 | # http://wiki2.dovecot.org/PasswordDatabase/ExtraFields 88 | # 89 | # Commonly used available substitutions (see http://wiki2.dovecot.org/Variables 90 | # for full list): 91 | # %%u = entire user@domain 92 | # %%n = user part of user@domain 93 | # %%d = domain part of user@domain 94 | # 95 | # Note that these can be used only as input to SQL query. If the query outputs 96 | # any of these substitutions, they're not touched. Otherwise it would be 97 | # difficult to have eg. usernames containing '%%' characters. 98 | # 99 | # Example: 100 | # password_query = SELECT userid AS user, pw AS password \ 101 | # FROM users WHERE userid = '%%u' AND active = 'Y' 102 | # 103 | #password_query = \ 104 | # SELECT username, domain, password \ 105 | # FROM users WHERE username = '%%n' AND domain = '%%d' 106 | 107 | # userdb query to retrieve the user information. It can return fields: 108 | # uid - System UID (overrides mail_uid setting) 109 | # gid - System GID (overrides mail_gid setting) 110 | # home - Home directory 111 | # mail - Mail location (overrides mail_location setting) 112 | # 113 | # None of these are strictly required. If you use a single UID and GID, and 114 | # home or mail directory fits to a template string, you could use userdb static 115 | # instead. For a list of all fields that can be returned, see 116 | # http://wiki2.dovecot.org/UserDatabase/ExtraFields 117 | # 118 | # Examples: 119 | # user_query = SELECT home, uid, gid FROM users WHERE userid = '%%u' 120 | # user_query = SELECT dir AS home, user AS uid, group AS gid FROM users where userid = '%%u' 121 | # user_query = SELECT home, 501 AS uid, 501 AS gid FROM users WHERE userid = '%%u' 122 | # 123 | #user_query = \ 124 | # SELECT home, uid, gid \ 125 | # FROM users WHERE username = '%%n' AND domain = '%%d' 126 | %{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' 127 | %{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' 128 | 129 | # If you wish to avoid two SQL lookups (passdb + userdb), you can use 130 | # userdb prefetch instead of userdb sql in dovecot.conf. In that case you'll 131 | # also have to return userdb fields in password_query prefixed with "userdb_" 132 | # string. For example: 133 | #password_query = \ 134 | # SELECT userid AS user, password, \ 135 | # home AS userdb_home, uid AS userdb_uid, gid AS userdb_gid \ 136 | # FROM users WHERE userid = '%%u' 137 | %{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 138 | %{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 139 | 140 | # Query to get a list of all usernames. 141 | #iterate_query = SELECT username AS user FROM users 142 | iterate_query = SELECT email AS user FROM core_user 143 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/dovecot/dovecot-sql-postgres.conf.ext.tpl: -------------------------------------------------------------------------------- 1 | # This file is opened as root, so it should be owned by root and mode 0600. 2 | # 3 | # http://wiki2.dovecot.org/AuthDatabase/SQL 4 | # 5 | # For the sql passdb module, you'll need a database with a table that 6 | # contains fields for at least the username and password. If you want to 7 | # use the user@domain syntax, you might want to have a separate domain 8 | # field as well. 9 | # 10 | # If your users all have the same uig/gid, and have predictable home 11 | # directories, you can use the static userdb module to generate the home 12 | # dir based on the username and domain. In this case, you won't need fields 13 | # for home, uid, or gid in the database. 14 | # 15 | # If you prefer to use the sql userdb module, you'll want to add fields 16 | # for home, uid, and gid. Here is an example table: 17 | # 18 | # CREATE TABLE users ( 19 | # username VARCHAR(128) NOT NULL, 20 | # domain VARCHAR(128) NOT NULL, 21 | # password VARCHAR(64) NOT NULL, 22 | # home VARCHAR(255) NOT NULL, 23 | # uid INTEGER NOT NULL, 24 | # gid INTEGER NOT NULL, 25 | # active CHAR(1) DEFAULT 'Y' NOT NULL 26 | # ); 27 | 28 | # Database driver: mysql, pgsql, sqlite 29 | driver = %db_driver 30 | 31 | # Database connection string. This is driver-specific setting. 32 | # 33 | # HA / round-robin load-balancing is supported by giving multiple host 34 | # settings, like: host=sql1.host.org host=sql2.host.org 35 | # 36 | # pgsql: 37 | # For available options, see the PostgreSQL documention for the 38 | # PQconnectdb function of libpq. 39 | # Use maxconns=n (default 5) to change how many connections Dovecot can 40 | # create to pgsql. 41 | # 42 | # mysql: 43 | # Basic options emulate PostgreSQL option names: 44 | # host, port, user, password, dbname 45 | # 46 | # But also adds some new settings: 47 | # client_flags - See MySQL manual 48 | # ssl_ca, ssl_ca_path - Set either one or both to enable SSL 49 | # ssl_cert, ssl_key - For sending client-side certificates to server 50 | # ssl_cipher - Set minimum allowed cipher security (default: HIGH) 51 | # option_file - Read options from the given file instead of 52 | # the default my.cnf location 53 | # option_group - Read options from the given group (default: client) 54 | # 55 | # You can connect to UNIX sockets by using host: host=/var/run/mysql.sock 56 | # Note that currently you can't use spaces in parameters. 57 | # 58 | # sqlite: 59 | # The path to the database file. 60 | # 61 | # Examples: 62 | # connect = host=192.168.1.1 dbname=users 63 | # connect = host=sql.example.com dbname=virtual user=virtual password=blarg 64 | # connect = /etc/dovecot/authdb.sqlite 65 | # 66 | #connect = 67 | connect = host=%dbhost port=%dbport dbname=%modoboa_dbname user=%modoboa_dbuser password=%modoboa_dbpassword 68 | 69 | # Default password scheme. 70 | # 71 | # List of supported schemes is in 72 | # http://wiki2.dovecot.org/Authentication/PasswordSchemes 73 | # 74 | #default_pass_scheme = MD5 75 | 76 | # passdb query to retrieve the password. It can return fields: 77 | # password - The user's password. This field must be returned. 78 | # user - user@domain from the database. Needed with case-insensitive lookups. 79 | # username and domain - An alternative way to represent the "user" field. 80 | # 81 | # The "user" field is often necessary with case-insensitive lookups to avoid 82 | # e.g. "name" and "nAme" logins creating two different mail directories. If 83 | # your user and domain names are in separate fields, you can return "username" 84 | # and "domain" fields instead of "user". 85 | # 86 | # The query can also return other fields which have a special meaning, see 87 | # http://wiki2.dovecot.org/PasswordDatabase/ExtraFields 88 | # 89 | # Commonly used available substitutions (see http://wiki2.dovecot.org/Variables 90 | # for full list): 91 | # %%u = entire user@domain 92 | # %%n = user part of user@domain 93 | # %%d = domain part of user@domain 94 | # 95 | # Note that these can be used only as input to SQL query. If the query outputs 96 | # any of these substitutions, they're not touched. Otherwise it would be 97 | # difficult to have eg. usernames containing '%%' characters. 98 | # 99 | # Example: 100 | # password_query = SELECT userid AS user, pw AS password \ 101 | # FROM users WHERE userid = '%%u' AND active = 'Y' 102 | # 103 | #password_query = \ 104 | # SELECT username, domain, password \ 105 | # FROM users WHERE username = '%%n' AND domain = '%%d' 106 | 107 | # userdb query to retrieve the user information. It can return fields: 108 | # uid - System UID (overrides mail_uid setting) 109 | # gid - System GID (overrides mail_gid setting) 110 | # home - Home directory 111 | # mail - Mail location (overrides mail_location setting) 112 | # 113 | # None of these are strictly required. If you use a single UID and GID, and 114 | # home or mail directory fits to a template string, you could use userdb static 115 | # instead. For a list of all fields that can be returned, see 116 | # http://wiki2.dovecot.org/UserDatabase/ExtraFields 117 | # 118 | # Examples: 119 | # user_query = SELECT home, uid, gid FROM users WHERE userid = '%%u' 120 | # user_query = SELECT dir AS home, user AS uid, group AS gid FROM users where userid = '%%u' 121 | # user_query = SELECT home, 501 AS uid, 501 AS gid FROM users WHERE userid = '%%u' 122 | # 123 | #user_query = \ 124 | # SELECT home, uid, gid \ 125 | # FROM users WHERE username = '%%n' AND domain = '%%d' 126 | %{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' 127 | %{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' 128 | 129 | # If you wish to avoid two SQL lookups (passdb + userdb), you can use 130 | # userdb prefetch instead of userdb sql in dovecot.conf. In that case you'll 131 | # also have to return userdb fields in password_query prefixed with "userdb_" 132 | # string. For example: 133 | #password_query = \ 134 | # SELECT userid AS user, password, \ 135 | # home AS userdb_home, uid AS userdb_uid, gid AS userdb_gid \ 136 | # FROM users WHERE userid = '%%u' 137 | %{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 138 | %{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 139 | 140 | # Query to get a list of all usernames. 141 | #iterate_query = SELECT username AS user FROM users 142 | iterate_query = SELECT email AS user FROM core_user 143 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/dovecot/dovecot.conf.tpl: -------------------------------------------------------------------------------- 1 | ## Dovecot configuration file 2 | 3 | # If you're in a hurry, see http://wiki2.dovecot.org/QuickConfiguration 4 | 5 | # "doveconf -n" command gives a clean output of the changed settings. Use it 6 | # instead of copy&pasting files when posting to the Dovecot mailing list. 7 | 8 | # '#' character and everything after it is treated as comments. Extra spaces 9 | # and tabs are ignored. If you want to use either of these explicitly, put the 10 | # value inside quotes, eg.: key = "# char and trailing whitespace " 11 | 12 | # Default values are shown for each setting, it's not required to uncomment 13 | # those. These are exceptions to this though: No sections (e.g. namespace {}) 14 | # or plugin settings are added by default, they're listed only as examples. 15 | # Paths are also just examples with the real defaults being based on configure 16 | # options. The paths listed here are for configure --prefix=/usr 17 | # --sysconfdir=/etc --localstatedir=/var 18 | 19 | # Enable installed protocols 20 | !include_try /usr/share/dovecot/protocols.d/*.protocol 21 | %protocols 22 | 23 | # A comma separated list of IPs or hosts where to listen in for connections. 24 | # "*" listens in all IPv4 interfaces, "::" listens in all IPv6 interfaces. 25 | # If you want to specify non-default ports or anything more complex, 26 | # edit conf.d/master.conf. 27 | #listen = *, :: 28 | 29 | # Base directory where to store runtime data. 30 | #base_dir = /var/run/dovecot/ 31 | 32 | # Name of this instance. In multi-instance setup doveadm and other commands 33 | # can use -i to select which instance is used (an alternative 34 | # to -c ). The instance name is also added to Dovecot processes 35 | # in ps output. 36 | #instance_name = dovecot 37 | 38 | # Greeting message for clients. 39 | #login_greeting = Dovecot ready. 40 | 41 | # Space separated list of trusted network ranges. Connections from these 42 | # IPs are allowed to override their IP addresses and ports (for logging and 43 | # for authentication checks). disable_plaintext_auth is also ignored for 44 | # these networks. Typically you'd specify your IMAP proxy servers here. 45 | #login_trusted_networks = 46 | 47 | # Sepace separated list of login access check sockets (e.g. tcpwrap) 48 | #login_access_sockets = 49 | 50 | # With proxy_maybe=yes if proxy destination matches any of these IPs, don't do 51 | # proxying. This isn't necessary normally, but may be useful if the destination 52 | # IP is e.g. a load balancer's IP. 53 | #auth_proxy_self = 54 | 55 | # Show more verbose process titles (in ps). Currently shows user name and 56 | # IP address. Useful for seeing who are actually using the IMAP processes 57 | # (eg. shared mailboxes or if same uid is used for multiple accounts). 58 | #verbose_proctitle = no 59 | 60 | # Should all processes be killed when Dovecot master process shuts down. 61 | # Setting this to "no" means that Dovecot can be upgraded without 62 | # forcing existing client connections to close (although that could also be 63 | # a problem if the upgrade is e.g. because of a security fix). 64 | #shutdown_clients = yes 65 | 66 | # If non-zero, run mail commands via this many connections to doveadm server, 67 | # instead of running them directly in the same process. 68 | #doveadm_worker_count = 0 69 | # UNIX socket or host:port used for connecting to doveadm server 70 | #doveadm_socket_path = doveadm-server 71 | 72 | # Space separated list of environment variables that are preserved on Dovecot 73 | # startup and passed down to all of its child processes. You can also give 74 | # key=value pairs to always set specific settings. 75 | #import_environment = TZ 76 | 77 | ## 78 | ## Dictionary server settings 79 | ## 80 | 81 | # Dictionary can be used to store key=value lists. This is used by several 82 | # plugins. The dictionary can be accessed either directly or though a 83 | # dictionary server. The following dict block maps dictionary names to URIs 84 | # when the server is used. These can then be referenced using URIs in format 85 | # "proxy::". 86 | 87 | dict { 88 | # Enable quota dictionnary 89 | quota = %{db_driver}:/etc/dovecot/dovecot-dict-sql.conf.ext 90 | #expire = sqlite:/etc/dovecot/dovecot-dict-sql.conf.ext 91 | } 92 | 93 | # Most of the actual configuration gets included below. The filenames are 94 | # first sorted by their ASCII value and parsed in that order. The 00-prefixes 95 | # in filenames are intended to make it easier to understand the ordering. 96 | !include conf.d/*.conf 97 | 98 | # A config file can also tried to be included without giving an error if 99 | # it's not found: 100 | !include_try local.conf 101 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/dovecot/fix_modoboa_postgres_schema.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE admin_quota ALTER COLUMN bytes SET DEFAULT 0; 2 | ALTER TABLE admin_quota ALTER COLUMN messages SET DEFAULT 0; 3 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/dovecot/install_modoboa_postgres_trigger.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION merge_quota() RETURNS TRIGGER AS $$ 2 | BEGIN 3 | IF NEW.messages < 0 OR NEW.messages IS NULL THEN 4 | -- ugly kludge: we came here from this function, really do try to insert 5 | IF NEW.messages IS NULL THEN 6 | NEW.messages = 0; 7 | ELSE 8 | NEW.messages = -NEW.messages; 9 | END IF; 10 | return NEW; 11 | END IF; 12 | 13 | LOOP 14 | UPDATE admin_quota SET bytes = bytes + NEW.bytes, 15 | messages = messages + NEW.messages 16 | WHERE username = NEW.username; 17 | IF found THEN 18 | RETURN NULL; 19 | END IF; 20 | 21 | BEGIN 22 | IF NEW.messages = 0 THEN 23 | RETURN NEW; 24 | ELSE 25 | NEW.messages = - NEW.messages; 26 | return NEW; 27 | END IF; 28 | EXCEPTION WHEN unique_violation THEN 29 | -- someone just inserted the record, update it 30 | END; 31 | END LOOP; 32 | END; 33 | $$ LANGUAGE plpgsql; 34 | 35 | DROP TRIGGER IF EXISTS mergequota ON admin_quota; 36 | CREATE TRIGGER mergequota BEFORE INSERT ON admin_quota 37 | FOR EACH ROW EXECUTE PROCEDURE merge_quota(); 38 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/dovecot/postlogin-mysql.sh.tpl: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | DBNAME=%modoboa_dbname DBUSER=%modoboa_dbuser DBPASSWORD=%modoboa_dbpassword 4 | 5 | echo "UPDATE core_user SET last_login=now() WHERE username='$USER'" | mysql -u $DBUSER -p$DBPASSWORD $DBNAME 6 | 7 | exec "$@" 8 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/dovecot/postlogin-postgres.sh.tpl: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | PATH="/usr/bin:/usr/local/bin:/bin" 4 | 5 | psql -c "UPDATE core_user SET last_login=now() WHERE username='$USER'" > /dev/null 6 | 7 | exec "$@" 8 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/fail2ban/filter.d/modoboa-auth.conf.tpl: -------------------------------------------------------------------------------- 1 | # Fail2Ban filter Modoboa authentication 2 | 3 | [INCLUDES] 4 | 5 | before = common.conf 6 | 7 | [Definition] 8 | 9 | failregex = modoboa\.auth: WARNING Failed connection attempt from \'\' as user \'.*?\'$ 10 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/fail2ban/jail.d/modoboa.conf.tpl: -------------------------------------------------------------------------------- 1 | [modoboa] 2 | enabled = true 3 | port = http,https 4 | protocol = tcp 5 | filter = modoboa-auth 6 | maxretry = %max_retry 7 | bantime = %ban_time 8 | findtime = %find_time 9 | logpath = /var/log/auth.log 10 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/modoboa/crontab.tpl: -------------------------------------------------------------------------------- 1 | # 2 | # Modoboa specific cron jobs 3 | # 4 | PYTHON=%{venv_path}/bin/python 5 | INSTANCE=%{instance_path} 6 | 7 | # Operations on mailboxes 8 | %{dovecot_enabled}* * * * * %{dovecot_mailboxes_owner} $PYTHON $INSTANCE/manage.py handle_mailbox_operations 9 | 10 | # Sessions table cleanup 11 | 0 0 * * * root $PYTHON $INSTANCE/manage.py clearsessions 12 | 13 | # Logs table cleanup 14 | 0 0 * * * root $PYTHON $INSTANCE/manage.py cleanlogs 15 | 16 | # Quarantine cleanup 17 | %{amavis_enabled}0 0 * * * root $PYTHON $INSTANCE/manage.py qcleanup 18 | 19 | # Notifications about pending release requests 20 | %{amavis_enabled}0 12 * * * root $PYTHON $INSTANCE/manage.py amnotify 21 | 22 | # Logs parsing 23 | */5 * * * * root $PYTHON $INSTANCE/manage.py logparser &> /dev/null 24 | 0 * * * * root $PYTHON $INSTANCE/manage.py update_statistics 25 | 26 | # Radicale rights file 27 | %{radicale_enabled}*/2 * * * * root $PYTHON $INSTANCE/manage.py generate_rights 28 | 29 | # DNSBL checks 30 | */30 * * * * root $PYTHON $INSTANCE/manage.py modo check_mx 31 | 32 | # Public API communication 33 | %{minutes} %{hours} * * * root $PYTHON $INSTANCE/manage.py communicate_with_public_api 34 | 35 | # Generate DKIM keys (they will belong to the user running this job) 36 | %{dkim_cron_enabled}* * * * * %{opendkim_user} umask 077 && $PYTHON $INSTANCE/manage.py modo manage_dkim_keys 37 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/modoboa/sudoers.tpl: -------------------------------------------------------------------------------- 1 | %{sudo_user} ALL=(%{dovecot_mailboxes_owner}) NOPASSWD: /usr/bin/doveadm 2 | %{opendkim_enabled}%{opendkim_user} ALL=(%{dovecot_mailboxes_owner}) NOPASSWD: /usr/bin/doveadm 3 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/modoboa/supervisor-rq-base.tpl: -------------------------------------------------------------------------------- 1 | [program:modoboa-base-worker] 2 | autostart=true 3 | autorestart=true 4 | command=%{venv_path}/bin/python %{home_dir}/instance/manage.py rqworker modoboa 5 | directory=%{home_dir} 6 | user=%{user} 7 | redirect_stderr=true 8 | numprocs=1 9 | stopsignal=TERM 10 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/modoboa/supervisor-rq-dkim.tpl: -------------------------------------------------------------------------------- 1 | [program:modoboa-dkim-worker] 2 | autostart=true 3 | autorestart=true 4 | command=%{venv_path}/bin/python %{home_dir}/instance/manage.py rqworker dkim 5 | directory=%{home_dir} 6 | user=%{opendkim_user} 7 | redirect_stderr=true 8 | numprocs=1 9 | stopsignal=TERM 10 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/modoboa/supervisor.tpl: -------------------------------------------------------------------------------- 1 | [program:policyd] 2 | autostart=true 3 | autorestart=true 4 | command=%{venv_path}/bin/python %{home_dir}/instance/manage.py policy_daemon 5 | directory=%{home_dir} 6 | redirect_stderr=true 7 | user=%{user} 8 | numprocs=1 9 | 10 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/nginx/automx.conf.tpl: -------------------------------------------------------------------------------- 1 | upstream automx { 2 | server unix:%uwsgi_socket_path fail_timeout=0; 3 | } 4 | 5 | server { 6 | listen 80; 7 | listen [::]:80; 8 | server_name %hostname; 9 | root /srv/automx/instance; 10 | 11 | access_log /var/log/nginx/%{hostname}-access.log; 12 | error_log /var/log/nginx/%{hostname}-error.log; 13 | 14 | location /mail/config-v1.1.xml { 15 | include uwsgi_params; 16 | uwsgi_pass automx; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/nginx/modoboa.conf.tpl: -------------------------------------------------------------------------------- 1 | upstream modoboa { 2 | server unix:%uwsgi_socket_path fail_timeout=0; 3 | } 4 | 5 | server { 6 | listen 80; 7 | listen [::]:80; 8 | server_name %hostname; 9 | rewrite ^ https://$server_name$request_uri? permanent; 10 | } 11 | 12 | server { 13 | listen 443 ssl http2; 14 | listen [::]:443 ssl http2; 15 | server_name %hostname; 16 | root %app_instance_path; 17 | 18 | ssl_certificate %tls_cert_file; 19 | ssl_certificate_key %tls_key_file; 20 | ssl_protocols TLSv1.2 TLSv1.3; 21 | ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384"; 22 | ssl_prefer_server_ciphers on; 23 | ssl_session_cache shared:SSL:10m; 24 | ssl_verify_depth 3; 25 | ssl_dhparam /etc/nginx/dhparam.pem; 26 | 27 | client_max_body_size 10M; 28 | 29 | access_log /var/log/nginx/%{hostname}-access.log; 30 | error_log /var/log/nginx/%{hostname}-error.log; 31 | 32 | location /sitestatic/ { 33 | try_files $uri $uri/ =404; 34 | } 35 | 36 | location /media/ { 37 | try_files $uri $uri/ =404; 38 | } 39 | 40 | location ^~ /new-admin { 41 | alias %{app_instance_path}/frontend/; 42 | index index.html; 43 | 44 | expires -1; 45 | add_header Pragma "no-cache"; 46 | add_header Cache-Control "no-store, no-cache, must-revalidate, post-check=0, pre-check=0"; 47 | 48 | try_files $uri $uri/ /index.html = 404; 49 | } 50 | 51 | location / { 52 | include uwsgi_params; 53 | uwsgi_param UWSGI_SCRIPT instance.wsgi:application; 54 | uwsgi_pass modoboa; 55 | } 56 | %{extra_config} 57 | } 58 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/opendkim/dkim_view_mysql.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE VIEW dkim AS ( 2 | SELECT id, name as domain_name, dkim_private_key_path AS private_key_path, 3 | dkim_key_selector AS selector 4 | FROM admin_domain WHERE enable_dkim=1 5 | ); 6 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/opendkim/dkim_view_postgres.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE VIEW dkim AS ( 2 | SELECT id, name as domain_name, dkim_private_key_path AS private_key_path, 3 | dkim_key_selector AS selector 4 | FROM admin_domain WHERE enable_dkim 5 | ); 6 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/opendkim/opendkim.conf.tpl: -------------------------------------------------------------------------------- 1 | # This is a basic configuration that can easily be adapted to suit a standard 2 | # installation. For more advanced options, see opendkim.conf(5) and/or 3 | # /usr/share/doc/opendkim/examples/opendkim.conf.sample. 4 | 5 | # Log to syslog 6 | Syslog yes 7 | SyslogSuccess Yes 8 | LogWhy Yes 9 | LogResults Yes 10 | 11 | # Required to use local socket with MTAs that access the socket as a non- 12 | # privileged user (e.g. Postfix) 13 | UMask 007 14 | 15 | # Sign for example.com with key in /etc/dkimkeys/dkim.key using 16 | # selector '2007' (e.g. 2007._domainkey.example.com) 17 | #Domain example.com 18 | #KeyFile /etc/dkimkeys/dkim.key 19 | #Selector 2007 20 | 21 | KeyTable dsn:%{db_driver}://%{db_user}:%{db_password}@%{dbport}+%{dbhost}/%{db_name}/table=dkim?keycol=id?datacol=domain_name,selector,private_key_path 22 | SigningTable dsn:%db_driver://%{db_user}:%{db_password}@%{dbport}+%{dbhost}/%{db_name}/table=dkim?keycol=domain_name?datacol=id 23 | 24 | # Commonly-used options; the commented-out versions show the defaults. 25 | #Canonicalization simple 26 | #Mode sv 27 | SubDomains yes 28 | Canonicalization relaxed/relaxed 29 | 30 | # Socket smtp://localhost 31 | # 32 | # ## Socket socketspec 33 | # ## 34 | # ## Names the socket where this filter should listen for milter connections 35 | # ## from the MTA. Required. Should be in one of these forms: 36 | # ## 37 | # ## inet:port@address to listen on a specific interface 38 | # ## inet:port to listen on all interfaces 39 | # ## local:/path/to/socket to listen on a UNIX domain socket 40 | # 41 | Socket inet:%{port}@localhost 42 | #Socket local:/var/run/opendkim/opendkim.sock 43 | 44 | ## PidFile filename 45 | ### default (none) 46 | ### 47 | ### Name of the file where the filter should write its pid before beginning 48 | ### normal operations. 49 | # 50 | PidFile /var/run/opendkim/opendkim.pid 51 | 52 | 53 | # Always oversign From (sign using actual From and a null From to prevent 54 | # malicious signatures header fields (From and/or others) between the signer 55 | # and the verifier. From is oversigned by default in the Debian pacakge 56 | # because it is often the identity key used by reputation systems and thus 57 | # somewhat security sensitive. 58 | OversignHeaders From 59 | 60 | ## ResolverConfiguration filename 61 | ## default (none) 62 | ## 63 | ## Specifies a configuration file to be passed to the Unbound library that 64 | ## performs DNS queries applying the DNSSEC protocol. See the Unbound 65 | ## documentation at http://unbound.net for the expected content of this file. 66 | ## The results of using this and the TrustAnchorFile setting at the same 67 | ## time are undefined. 68 | ## In Debian, /etc/unbound/unbound.conf is shipped as part of the Suggested 69 | ## unbound package 70 | 71 | # ResolverConfiguration /etc/unbound/unbound.conf 72 | 73 | ## TrustAnchorFile filename 74 | ## default (none) 75 | ## 76 | ## Specifies a file from which trust anchor data should be read when doing 77 | ## DNS queries and applying the DNSSEC protocol. See the Unbound documentation 78 | ## at http://unbound.net for the expected format of this file. 79 | 80 | # TrustAnchorFile /usr/share/dns/root.key 81 | 82 | ## Userid userid 83 | ### default (none) 84 | ### 85 | ### Change to user "userid" before starting normal operation? May include 86 | ### a group ID as well, separated from the userid by a colon. 87 | # 88 | UserID %{user} 89 | 90 | ExternalIgnoreList /etc/opendkim.hosts 91 | InternalHosts /etc/opendkim.hosts 92 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/opendkim/opendkim.hosts.tpl: -------------------------------------------------------------------------------- 1 | 127.0.0.1 2 | ::1 3 | localhost 4 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/postfix/main.cf.tpl: -------------------------------------------------------------------------------- 1 | inet_interfaces = all 2 | inet_protocols = all 3 | myhostname = %hostname 4 | myorigin = $myhostname 5 | mydestination = $myhostname 6 | mynetworks = 127.0.0.0/8 [::1]/128 7 | smtpd_banner = $myhostname ESMTP 8 | biff = no 9 | unknown_local_recipient_reject_code = 550 10 | unverified_recipient_reject_code = 550 11 | 12 | # appending .domain is the MUA's job. 13 | append_dot_mydomain = no 14 | 15 | readme_directory = no 16 | 17 | mailbox_size_limit = 0 18 | message_size_limit = %message_size_limit 19 | recipient_delimiter = + 20 | 21 | alias_maps = hash:/etc/aliases 22 | alias_database = hash:/etc/aliases 23 | 24 | ## Proxy maps 25 | proxy_read_maps = 26 | proxy:unix:passwd.byname 27 | proxy:%{db_driver}:/etc/postfix/sql-domains.cf 28 | proxy:%{db_driver}:/etc/postfix/sql-domain-aliases.cf 29 | proxy:%{db_driver}:/etc/postfix/sql-aliases.cf 30 | proxy:%{db_driver}:/etc/postfix/sql-relaydomains.cf 31 | proxy:%{db_driver}:/etc/postfix/sql-maintain.cf 32 | proxy:%{db_driver}:/etc/postfix/sql-relay-recipient-verification.cf 33 | proxy:%{db_driver}:/etc/postfix/sql-sender-login-map.cf 34 | proxy:%{db_driver}:/etc/postfix/sql-spliteddomains-transport.cf 35 | proxy:%{db_driver}:/etc/postfix/sql-transport.cf 36 | 37 | ## TLS settings 38 | # 39 | smtpd_use_tls = yes 40 | smtpd_tls_auth_only = no 41 | smtpd_tls_CApath = /etc/ssl/certs 42 | smtpd_tls_key_file = %tls_key_file 43 | smtpd_tls_cert_file = %tls_cert_file 44 | smtpd_tls_dh1024_param_file = ${config_directory}/ffdhe%{dhe_group}.pem 45 | smtpd_tls_loglevel = 1 46 | smtpd_tls_session_cache_database = btree:$data_directory/smtpd_tls_session_cache 47 | smtpd_tls_security_level = may 48 | smtpd_tls_received_header = yes 49 | 50 | # Disallow SSLv2 and SSLv3, only accept secure ciphers 51 | smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 52 | smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 53 | smtpd_tls_mandatory_ciphers = high 54 | smtpd_tls_mandatory_exclude_ciphers = aNULL, eNULL, EXPORT, DES, RC4, MD5, PSK, aECDH, EDH-DSS-DES-CBC3-SHA, EDH-RSA-DES-CBC3-SHA, KRB5-DES, CBC3-SHA, CAMELLIA, SEED-SHA, AES256-SHA, AES256-SHA256, AES256-GCM-SHA384, AES128-SHA, AES128-SHA256, AES128-GCM-SHA256, DHE-RSA-AES128-GCM-SHA256, DHE-RSA-AES128-SHA, DHE-RSA-AES128-SHA256, DHE-RSA-AES256-GCM-SHA384, DHE-RSA-AES256-SHA, DHE-RSA-AES256-SHA256, DHE-RSA-CHACHA20-POLY1305, ECDHE-RSA-AES128-SHA, ECDHE-RSA-AES256-SHA 55 | smtpd_tls_exclude_ciphers = aNULL, eNULL, EXPORT, DES, RC4, MD5, PSK, aECDH, EDH-DSS-DES-CBC3-SHA, EDH-RSA-DES-CBC3-SHA, KRB5-DES, CBC3-SHA, CAMELLIA, SEED-SHA, AES256-SHA, AES256-SHA256, AES256-GCM-SHA384, AES128-SHA, AES128-SHA256, AES128-GCM-SHA256, DHE-RSA-AES128-GCM-SHA256, DHE-RSA-AES128-SHA, DHE-RSA-AES128-SHA256, DHE-RSA-AES256-GCM-SHA384, DHE-RSA-AES256-SHA, DHE-RSA-AES256-SHA256, DHE-RSA-CHACHA20-POLY1305, ECDHE-RSA-AES128-SHA, ECDHE-RSA-AES256-SHA 56 | tls_preempt_cipherlist = yes 57 | tls_ssl_options = NO_COMPRESSION 58 | 59 | # Enable elliptic curve cryptography 60 | smtpd_tls_eecdh_grade = strong 61 | 62 | # SMTP Smuggling prevention 63 | # See https://www.postfix.org/smtp-smuggling.html 64 | smtpd_data_restrictions = reject_unauth_pipelining 65 | smtpd_forbid_unauth_pipelining = yes 66 | 67 | # Use TLS if this is supported by the remote SMTP server, otherwise use plaintext. 68 | smtp_tls_CApath = /etc/ssl/certs 69 | smtp_tls_security_level = may 70 | smtp_tls_loglevel = 1 71 | smtp_tls_exclude_ciphers = EXPORT, LOW 72 | 73 | ## Virtual transport settings 74 | # 75 | %{dovecot_enabled}virtual_transport = lmtp:unix:private/dovecot-lmtp 76 | 77 | %{dovecot_enabled}virtual_mailbox_domains = proxy:%{db_driver}:/etc/postfix/sql-domains.cf 78 | %{dovecot_enabled}virtual_alias_domains = proxy:%{db_driver}:/etc/postfix/sql-domain-aliases.cf 79 | %{dovecot_enabled}virtual_alias_maps = 80 | %{dovecot_enabled} proxy:%{db_driver}:/etc/postfix/sql-aliases.cf 81 | 82 | ## Relay domains 83 | # 84 | relay_domains = 85 | proxy:%{db_driver}:/etc/postfix/sql-relaydomains.cf 86 | transport_maps = 87 | proxy:%{db_driver}:/etc/postfix/sql-transport.cf 88 | proxy:%{db_driver}:/etc/postfix/sql-spliteddomains-transport.cf 89 | 90 | ## SASL authentication through Dovecot 91 | # 92 | %{dovecot_enabled}smtpd_sasl_type = dovecot 93 | %{dovecot_enabled}smtpd_sasl_path = private/auth 94 | %{dovecot_enabled}smtpd_sasl_auth_enable = yes 95 | %{dovecot_enabled}broken_sasl_auth_clients = yes 96 | %{dovecot_enabled}smtpd_sasl_security_options = noanonymous 97 | 98 | ## SMTP session policies 99 | # 100 | 101 | # We require HELO to check it later 102 | smtpd_helo_required = yes 103 | 104 | # We do not let others find out which recipients are valid 105 | disable_vrfy_command = yes 106 | 107 | # MTA to MTA communication on Port 25. We expect (!) the other party to 108 | # specify messages as required by RFC 821. 109 | strict_rfc821_envelopes = yes 110 | 111 | # Verify cache setup 112 | %{dovecot_enabled}address_verify_map = proxy:btree:$data_directory/verify_cache 113 | 114 | %{dovecot_enabled}proxy_write_maps = 115 | %{dovecot_enabled} $smtp_sasl_auth_cache_name 116 | %{dovecot_enabled} $lmtp_sasl_auth_cache_name 117 | %{dovecot_enabled} $address_verify_map 118 | 119 | # OpenDKIM setup 120 | %{opendkim_enabled}smtpd_milters = inet:127.0.0.1:%{opendkim_port} 121 | %{opendkim_enabled}non_smtpd_milters = inet:127.0.0.1:%{opendkim_port} 122 | %{opendkim_enabled}milter_default_action = accept 123 | %{opendkim_enabled}milter_content_timeout = 30s 124 | 125 | # List of authorized senders 126 | smtpd_sender_login_maps = 127 | proxy:%{db_driver}:/etc/postfix/sql-sender-login-map.cf 128 | 129 | # Recipient restriction rules 130 | smtpd_recipient_restrictions = 131 | check_policy_service inet:127.0.0.1:9999 132 | permit_mynetworks 133 | permit_sasl_authenticated 134 | check_recipient_access 135 | proxy:%{db_driver}:/etc/postfix/sql-maintain.cf 136 | proxy:%{db_driver}:/etc/postfix/sql-relay-recipient-verification.cf 137 | reject_unverified_recipient 138 | reject_unauth_destination 139 | reject_non_fqdn_sender 140 | reject_non_fqdn_recipient 141 | reject_non_fqdn_helo_hostname 142 | 143 | ## Postcreen settings 144 | # 145 | postscreen_access_list = 146 | permit_mynetworks 147 | cidr:/etc/postfix/postscreen_spf_whitelist.cidr 148 | postscreen_blacklist_action = enforce 149 | 150 | # Use some DNSBL 151 | postscreen_dnsbl_sites = 152 | zen.spamhaus.org=127.0.0.[2..11]*3 153 | bl.spameatingmonkey.net=127.0.0.2*2 154 | bl.spamcop.net=127.0.0.2 155 | postscreen_dnsbl_threshold = 3 156 | postscreen_dnsbl_action = enforce 157 | 158 | postscreen_greet_banner = Welcome, please wait... 159 | postscreen_greet_action = enforce 160 | 161 | postscreen_pipelining_enable = yes 162 | postscreen_pipelining_action = enforce 163 | 164 | postscreen_non_smtp_command_enable = yes 165 | postscreen_non_smtp_command_action = enforce 166 | 167 | postscreen_bare_newline_enable = yes 168 | postscreen_bare_newline_action = enforce 169 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/postfix/master.cf.tpl: -------------------------------------------------------------------------------- 1 | # 2 | # Postfix master process configuration file. For details on the format 3 | # of the file, see the master(5) manual page (command: "man 5 master" or 4 | # on-line: http://www.postfix.org/master.5.html). 5 | # 6 | # Do not forget to execute "postfix reload" after editing this file. 7 | # 8 | # ========================================================================== 9 | # service type private unpriv chroot wakeup maxproc command + args 10 | # (yes) (yes) (yes) (never) (100) 11 | # ========================================================================== 12 | smtp inet n - - - 1 postscreen 13 | smtpd pass - - - - - smtpd 14 | %{amavis_enabled} -o smtpd_proxy_filter=inet:[127.0.0.1]:10024 15 | %{amavis_enabled} -o smtpd_proxy_options=speed_adjust 16 | dnsblog unix - - - - 0 dnsblog 17 | 18 | tlsproxy unix - - - - 0 tlsproxy 19 | submission inet n - - - - smtpd 20 | -o syslog_name=postfix/submission 21 | -o smtpd_tls_security_level=encrypt 22 | -o tls_preempt_cipherlist=yes 23 | -o smtpd_sasl_auth_enable=yes 24 | -o smtpd_reject_unlisted_recipient=no 25 | -o smtpd_client_restrictions=permit_sasl_authenticated,reject 26 | -o smtpd_helo_restrictions= 27 | -o smtpd_sender_restrictions=reject_sender_login_mismatch 28 | -o milter_macro_daemon_name=ORIGINATING 29 | %{amavis_enabled} -o smtpd_proxy_filter=inet:[127.0.0.1]:10026 30 | #smtps inet n - - - - smtpd 31 | # -o syslog_name=postfix/smtps 32 | # -o smtpd_tls_wrappermode=yes 33 | # -o smtpd_sasl_auth_enable=yes 34 | # -o smtpd_reject_unlisted_recipient=no 35 | # -o smtpd_client_restrictions=$mua_client_restrictions 36 | # -o smtpd_helo_restrictions=$mua_helo_restrictions 37 | # -o smtpd_sender_restrictions=$mua_sender_restrictions 38 | # -o smtpd_recipient_restrictions= 39 | # -o smtpd_relay_restrictions=permit_sasl_authenticated,reject 40 | # -o milter_macro_daemon_name=ORIGINATING 41 | #628 inet n - - - - qmqpd 42 | pickup unix n - - 60 1 pickup 43 | cleanup unix n - - - 0 cleanup 44 | qmgr unix n - n 300 1 qmgr 45 | #qmgr unix n - n 300 1 oqmgr 46 | tlsmgr unix - - - 1000? 1 tlsmgr 47 | rewrite unix - - - - - trivial-rewrite 48 | bounce unix - - - - 0 bounce 49 | defer unix - - - - 0 bounce 50 | trace unix - - - - 0 bounce 51 | verify unix - - - - 1 verify 52 | flush unix n - - 1000? 0 flush 53 | proxymap unix - - n - - proxymap 54 | proxywrite unix - - n - 1 proxymap 55 | smtp unix - - - - - smtp 56 | relay unix - - - - - smtp 57 | # -o smtp_helo_timeout=5 -o smtp_connect_timeout=5 58 | showq unix n - - - - showq 59 | error unix - - - - - error 60 | retry unix - - - - - error 61 | discard unix - - - - - discard 62 | local unix - n n - - local 63 | virtual unix - n n - - virtual 64 | lmtp unix - - - - - lmtp 65 | anvil unix - - - - 1 anvil 66 | scache unix - - - - 1 scache 67 | # 68 | # ==================================================================== 69 | # Interfaces to non-Postfix software. Be sure to examine the manual 70 | # pages of the non-Postfix software to find out what options it wants. 71 | # 72 | # Many of the following services use the Postfix pipe(8) delivery 73 | # agent. See the pipe(8) man page for information about ${recipient} 74 | # and other message envelope options. 75 | # ==================================================================== 76 | # 77 | # maildrop. See the Postfix MAILDROP_README file for details. 78 | # Also specify in main.cf: maildrop_destination_recipient_limit=1 79 | # 80 | maildrop unix - n n - - pipe 81 | flags=DRhu user=%{dovecot_mailboxes_owner} argv=/usr/bin/maildrop -d ${recipient} 82 | # 83 | # ==================================================================== 84 | # 85 | # Recent Cyrus versions can use the existing "lmtp" master.cf entry. 86 | # 87 | # Specify in cyrus.conf: 88 | # lmtp cmd="lmtpd -a" listen="localhost:lmtp" proto=tcp4 89 | # 90 | # Specify in main.cf one or more of the following: 91 | # mailbox_transport = lmtp:inet:localhost 92 | # virtual_transport = lmtp:inet:localhost 93 | # 94 | # ==================================================================== 95 | # 96 | # Cyrus 2.1.5 (Amos Gouaux) 97 | # Also specify in main.cf: cyrus_destination_recipient_limit=1 98 | # 99 | #cyrus unix - n n - - pipe 100 | # user=cyrus argv=/cyrus/bin/deliver -e -r ${sender} -m ${extension} ${user} 101 | # 102 | # ==================================================================== 103 | # Old example of delivery via Cyrus. 104 | # 105 | #old-cyrus unix - n n - - pipe 106 | # flags=R user=cyrus argv=/cyrus/bin/deliver -e -m ${extension} ${user} 107 | # 108 | # ==================================================================== 109 | # 110 | # See the Postfix UUCP_README file for configuration details. 111 | # 112 | uucp unix - n n - - pipe 113 | flags=Fqhu user=uucp argv=uux -r -n -z -a$sender - $nexthop!rmail ($recipient) 114 | # 115 | # Other external delivery methods. 116 | # 117 | ifmail unix - n n - - pipe 118 | flags=F user=ftn argv=/usr/lib/ifmail/ifmail -r $nexthop ($recipient) 119 | bsmtp unix - n n - - pipe 120 | flags=Fq. user=bsmtp argv=/usr/lib/bsmtp/bsmtp -t$nexthop -f$sender $recipient 121 | scalemail-backend unix - n n - 2 pipe 122 | flags=R user=scalemail argv=/usr/lib/scalemail/bin/scalemail-store ${nexthop} ${user} ${extension} 123 | mailman unix - n n - - pipe 124 | flags=FR user=list argv=/usr/lib/mailman/bin/postfix-to-mailman.py 125 | ${nexthop} ${user} 126 | 127 | # Modoboa autoreply service 128 | # 129 | autoreply unix - n n - - pipe 130 | flags= user=%{dovecot_mailboxes_owner}:%{dovecot_mailboxes_owner} argv=%{modoboa_venv_path}/bin/python %{modoboa_instance_path}/manage.py autoreply $sender $mailbox 131 | 132 | # Amavis return path 133 | # 134 | %{amavis_enabled}127.0.0.1:10025 inet n - n - - smtpd 135 | %{amavis_enabled} -o content_filter= 136 | %{amavis_enabled} -o smtpd_authorized_xforward_hosts=127.0.0.0/8 137 | %{amavis_enabled} -o smtpd_delay_reject=no 138 | %{amavis_enabled} -o smtpd_client_restrictions=permit_mynetworks,reject 139 | %{amavis_enabled} -o smtpd_helo_restrictions= 140 | %{amavis_enabled} -o smtpd_sender_restrictions= 141 | %{amavis_enabled} -o smtpd_recipient_restrictions=permit_mynetworks,reject 142 | %{amavis_enabled} -o smtpd_data_restrictions=reject_unauth_pipelining 143 | %{amavis_enabled} -o smtpd_end_of_data_restrictions= 144 | %{amavis_enabled} -o smtpd_restriction_classes= 145 | %{amavis_enabled} -o mynetworks=127.0.0.0/8 146 | %{amavis_enabled} -o smtpd_error_sleep_time=0 147 | %{amavis_enabled} -o smtpd_soft_error_limit=1001 148 | %{amavis_enabled} -o smtpd_hard_error_limit=1000 149 | %{amavis_enabled} -o smtpd_client_connection_count_limit=0 150 | %{amavis_enabled} -o smtpd_client_connection_rate_limit=0 151 | %{amavis_enabled} -o receive_override_options=no_header_body_checks,no_unknown_recipient_checks 152 | %{amavis_enabled} -o local_header_rewrite_clients=permit_mynetworks,permit_sasl_authenticated 153 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/postwhite/crontab.tpl: -------------------------------------------------------------------------------- 1 | # 2 | # Postwhite specific cron jobs 3 | # 4 | 5 | # Update Postscreen Whitelists 6 | @daily root /usr/local/bin/postwhite/postwhite > /dev/null 2>&1 7 | 8 | # Update Yahoo! IPs for Postscreen Whitelists 9 | @weekly root /usr/local/bin/postwhite/scrape_yahoo > /dev/null 2>&1 10 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/radicale/config.tpl: -------------------------------------------------------------------------------- 1 | # -*- mode: conf -*- 2 | # vim:ft=cfg 3 | 4 | # Config file for Radicale - A simple calendar server 5 | # 6 | # Place it into /etc/radicale/config (global) 7 | # or ~/.config/radicale/config (user) 8 | # 9 | # The current values are the default ones 10 | 11 | 12 | [server] 13 | 14 | # CalDAV server hostnames separated by a comma 15 | # IPv4 syntax: address:port 16 | # IPv6 syntax: [address]:port 17 | # For example: 0.0.0.0:9999, [::]:9999 18 | #hosts = 127.0.0.1:5232 19 | 20 | # Daemon flag 21 | #daemon = False 22 | 23 | # File storing the PID in daemon mode 24 | #pid = 25 | 26 | # Max parallel connections 27 | #max_connections = 20 28 | 29 | # Max size of request body (bytes) 30 | #max_content_length = 10000000 31 | 32 | # Socket timeout (seconds) 33 | #timeout = 10 34 | 35 | # SSL flag, enable HTTPS protocol 36 | #ssl = False 37 | 38 | # SSL certificate path 39 | #certificate = /etc/ssl/radicale.cert.pem 40 | 41 | # SSL private key 42 | #key = /etc/ssl/radicale.key.pem 43 | 44 | # CA certificate for validating clients. This can be used to secure 45 | # TCP traffic between Radicale and a reverse proxy 46 | #certificate_authority = 47 | 48 | # SSL Protocol used. See python's ssl module for available values 49 | #protocol = PROTOCOL_TLSv1_2 50 | 51 | # Available ciphers. See python's ssl module for available ciphers 52 | #ciphers = 53 | 54 | # Reverse DNS to resolve client address in logs 55 | #dns_lookup = True 56 | 57 | # Message displayed in the client when a password is needed 58 | #realm = Radicale - Password Required 59 | 60 | 61 | [encoding] 62 | 63 | # Encoding for responding requests 64 | #request = utf-8 65 | 66 | # Encoding for storing local collections 67 | #stock = utf-8 68 | 69 | 70 | [auth] 71 | 72 | # Authentication method 73 | # Value: none | htpasswd | remote_user | http_x_remote_user 74 | type = dovecot 75 | 76 | # Htpasswd filename 77 | # htpasswd_filename = users 78 | 79 | # Htpasswd encryption method 80 | # Value: plain | sha1 | ssha | crypt | bcrypt | md5 81 | # Only bcrypt can be considered secure. 82 | # bcrypt and md5 require the passlib library to be installed. 83 | # htpasswd_encryption = plain 84 | 85 | # Incorrect authentication delay (seconds) 86 | #delay = 1 87 | 88 | dovecot_socket = %{auth_socket_path} 89 | 90 | 91 | [rights] 92 | 93 | # Rights backend 94 | # Value: none | authenticated | owner_only | owner_write | from_file 95 | type = from_file 96 | 97 | # File for rights management from_file 98 | file = %{config_dir}/rights 99 | 100 | 101 | [storage] 102 | 103 | # Storage backend 104 | # Value: multifilesystem 105 | type = radicale_storage_by_index 106 | radicale_storage_by_index_fields = dtstart, dtend, uid, summary 107 | 108 | # Folder for storing local collections, created if not present 109 | filesystem_folder = %{home_dir}/collections 110 | 111 | # Lock the storage. Never start multiple instances of Radicale or edit the 112 | # storage externally while Radicale is running if disabled. 113 | #filesystem_locking = True 114 | 115 | # Sync all changes to disk during requests. (This can impair performance.) 116 | # Disabling it increases the risk of data loss, when the system crashes or 117 | # power fails! 118 | #filesystem_fsync = True 119 | 120 | # Delete sync token that are older (seconds) 121 | #max_sync_token_age = 2592000 122 | 123 | # Close the lock file when no more clients are waiting. 124 | # This option is not very useful in general, but on Windows files that are 125 | # opened cannot be deleted. 126 | #filesystem_close_lock_file = False 127 | 128 | # Command that is run after changes to storage 129 | # Example: ([ -d .git ] || git init) && git add -A && (git diff --cached --quiet || git commit -m "Changes by "%%(user)s) 130 | #hook = 131 | 132 | 133 | [web] 134 | 135 | # Web interface backend 136 | # Value: none | internal 137 | type = none 138 | 139 | 140 | [logging] 141 | 142 | # Logging configuration file 143 | # If no config is given, simple information is printed on the standard output 144 | # For more information about the syntax of the configuration file, see: 145 | # http://docs.python.org/library/logging.config.html 146 | #config = /etc/radicale/logging 147 | 148 | # Store all environment variables (including those set in the shell) 149 | #full_environment = False 150 | 151 | # Don't include passwords in logs 152 | #mask_passwords = True 153 | 154 | 155 | [headers] 156 | 157 | # Additional HTTP headers 158 | #Access-Control-Allow-Origin = * 159 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/radicale/supervisor.tpl: -------------------------------------------------------------------------------- 1 | [program:radicale] 2 | autostart=true 3 | autorestart=true 4 | command=%{venv_path}/bin/radicale -C %{config_dir}/config 5 | directory=%{home_dir} 6 | redirect_stderr=true 7 | user=%{user} 8 | numprocs=1 9 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/spamassassin/local.cf.tpl: -------------------------------------------------------------------------------- 1 | # This is the right place to customize your installation of SpamAssassin. 2 | # 3 | # See 'perldoc Mail::SpamAssassin::Conf' for details of what can be 4 | # tweaked. 5 | # 6 | # Only a small subset of options are listed below 7 | # 8 | ########################################################################### 9 | 10 | # Add *****SPAM***** to the Subject header of spam e-mails 11 | # 12 | # rewrite_header Subject *****SPAM***** 13 | 14 | 15 | # Save spam messages as a message/rfc822 MIME attachment instead of 16 | # modifying the original message (0: off, 2: use text/plain instead) 17 | # 18 | # report_safe 1 19 | 20 | 21 | # Set which networks or hosts are considered 'trusted' by your mail 22 | # server (i.e. not spammers) 23 | # 24 | # trusted_networks 212.17.35. 25 | 26 | 27 | # Set file-locking method (flock is not safe over NFS, but is faster) 28 | # 29 | # lock_method flock 30 | 31 | 32 | # Set the threshold at which a message is considered spam (default: 5.0) 33 | # 34 | # required_score 5.0 35 | 36 | 37 | # Use Bayesian classifier (default: 1) 38 | # 39 | # use_bayes 1 40 | 41 | 42 | # Bayesian classifier auto-learning (default: 1) 43 | # 44 | # bayes_auto_learn 1 45 | 46 | bayes_store_module %store_module 47 | bayes_sql_dsn %dsn 48 | bayes_sql_username %dbname 49 | bayes_sql_password %dbpassword 50 | 51 | # Set headers which may provide inappropriate cues to the Bayesian 52 | # classifier 53 | # 54 | # bayes_ignore_header X-Bogosity 55 | # bayes_ignore_header X-Spam-Flag 56 | # bayes_ignore_header X-Spam-Status 57 | 58 | 59 | # Some shortcircuiting, if the plugin is enabled 60 | # 61 | ifplugin Mail::SpamAssassin::Plugin::Shortcircuit 62 | # 63 | # default: strongly-whitelisted mails are *really* whitelisted now, if the 64 | # shortcircuiting plugin is active, causing early exit to save CPU load. 65 | # Uncomment to turn this on 66 | # 67 | # shortcircuit USER_IN_WHITELIST on 68 | # shortcircuit USER_IN_DEF_WHITELIST on 69 | # shortcircuit USER_IN_ALL_SPAM_TO on 70 | # shortcircuit SUBJECT_IN_WHITELIST on 71 | 72 | # the opposite; blacklisted mails can also save CPU 73 | # 74 | # shortcircuit USER_IN_BLACKLIST on 75 | # shortcircuit USER_IN_BLACKLIST_TO on 76 | # shortcircuit SUBJECT_IN_BLACKLIST on 77 | 78 | # if you have taken the time to correctly specify your "trusted_networks", 79 | # this is another good way to save CPU 80 | # 81 | # shortcircuit ALL_TRUSTED on 82 | 83 | # and a well-trained bayes DB can save running rules, too 84 | # 85 | # shortcircuit BAYES_99 spam 86 | # shortcircuit BAYES_00 ham 87 | 88 | endif # Mail::SpamAssassin::Plugin::Shortcircuit 89 | 90 | # Razor 91 | use_razor2 1 92 | razor_config /etc/razor/razor-agent.conf 93 | 94 | # Pyzor 95 | use_pyzor 1 96 | pyzor_path /usr/bin/pyzor 97 | 98 | # DCC 99 | %{dcc_enabled}use_dcc 1 100 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/spamassassin/v310.pre.tpl: -------------------------------------------------------------------------------- 1 | # This is the right place to customize your installation of SpamAssassin. 2 | # 3 | # See 'perldoc Mail::SpamAssassin::Conf' for details of what can be 4 | # tweaked. 5 | # 6 | # This file was installed during the installation of SpamAssassin 3.1.0, 7 | # and contains plugin loading commands for the new plugins added in that 8 | # release. It will not be overwritten during future SpamAssassin installs, 9 | # so you can modify it to enable some disabled-by-default plugins below, 10 | # if you so wish. 11 | # 12 | # There are now multiple files read to enable plugins in the 13 | # /etc/mail/spamassassin directory; previously only one, "init.pre" was 14 | # read. Now both "init.pre", "v310.pre", and any other files ending in 15 | # ".pre" will be read. As future releases are made, new plugins will be 16 | # added to new files, named according to the release they're added in. 17 | ########################################################################### 18 | 19 | # DCC - perform DCC message checks. 20 | # 21 | # DCC is disabled here because it is not open source. See the DCC 22 | # license for more details. 23 | # 24 | %{dcc_enabled}loadplugin Mail::SpamAssassin::Plugin::DCC 25 | 26 | # Pyzor - perform Pyzor message checks. 27 | # 28 | loadplugin Mail::SpamAssassin::Plugin::Pyzor 29 | 30 | # Razor2 - perform Razor2 message checks. 31 | # 32 | loadplugin Mail::SpamAssassin::Plugin::Razor2 33 | 34 | # SpamCop - perform SpamCop message reporting 35 | # 36 | loadplugin Mail::SpamAssassin::Plugin::SpamCop 37 | 38 | # AntiVirus - some simple anti-virus checks, this is not a replacement 39 | # for an anti-virus filter like Clam AntiVirus 40 | # 41 | #loadplugin Mail::SpamAssassin::Plugin::AntiVirus 42 | 43 | # AWL - do auto-whitelist checks 44 | # 45 | #loadplugin Mail::SpamAssassin::Plugin::AWL 46 | 47 | # AutoLearnThreshold - threshold-based discriminator for Bayes auto-learning 48 | # 49 | loadplugin Mail::SpamAssassin::Plugin::AutoLearnThreshold 50 | 51 | # TextCat - language guesser 52 | # 53 | #loadplugin Mail::SpamAssassin::Plugin::TextCat 54 | 55 | # AccessDB - lookup from-addresses in access database 56 | # 57 | #loadplugin Mail::SpamAssassin::Plugin::AccessDB 58 | 59 | # WhitelistSubject - Whitelist/Blacklist certain subject regular expressions 60 | # 61 | loadplugin Mail::SpamAssassin::Plugin::WhiteListSubject 62 | 63 | ########################################################################### 64 | # experimental plugins 65 | 66 | # DomainKeys - perform DomainKeys verification 67 | # 68 | # This plugin has been removed as of v3.3.0. Use the DKIM plugin instead, 69 | # which supports both Domain Keys and DKIM. 70 | 71 | # MIMEHeader - apply regexp rules against MIME headers in the message 72 | # 73 | loadplugin Mail::SpamAssassin::Plugin::MIMEHeader 74 | 75 | # ReplaceTags 76 | # 77 | loadplugin Mail::SpamAssassin::Plugin::ReplaceTags 78 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/uwsgi/automx.ini.tpl: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | uid = %app_user 3 | gid = %app_user 4 | plugins = %uwsgi_plugin 5 | home = %app_venv_path 6 | chdir = %app_instance_path 7 | module = automx_wsgi 8 | master = true 9 | vhost = true 10 | harakiri = 60 11 | processes = %nb_processes 12 | socket = %uwsgi_socket_path 13 | chmod-socket = 660 14 | vacuum = true 15 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/files/uwsgi/modoboa.ini.tpl: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | uid = %app_user 3 | gid = %app_user 4 | plugins = %uwsgi_plugin 5 | home = %app_venv_path 6 | chdir = %app_instance_path 7 | module = instance.wsgi:application 8 | master = true 9 | processes = %nb_processes 10 | vhost = true 11 | no-default-app = true 12 | socket = %uwsgi_socket_path 13 | chmod-socket = 660 14 | vacuum = true 15 | single-interpreter = True 16 | buffer-size = 8192 17 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/nginx.py: -------------------------------------------------------------------------------- 1 | """Nginx related tools.""" 2 | 3 | import os 4 | 5 | from .. import package 6 | from .. import system 7 | from .. import utils 8 | 9 | from . import base 10 | from .uwsgi import Uwsgi 11 | 12 | 13 | class Nginx(base.Installer): 14 | """Nginx installer.""" 15 | 16 | appname = "nginx" 17 | packages = { 18 | "deb": ["nginx", "ssl-cert"], 19 | "rpm": ["nginx"] 20 | } 21 | 22 | def get_template_context(self, app): 23 | """Additionnal variables.""" 24 | context = super().get_template_context() 25 | context.update({ 26 | "app_instance_path": ( 27 | self.config.get(app, "instance_path")), 28 | "uwsgi_socket_path": ( 29 | Uwsgi(self.config, self.upgrade, self.restore).get_socket_path(app)) 30 | }) 31 | return context 32 | 33 | def _setup_config(self, app, hostname=None, extra_config=None): 34 | """Custom app configuration.""" 35 | if hostname is None: 36 | hostname = self.config.get("general", "hostname") 37 | context = self.get_template_context(app) 38 | context.update({"hostname": hostname, "extra_config": extra_config}) 39 | src = self.get_file_path("{}.conf.tpl".format(app)) 40 | if package.backend.FORMAT == "deb": 41 | dst = os.path.join( 42 | self.config_dir, "sites-available", "{}.conf".format(hostname)) 43 | utils.copy_from_template(src, dst, context) 44 | link = os.path.join( 45 | self.config_dir, "sites-enabled", os.path.basename(dst)) 46 | if os.path.exists(link): 47 | return 48 | os.symlink(dst, link) 49 | group = self.config.get(app, "user") 50 | user = "www-data" 51 | else: 52 | dst = os.path.join( 53 | self.config_dir, "conf.d", "{}.conf".format(hostname)) 54 | utils.copy_from_template(src, dst, context) 55 | group = "uwsgi" 56 | user = "nginx" 57 | system.add_user_to_group(user, group) 58 | 59 | def post_run(self): 60 | """Additionnal tasks.""" 61 | extra_modoboa_config = "" 62 | if self.config.getboolean("automx", "enabled"): 63 | hostname = "autoconfig.{}".format( 64 | self.config.get("general", "domain")) 65 | self._setup_config("automx", hostname) 66 | extra_modoboa_config = """ 67 | location ~* ^/autodiscover/autodiscover.xml { 68 | include uwsgi_params; 69 | uwsgi_pass automx; 70 | } 71 | location /mobileconfig { 72 | include uwsgi_params; 73 | uwsgi_pass automx; 74 | } 75 | """ 76 | if self.config.get("radicale", "enabled"): 77 | extra_modoboa_config += """ 78 | location /radicale/ { 79 | proxy_pass http://localhost:5232/; # The / is important! 80 | proxy_set_header X-Script-Name /radicale; 81 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 82 | proxy_pass_header Authorization; 83 | } 84 | """ 85 | self._setup_config( 86 | "modoboa", extra_config=extra_modoboa_config) 87 | 88 | if not os.path.exists("{}/dhparam.pem".format(self.config_dir)): 89 | cmd = "openssl dhparam -dsaparam -out dhparam.pem 4096" 90 | utils.exec_cmd(cmd, cwd=self.config_dir) 91 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/opendkim.py: -------------------------------------------------------------------------------- 1 | """OpenDKIM related tools.""" 2 | 3 | import os 4 | import pwd 5 | import shutil 6 | import stat 7 | 8 | from .. import database 9 | from .. import package 10 | from .. import utils 11 | 12 | from . import base 13 | 14 | 15 | class Opendkim(base.Installer): 16 | """OpenDKIM installer.""" 17 | 18 | appname = "opendkim" 19 | packages = { 20 | "deb": ["opendkim"], 21 | "rpm": ["opendkim"] 22 | } 23 | config_files = ["opendkim.conf", "opendkim.hosts"] 24 | 25 | def get_packages(self): 26 | """Additional packages.""" 27 | packages = super(Opendkim, self).get_packages() 28 | if package.backend.FORMAT == "deb": 29 | packages += ["libopendbx1-{}".format(self.db_driver)] 30 | else: 31 | dbengine = "postgresql" if self.dbengine == "postgres" else "mysql" 32 | packages += ["opendbx-{}".format(dbengine)] 33 | return packages 34 | 35 | def install_config_files(self): 36 | """Make sure config directory exists.""" 37 | user = self.config.get("opendkim", "user") 38 | pw = pwd.getpwnam(user) 39 | targets = [ 40 | [self.app_config["keys_storage_dir"], pw[2], pw[3]] 41 | ] 42 | for target in targets: 43 | if not os.path.exists(target[0]): 44 | utils.mkdir( 45 | target[0], 46 | stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | 47 | stat.S_IROTH | stat.S_IXOTH, 48 | target[1], target[2] 49 | ) 50 | super().install_config_files() 51 | 52 | def get_template_context(self): 53 | """Additional variables.""" 54 | context = super(Opendkim, self).get_template_context() 55 | context.update({ 56 | "db_driver": self.db_driver, 57 | "db_name": self.config.get("modoboa", "dbname"), 58 | "db_user": self.app_config["dbuser"], 59 | "db_password": self.app_config["dbpassword"], 60 | "port": self.app_config["port"], 61 | "user": self.app_config["user"] 62 | }) 63 | return context 64 | 65 | def setup_database(self): 66 | """Setup database.""" 67 | self.backend = database.get_backend(self.config) 68 | self.backend.create_user( 69 | self.app_config["dbuser"], self.app_config["dbpassword"] 70 | ) 71 | dbname = self.config.get("modoboa", "dbname") 72 | dbuser = self.config.get("modoboa", "dbuser") 73 | dbpassword = self.config.get("modoboa", "dbpassword") 74 | self.backend.load_sql_file( 75 | dbname, dbuser, dbpassword, 76 | self.get_file_path("dkim_view_{}.sql".format(self.dbengine)) 77 | ) 78 | self.backend.grant_right_on_table( 79 | dbname, "dkim", self.app_config["dbuser"], "SELECT") 80 | 81 | def post_run(self): 82 | """Additional tasks. 83 | Check linux distribution (package deb, rpm), to adapt 84 | to config file location and syntax. 85 | - update opendkim isocket port config 86 | - make sure opendkim starts after db service started 87 | """ 88 | if package.backend.FORMAT == "deb": 89 | params_file = "/etc/default/opendkim" 90 | else: 91 | params_file = "/etc/opendkim.conf" 92 | pattern = r"s/^(SOCKET=.*)/#\1/" 93 | utils.exec_cmd( 94 | "perl -pi -e '{}' {}".format(pattern, params_file)) 95 | with open(params_file, "a") as f: 96 | f.write('\n'.join([ 97 | "", 98 | 'SOCKET="inet:12345@localhost"', 99 | ])) 100 | 101 | # Make sure opendkim is started after postgresql and mysql, 102 | # respectively. 103 | if (self.dbengine != "postgres" and package.backend.FORMAT == "deb"): 104 | dbservice = "mysql.service" 105 | elif (self.dbengine != "postgres" and package.backend.FORMAT != "deb"): 106 | dbservice = "mysqld.service" 107 | else: 108 | dbservice = "postgresql.service" 109 | pattern = ( 110 | "s/^After=(.*)$/After=$1 {}/".format(dbservice)) 111 | utils.exec_cmd( 112 | "perl -pi -e '{}' /lib/systemd/system/opendkim.service".format(pattern)) 113 | 114 | def restore(self): 115 | """Restore keys.""" 116 | dkim_keys_backup = os.path.join( 117 | self.archive_path, "custom/dkim") 118 | keys_storage_dir = self.app_config["keys_storage_dir"] 119 | if os.path.isdir(dkim_keys_backup): 120 | for file in os.listdir(dkim_keys_backup): 121 | file_path = os.path.join(dkim_keys_backup, file) 122 | if os.path.isfile(file_path): 123 | utils.copy_file(file_path, keys_storage_dir) 124 | utils.success("DKIM keys restored from backup") 125 | # Setup permissions 126 | user = self.config.get("opendkim", "user") 127 | utils.exec_cmd(f"chown -R {user}:{user} {keys_storage_dir}") 128 | 129 | def custom_backup(self, path): 130 | """Backup DKIM keys.""" 131 | if os.path.isdir(self.app_config["keys_storage_dir"]): 132 | shutil.copytree(self.app_config["keys_storage_dir"], os.path.join(path, "dkim")) 133 | utils.printcolor( 134 | "DKIM keys saved!", utils.GREEN) 135 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/postfix.py: -------------------------------------------------------------------------------- 1 | """Postfix related tools.""" 2 | 3 | try: 4 | import configparser 5 | except ImportError: 6 | import ConfigParser as configparser 7 | import os 8 | 9 | from .. import package 10 | from .. import utils 11 | 12 | from . import base 13 | from . import backup, install 14 | 15 | 16 | class Postfix(base.Installer): 17 | """Postfix installer.""" 18 | 19 | appname = "postfix" 20 | packages = { 21 | "deb": ["postfix"], 22 | "rpm": ["postfix"], 23 | } 24 | config_files = ["main.cf", "master.cf"] 25 | 26 | def get_packages(self): 27 | """Additional packages.""" 28 | if package.backend.FORMAT == "deb": 29 | packages = ["postfix-{}".format(self.db_driver)] 30 | else: 31 | packages = [] 32 | return super().get_packages() + packages 33 | 34 | def install_packages(self): 35 | """Preconfigure postfix package installation.""" 36 | if "centos" in utils.dist_name(): 37 | config = configparser.ConfigParser() 38 | with open("/etc/yum.repos.d/CentOS-Base.repo") as fp: 39 | config.read_file(fp) 40 | config.set("centosplus", "enabled", "1") 41 | config.set("centosplus", "includepkgs", "postfix-*") 42 | config.set("base", "exclude", "postfix-*") 43 | config.set("updates", "exclude", "postfix-*") 44 | with open("/etc/yum.repos.d/CentOS-Base.repo", "w") as fp: 45 | config.write(fp) 46 | 47 | package.backend.preconfigure( 48 | "postfix", "main_mailer_type", "select", "No configuration") 49 | super().install_packages() 50 | 51 | def get_template_context(self): 52 | """Additional variables.""" 53 | context = super().get_template_context() 54 | context.update({ 55 | "db_driver": self.db_driver, 56 | "dovecot_mailboxes_owner": self.config.get( 57 | "dovecot", "mailboxes_owner"), 58 | "modoboa_venv_path": self.config.get( 59 | "modoboa", "venv_path"), 60 | "modoboa_instance_path": self.config.get( 61 | "modoboa", "instance_path"), 62 | "opendkim_port": self.config.get( 63 | "opendkim", "port") 64 | }) 65 | return context 66 | 67 | def check_dhe_group_file(self): 68 | group = self.config.get(self.appname, "dhe_group") 69 | file_name = f"ffdhe{group}.pem" 70 | if not os.path.exists(f"{self.config_dir}/{file_name}"): 71 | url = f"https://raw.githubusercontent.com/internetstandards/dhe_groups/main/{file_name}" 72 | utils.exec_cmd(f"wget {url}", cwd=self.config_dir) 73 | 74 | def post_run(self): 75 | """Additional tasks.""" 76 | venv_path = self.config.get("modoboa", "venv_path") 77 | python_path = os.path.join(venv_path, "bin", "python") 78 | instance_path = self.config.get("modoboa", "instance_path") 79 | script_path = os.path.join(instance_path, "manage.py") 80 | cmd = ( 81 | "{} {} generate_postfix_maps --destdir {} --force-overwrite" 82 | .format(python_path, script_path, self.config_dir)) 83 | utils.exec_cmd(cmd) 84 | 85 | # Check chroot directory 86 | chroot_dir = "/var/spool/postfix/etc" 87 | chroot_files = ["services", "resolv.conf"] 88 | if not os.path.exists(chroot_dir): 89 | os.mkdir(chroot_dir) 90 | for f in chroot_files: 91 | path = os.path.join(chroot_dir, f) 92 | if not os.path.exists(path): 93 | utils.copy_file(os.path.join("/etc", f), path) 94 | 95 | # Generate DHE group 96 | self.check_dhe_group_file() 97 | 98 | # Generate /etc/aliases.db file to avoid warnings 99 | aliases_file = "/etc/aliases" 100 | if os.path.exists(aliases_file): 101 | utils.exec_cmd("postalias {}".format(aliases_file)) 102 | 103 | # Postwhite 104 | install("postwhite", self.config, self.upgrade, self.archive_path) 105 | 106 | def backup(self, path): 107 | """Launch postwhite backup.""" 108 | backup("postwhite", self.config, path) 109 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/postwhite.py: -------------------------------------------------------------------------------- 1 | """postwhite related functions.""" 2 | 3 | import os 4 | import shutil 5 | 6 | from .. import utils 7 | 8 | from . import base 9 | 10 | POSTWHITE_REPOSITORY = "https://github.com/stevejenkins/postwhite" 11 | SPF_TOOLS_REPOSITORY = "https://github.com/jsarenik/spf-tools" 12 | 13 | 14 | class Postwhite(base.Installer): 15 | """Postwhite installer.""" 16 | 17 | appname = "postwhite" 18 | config_files = [ 19 | "crontab=/etc/cron.d/postwhite", 20 | ] 21 | no_daemon = True 22 | packages = { 23 | "deb": ["bind9-host"], 24 | "rpm": ["bind-utils"] 25 | } 26 | 27 | def install_from_archive(self, repository, target_dir): 28 | """Install from an archive.""" 29 | url = "{}/archive/master.zip".format(repository) 30 | target = os.path.join(target_dir, os.path.basename(url)) 31 | if os.path.exists(target): 32 | os.unlink(target) 33 | utils.exec_cmd("wget {}".format(url), cwd=target_dir) 34 | app_name = os.path.basename(repository) 35 | archive_dir = os.path.join(target_dir, app_name) 36 | if os.path.exists(archive_dir): 37 | shutil.rmtree(archive_dir) 38 | utils.exec_cmd("unzip master.zip", cwd=target_dir) 39 | utils.exec_cmd( 40 | "mv {name}-master {name}".format(name=app_name), cwd=target_dir) 41 | os.unlink(target) 42 | return archive_dir 43 | 44 | def post_run(self): 45 | """Additionnal tasks.""" 46 | install_dir = "/usr/local/bin" 47 | self.install_from_archive(SPF_TOOLS_REPOSITORY, install_dir) 48 | self.postw_dir = self.install_from_archive( 49 | POSTWHITE_REPOSITORY, install_dir) 50 | utils.copy_file( 51 | os.path.join(self.postw_dir, "postwhite.conf"), self.config_dir) 52 | self.postw_bin = os.path.join(self.postw_dir, "postwhite") 53 | utils.exec_cmd("{} /etc/postwhite.conf".format(self.postw_bin)) 54 | 55 | def custom_backup(self, path): 56 | """Backup custom configuration if any.""" 57 | postswhite_custom = "/etc/postwhite.conf" 58 | if os.path.isfile(postswhite_custom): 59 | utils.copy_file(postswhite_custom, path) 60 | utils.printcolor( 61 | "Postwhite configuration saved!", utils.GREEN) 62 | 63 | def restore(self): 64 | """Restore config files.""" 65 | postwhite_backup_configuration = os.path.join( 66 | self.archive_path, "custom/postwhite.conf") 67 | if os.path.isfile(postwhite_backup_configuration): 68 | utils.copy_file(postwhite_backup_configuration, self.config_dir) 69 | utils.success("postwhite.conf restored from backup") 70 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/radicale.py: -------------------------------------------------------------------------------- 1 | """Radicale related tasks.""" 2 | 3 | import os 4 | import shutil 5 | import stat 6 | 7 | from .. import package 8 | from .. import python 9 | from .. import system 10 | from .. import utils 11 | 12 | from . import base 13 | 14 | 15 | class Radicale(base.Installer): 16 | """Radicale installation.""" 17 | 18 | appname = "radicale" 19 | config_files = ["config"] 20 | no_daemon = True 21 | packages = { 22 | "deb": ["supervisor"], 23 | "rpm": ["supervisor"] 24 | } 25 | with_user = True 26 | 27 | def __init__(self, *args, **kwargs): 28 | """Get configuration.""" 29 | super().__init__(*args, **kwargs) 30 | self.venv_path = self.config.get("radicale", "venv_path") 31 | 32 | def _setup_venv(self): 33 | """Prepare a dedicated virtualenv.""" 34 | python.setup_virtualenv(self.venv_path, sudo_user=self.user) 35 | packages = [ 36 | "Radicale", "pytz" 37 | ] 38 | python.install_packages(packages, self.venv_path, sudo_user=self.user) 39 | python.install_package_from_repository( 40 | "radicale-storage-by-index", 41 | "https://github.com/tonioo/RadicaleStorageByIndex", 42 | venv=self.venv_path, sudo_user=self.user) 43 | 44 | def get_template_context(self): 45 | """Additional variables.""" 46 | context = super(Radicale, self).get_template_context() 47 | radicale_auth_socket_path = self.config.get( 48 | "dovecot", "radicale_auth_socket_path") 49 | context.update({ 50 | "auth_socket_path": radicale_auth_socket_path 51 | }) 52 | return context 53 | 54 | def get_config_files(self): 55 | """Return appropriate path.""" 56 | config_files = super(Radicale, self).get_config_files() 57 | if package.backend.FORMAT == "deb": 58 | path = "supervisor=/etc/supervisor/conf.d/radicale.conf" 59 | else: 60 | path = "supervisor=/etc/supervisord.d/radicale.ini" 61 | config_files.append(path) 62 | return config_files 63 | 64 | def install_config_files(self): 65 | """Make sure config directory exists.""" 66 | if not os.path.exists(self.config_dir): 67 | utils.mkdir( 68 | self.config_dir, 69 | stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | 70 | stat.S_IROTH | stat.S_IXOTH, 71 | 0, 0 72 | ) 73 | super().install_config_files() 74 | 75 | def restore(self): 76 | """Restore collections.""" 77 | radicale_backup = os.path.join( 78 | self.archive_path, "custom/radicale") 79 | if os.path.isdir(radicale_backup): 80 | restore_target = os.path.join(self.home_dir, "collections") 81 | if os.path.isdir(restore_target): 82 | shutil.rmtree(restore_target) 83 | shutil.copytree(radicale_backup, restore_target) 84 | utils.success("Radicale collections restored from backup") 85 | 86 | def post_run(self): 87 | """Additional tasks.""" 88 | self._setup_venv() 89 | daemon_name = ( 90 | "supervisor" if package.backend.FORMAT == "deb" else "supervisord" 91 | ) 92 | system.enable_service(daemon_name) 93 | utils.exec_cmd("service {} stop".format(daemon_name)) 94 | utils.exec_cmd("service {} start".format(daemon_name)) 95 | 96 | def custom_backup(self, path): 97 | """Backup collections.""" 98 | radicale_backup = os.path.join(self.config.get( 99 | "radicale", "home_dir", fallback="/srv/radicale"), "collections") 100 | if os.path.isdir(radicale_backup): 101 | shutil.copytree(radicale_backup, os.path.join( 102 | path, "radicale")) 103 | utils.printcolor("Radicale files saved", utils.GREEN) 104 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/razor.py: -------------------------------------------------------------------------------- 1 | """Razor related functions.""" 2 | 3 | import os 4 | import pwd 5 | import stat 6 | 7 | from .. import utils 8 | 9 | from . import base 10 | 11 | 12 | class Razor(base.Installer): 13 | 14 | """Razor installer.""" 15 | 16 | appname = "razor" 17 | no_daemon = True 18 | packages = { 19 | "deb": ["razor"], 20 | "rpm": ["perl-Razor-Agent"] 21 | } 22 | 23 | def post_run(self): 24 | """Additional tasks.""" 25 | user = self.config.get("amavis", "user") 26 | pw = pwd.getpwnam(user) 27 | utils.mkdir( 28 | "/var/log/razor", 29 | stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | 30 | stat.S_IROTH | stat.S_IXOTH, 31 | pw[2], pw[3] 32 | ) 33 | path = os.path.join(pw[5], ".razor") 34 | utils.mkdir(path, stat.S_IRWXU, pw[2], pw[3]) 35 | utils.exec_cmd("razor-admin -home {} -create".format(path)) 36 | utils.mkdir( 37 | self.config_dir, 38 | stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | 39 | stat.S_IROTH | stat.S_IXOTH, 40 | 0, 0 41 | ) 42 | utils.copy_file( 43 | os.path.join(path, "razor-agent.conf"), self.config_dir) 44 | utils.exec_cmd("razor-admin -home {} -discover".format(path), 45 | sudo_user=user, login=False) 46 | utils.exec_cmd("razor-admin -home {} -register".format(path), 47 | sudo_user=user, login=False) 48 | # FIXME: move log file to /var/log ? 49 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/restore.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from .. import utils 4 | 5 | 6 | class Restore: 7 | def __init__(self, restore): 8 | """ 9 | Restoring pre-check (backup integriety) 10 | REQUIRED : modoboa.sql 11 | OPTIONAL : mails/, custom/, amavis.sql, spamassassin.sql 12 | Only checking required 13 | """ 14 | 15 | if not os.path.isdir(restore): 16 | utils.error( 17 | "Provided path is not a directory !") 18 | sys.exit(1) 19 | 20 | modoba_sql_file = os.path.join(restore, "databases/modoboa.sql") 21 | if not os.path.isfile(modoba_sql_file): 22 | utils.error( 23 | modoba_sql_file + " not found, please check your backup") 24 | sys.exit(1) 25 | 26 | # Everything seems alright here, proceeding... 27 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/spamassassin.py: -------------------------------------------------------------------------------- 1 | """Spamassassin related functions.""" 2 | 3 | import os 4 | import pwd 5 | 6 | from .. import package 7 | from .. import utils 8 | 9 | from . import base 10 | from . import install 11 | 12 | 13 | class Spamassassin(base.Installer): 14 | 15 | """SpamAssassin installer.""" 16 | 17 | appname = "spamassassin" 18 | no_daemon = True 19 | packages = { 20 | "deb": ["spamassassin", "pyzor"], 21 | "rpm": ["spamassassin", "pyzor"] 22 | } 23 | with_db = True 24 | config_files = ["v310.pre", "local.cf"] 25 | 26 | def get_sql_schema_path(self): 27 | """Return SQL schema.""" 28 | if self.dbengine == "postgres": 29 | fname = "bayes_pg.sql" 30 | else: 31 | fname = "bayes_mysql.sql" 32 | schema = "/usr/share/doc/spamassassin/sql/{}".format(fname) 33 | if not os.path.exists(schema): 34 | version = package.backend.get_installed_version("spamassassin") 35 | version = version.replace(".", "_") 36 | url = ( 37 | "http://svn.apache.org/repos/asf/spamassassin/tags/" 38 | "spamassassin_release_{}/sql/{}".format(version, fname)) 39 | schema = "/tmp/{}".format(fname) 40 | utils.exec_cmd("wget {} -O {}".format(url, schema)) 41 | return schema 42 | 43 | def get_template_context(self): 44 | """Add additional variables to context.""" 45 | context = super(Spamassassin, self).get_template_context() 46 | if self.dbengine == "postgres": 47 | store_module = "Mail::SpamAssassin::BayesStore::PgSQL" 48 | dsn = "DBI:Pg:dbname={};host={};port={}".format( 49 | self.dbname, self.dbhost, self.dbport) 50 | else: 51 | store_module = "Mail::SpamAssassin::BayesStore::MySQL" 52 | dsn = "DBI:mysql:{}:{}:{}".format( 53 | self.dbname, self.dbhost, self.dbport) 54 | context.update({ 55 | "store_module": store_module, "dsn": dsn, "dcc_enabled": "#"}) 56 | return context 57 | 58 | def post_run(self): 59 | """Additional tasks.""" 60 | install("razor", self.config, self.upgrade, self.restore) 61 | if utils.dist_name() in ["debian", "ubuntu"]: 62 | utils.exec_cmd( 63 | "perl -pi -e 's/^CRON=0/CRON=1/' /etc/cron.daily/spamassassin") 64 | -------------------------------------------------------------------------------- /modoboa_installer/scripts/uwsgi.py: -------------------------------------------------------------------------------- 1 | """uWSGI related tools.""" 2 | 3 | import os 4 | import pwd 5 | import stat 6 | 7 | from .. import package 8 | from .. import system 9 | from .. import utils 10 | 11 | from . import base 12 | 13 | 14 | class Uwsgi(base.Installer): 15 | """uWSGI installer.""" 16 | 17 | appname = "uwsgi" 18 | packages = { 19 | "deb": ["uwsgi", "uwsgi-plugin-python3"], 20 | "rpm": ["uwsgi", "uwsgi-plugin-python36"], 21 | } 22 | 23 | def get_socket_path(self, app): 24 | """Return socket path.""" 25 | if package.backend.FORMAT == "deb": 26 | return "/run/uwsgi/app/{}_instance/socket".format(app) 27 | return "/run/uwsgi/{}_instance.sock".format(app) 28 | 29 | def get_template_context(self, app): 30 | """Additionnal variables.""" 31 | context = super(Uwsgi, self).get_template_context() 32 | if package.backend.FORMAT == "deb": 33 | uwsgi_plugin = "python3" 34 | else: 35 | uwsgi_plugin = "python36" 36 | context.update({ 37 | "app_user": self.config.get(app, "user"), 38 | "app_venv_path": self.config.get(app, "venv_path"), 39 | "app_instance_path": ( 40 | self.config.get(app, "instance_path")), 41 | "uwsgi_socket_path": self.get_socket_path(app), 42 | "uwsgi_plugin": uwsgi_plugin, 43 | }) 44 | return context 45 | 46 | def get_config_dir(self): 47 | """Return appropriate configuration directory.""" 48 | if package.backend.FORMAT == "deb": 49 | return os.path.join(self.config_dir, "apps-available") 50 | return "{}.d".format(self.config_dir) 51 | 52 | def _enable_config_debian(self, dst): 53 | """Enable config file.""" 54 | link = os.path.join( 55 | self.config_dir, "apps-enabled", os.path.basename(dst)) 56 | if os.path.exists(link): 57 | return 58 | os.symlink(dst, link) 59 | 60 | def _setup_config(self, app): 61 | """Common setup code.""" 62 | context = self.get_template_context(app) 63 | src = self.get_file_path("{}.ini.tpl".format(app)) 64 | dst = os.path.join( 65 | self.get_config_dir(), "{}_instance.ini".format(app)) 66 | utils.copy_from_template(src, dst, context) 67 | return dst 68 | 69 | def _setup_modoboa_config(self): 70 | """Custom modoboa configuration.""" 71 | dst = self._setup_config("modoboa") 72 | if package.backend.FORMAT == "deb": 73 | self._enable_config_debian(dst) 74 | else: 75 | system.add_user_to_group( 76 | "uwsgi", self.config.get("modoboa", "user")) 77 | utils.exec_cmd("chmod -R g+w {}/media".format( 78 | self.config.get("modoboa", "instance_path"))) 79 | utils.exec_cmd("chmod -R g+w {}/pdfcredentials".format( 80 | self.config.get("modoboa", "home_dir"))) 81 | pattern = ( 82 | "s/emperor-tyrant = true/emperor-tyrant = false/") 83 | utils.exec_cmd( 84 | "perl -pi -e '{}' /etc/uwsgi.ini".format(pattern)) 85 | 86 | def _setup_automx_config(self): 87 | """Custom automx configuration.""" 88 | dst = self._setup_config("automx") 89 | if package.backend.FORMAT == "deb": 90 | self._enable_config_debian(dst) 91 | else: 92 | system.add_user_to_group( 93 | "uwsgi", self.config.get("automx", "user")) 94 | pattern = ( 95 | "s/emperor-tyrant = true/emperor-tyrant = false/") 96 | utils.exec_cmd( 97 | "perl -pi -e '{}' /etc/uwsgi.ini".format(pattern)) 98 | 99 | def post_run(self): 100 | """Additionnal tasks.""" 101 | self._setup_modoboa_config() 102 | if self.config.getboolean("automx", "enabled"): 103 | self._setup_automx_config() 104 | 105 | def restart_daemon(self): 106 | """Restart daemon process.""" 107 | # Temp. fix for CentOS 108 | if utils.dist_name().startswith("centos"): 109 | pw = pwd.getpwnam("uwsgi") 110 | utils.mkdir( 111 | "/run/uwsgi", 112 | stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | 113 | stat.S_IROTH | stat.S_IXOTH, 114 | pw[2], pw[3] 115 | ) 116 | code, output = utils.exec_cmd("service uwsgi status") 117 | action = "start" if code else "restart" 118 | utils.exec_cmd("service uwsgi {}".format(action)) 119 | system.enable_service(self.get_daemon_name()) 120 | -------------------------------------------------------------------------------- /modoboa_installer/ssl.py: -------------------------------------------------------------------------------- 1 | """SSL tools.""" 2 | 3 | import os 4 | import sys 5 | 6 | from . import package 7 | from . import utils 8 | 9 | 10 | class CertificateBackend: 11 | """Base class.""" 12 | 13 | def __init__(self, config): 14 | """Set path to certificates.""" 15 | self.config = config 16 | 17 | def overwrite_existing_certificate(self): 18 | """Check if certificate already exists.""" 19 | if os.path.exists(self.config.get("general", "tls_key_file")): 20 | if not self.config.getboolean("general", "force"): 21 | answer = utils.user_input( 22 | "Overwrite the existing SSL certificate? (y/N) ") 23 | if not answer.lower().startswith("y"): 24 | return False 25 | return True 26 | 27 | def generate_cert(self): 28 | """Create a certificate.""" 29 | pass 30 | 31 | 32 | class ManualCertificate(CertificateBackend): 33 | """Use certificate provided.""" 34 | 35 | def __init__(self, *args, **kwargs): 36 | super().__init__(*args, **kwargs) 37 | path_correct = True 38 | self.tls_cert_file_path = self.config.get("certificate", 39 | "tls_cert_file_path") 40 | self.tls_key_file_path = self.config.get("certificate", 41 | "tls_key_file_path") 42 | 43 | if not os.path.exists(self.tls_key_file_path): 44 | utils.error("'tls_key_file_path' path is not accessible") 45 | path_correct = False 46 | if not os.path.exists(self.tls_cert_file_path): 47 | utils.error("'tls_cert_file_path' path is not accessible") 48 | path_correct = False 49 | 50 | if not path_correct: 51 | sys.exit(1) 52 | 53 | self.config.set("general", "tls_key_file", 54 | self.tls_key_file_path) 55 | self.config.set("general", "tls_cert_file", 56 | self.tls_cert_file_path) 57 | 58 | 59 | class SelfSignedCertificate(CertificateBackend): 60 | """Create a self signed certificate.""" 61 | 62 | def __init__(self, *args, **kwargs): 63 | """Sanity checks.""" 64 | super().__init__(*args, **kwargs) 65 | if self.config.has_option("general", "tls_key_file"): 66 | # Compatibility 67 | return 68 | for base_dir in ["/etc/pki/tls", "/etc/ssl"]: 69 | if os.path.exists(base_dir): 70 | self.config.set( 71 | "general", "tls_key_file", 72 | "{}/private/%(hostname)s.key".format(base_dir)) 73 | self.config.set( 74 | "general", "tls_cert_file", 75 | "{}/certs/%(hostname)s.cert".format(base_dir)) 76 | return 77 | raise RuntimeError("Cannot find a directory to store certificate") 78 | 79 | def generate_cert(self): 80 | """Create a certificate.""" 81 | if not self.overwrite_existing_certificate(): 82 | return 83 | utils.printcolor( 84 | "Generating new self-signed certificate", utils.YELLOW) 85 | utils.exec_cmd( 86 | "openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 " 87 | "-subj '/CN={}' -keyout {} -out {}".format( 88 | self.config.get("general", "hostname"), 89 | self.config.get("general", "tls_key_file"), 90 | self.config.get("general", "tls_cert_file")) 91 | ) 92 | 93 | 94 | class LetsEncryptCertificate(CertificateBackend): 95 | """Create a certificate using letsencrypt.""" 96 | 97 | def __init__(self, *args, **kwargs): 98 | """Update config.""" 99 | super().__init__(*args, **kwargs) 100 | self.hostname = self.config.get("general", "hostname") 101 | self.config.set("general", "tls_cert_file", ( 102 | "/etc/letsencrypt/live/{}/fullchain.pem".format(self.hostname))) 103 | self.config.set("general", "tls_key_file", ( 104 | "/etc/letsencrypt/live/{}/privkey.pem".format(self.hostname))) 105 | 106 | def install_certbot(self): 107 | """Install certbot script to generate cert.""" 108 | name, version = utils.dist_info() 109 | name = name.lower() 110 | if name == "ubuntu": 111 | package.backend.update() 112 | package.backend.install("software-properties-common") 113 | utils.exec_cmd("add-apt-repository -y universe") 114 | if version == "18.04": 115 | utils.exec_cmd("add-apt-repository -y ppa:certbot/certbot") 116 | package.backend.update() 117 | package.backend.install("certbot") 118 | elif name.startswith("debian"): 119 | package.backend.update() 120 | package.backend.install("certbot") 121 | elif "centos" in name: 122 | package.backend.install("certbot") 123 | else: 124 | utils.printcolor("Failed to install certbot, aborting.") 125 | sys.exit(1) 126 | # Nginx plugin certbot 127 | if ( 128 | self.config.has_option("nginx", "enabled") and 129 | self.config.getboolean("nginx", "enabled") 130 | ): 131 | if name == "ubuntu" or name.startswith("debian"): 132 | package.backend.install("python3-certbot-nginx") 133 | 134 | def generate_cert(self): 135 | """Create a certificate.""" 136 | utils.printcolor( 137 | "Generating new certificate using letsencrypt", utils.YELLOW) 138 | self.install_certbot() 139 | utils.exec_cmd( 140 | "certbot certonly -n --standalone -d {} -m {} --agree-tos" 141 | .format( 142 | self.hostname, self.config.get("letsencrypt", "email"))) 143 | with open("/etc/cron.d/letsencrypt", "w") as fp: 144 | fp.write("0 */12 * * * root certbot renew " 145 | "--quiet\n") 146 | cfg_file = "/etc/letsencrypt/renewal/{}.conf".format(self.hostname) 147 | pattern = "s/authenticator = standalone/authenticator = nginx/" 148 | utils.exec_cmd("perl -pi -e '{}' {}".format(pattern, cfg_file)) 149 | with open("/etc/letsencrypt/renewal-hooks/deploy/reload-services.sh", "w") as fp: 150 | fp.write(f"""#!/bin/bash 151 | 152 | HOSTNAME=$(basename $RENEWED_LINEAGE) 153 | 154 | if [ "$HOSTNAME" = "{self.hostname}" ] 155 | then 156 | systemctl reload dovecot 157 | systemctl reload postfix 158 | fi 159 | """) 160 | 161 | 162 | def get_backend(config): 163 | """Return the appropriate backend.""" 164 | cert_type = config.get("certificate", "type") 165 | if cert_type == "letsencrypt": 166 | return LetsEncryptCertificate(config) 167 | if cert_type == "manual": 168 | return ManualCertificate(config) 169 | return SelfSignedCertificate(config) 170 | -------------------------------------------------------------------------------- /modoboa_installer/system.py: -------------------------------------------------------------------------------- 1 | """System related functions.""" 2 | 3 | import grp 4 | import pwd 5 | import sys 6 | 7 | from . import utils 8 | 9 | 10 | def create_user(name, home=None): 11 | """Create a new system user.""" 12 | try: 13 | pwd.getpwnam(name) 14 | except KeyError: 15 | pass 16 | else: 17 | extra_message = "." 18 | if home: 19 | extra_message = ( 20 | " but please make sure the {} directory exists.".format( 21 | home)) 22 | utils.printcolor( 23 | "User {} already exists, skipping creation{}".format( 24 | name, extra_message), utils.YELLOW) 25 | return 26 | cmd = "useradd -m " 27 | if home: 28 | cmd += "-d {} ".format(home) 29 | utils.exec_cmd("{} {}".format(cmd, name)) 30 | if home: 31 | utils.exec_cmd("chmod 755 {}".format(home)) 32 | 33 | 34 | def add_user_to_group(user, group): 35 | """Add system user to group.""" 36 | try: 37 | pwd.getpwnam(user) 38 | except KeyError: 39 | print("User {} does not exist".format(user)) 40 | sys.exit(1) 41 | try: 42 | grp.getgrnam(group) 43 | except KeyError: 44 | print("Group {} does not exist".format(group)) 45 | sys.exit(1) 46 | utils.exec_cmd("usermod -a -G {} {}".format(group, user)) 47 | 48 | 49 | def enable_service(name): 50 | """Enable a service at startup.""" 51 | utils.exec_cmd("systemctl enable {}".format(name)) 52 | 53 | 54 | def enable_and_start_service(name): 55 | """Enable a start a service.""" 56 | enable_service(name) 57 | code, output = utils.exec_cmd("service {} status".format(name)) 58 | action = "start" if code else "restart" 59 | utils.exec_cmd("service {} {}".format(name, action)) 60 | 61 | 62 | def restart_service(name): 63 | """Restart a service.""" 64 | utils.exec_cmd("service {} restart".format(name)) 65 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | codecov 2 | mock 3 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | """Installer unit tests.""" 2 | 3 | import os 4 | import shutil 5 | import sys 6 | import tempfile 7 | import unittest 8 | 9 | from io import StringIO 10 | from pathlib import Path 11 | 12 | try: 13 | import configparser 14 | except ImportError: 15 | import ConfigParser as configparser 16 | try: 17 | from unittest.mock import patch 18 | except ImportError: 19 | from mock import patch 20 | 21 | import run 22 | 23 | 24 | class ConfigFileTestCase(unittest.TestCase): 25 | """Test configuration file generation.""" 26 | 27 | def setUp(self): 28 | """Create temp dir.""" 29 | self.workdir = tempfile.mkdtemp() 30 | self.cfgfile = os.path.join(self.workdir, "installer.cfg") 31 | 32 | def tearDown(self): 33 | """Delete temp dir.""" 34 | shutil.rmtree(self.workdir) 35 | 36 | def test_configfile_generation(self): 37 | """Check simple case.""" 38 | out = StringIO() 39 | sys.stdout = out 40 | run.main([ 41 | "--stop-after-configfile-check", 42 | "--configfile", self.cfgfile, 43 | "example.test"]) 44 | self.assertTrue(os.path.exists(self.cfgfile)) 45 | 46 | @patch("modoboa_installer.utils.user_input") 47 | def test_interactive_mode(self, mock_user_input): 48 | """Check interactive mode.""" 49 | mock_user_input.side_effect = [ 50 | "0", "0", "", "", "", "", "" 51 | ] 52 | with open(os.devnull, "w") as fp: 53 | sys.stdout = fp 54 | run.main([ 55 | "--stop-after-configfile-check", 56 | "--configfile", self.cfgfile, 57 | "--interactive", 58 | "example.test"]) 59 | self.assertTrue(os.path.exists(self.cfgfile)) 60 | config = configparser.ConfigParser() 61 | config.read(self.cfgfile) 62 | self.assertEqual(config.get("certificate", "type"), "self-signed") 63 | self.assertEqual(config.get("database", "engine"), "postgres") 64 | 65 | @patch("modoboa_installer.utils.user_input") 66 | def test_updating_configfile(self, mock_user_input): 67 | """Check configfile update mechanism.""" 68 | cfgfile_temp = os.path.join(self.workdir, "installer_old.cfg") 69 | 70 | out = StringIO() 71 | sys.stdout = out 72 | run.main([ 73 | "--stop-after-configfile-check", 74 | "--configfile", cfgfile_temp, 75 | "example.test"]) 76 | self.assertTrue(os.path.exists(cfgfile_temp)) 77 | 78 | # Adding a dummy section 79 | with open(cfgfile_temp, "a") as fp: 80 | fp.write( 81 | """ 82 | [dummy] 83 | weird_old_option = "hey 84 | """) 85 | mock_user_input.side_effect = ["y"] 86 | out = StringIO() 87 | sys.stdout = out 88 | run.main([ 89 | "--stop-after-configfile-check", 90 | "--configfile", cfgfile_temp, 91 | "example.test"]) 92 | self.assertIn("dummy", out.getvalue()) 93 | self.assertTrue(Path(self.workdir).glob("*.old")) 94 | self.assertIn("Update complete", 95 | out.getvalue() 96 | ) 97 | 98 | @patch("modoboa_installer.utils.user_input") 99 | def test_interactive_mode_letsencrypt(self, mock_user_input): 100 | """Check interactive mode.""" 101 | mock_user_input.side_effect = [ 102 | "1", "admin@example.test", "0", "", "", "", "", "" 103 | ] 104 | with open(os.devnull, "w") as fp: 105 | sys.stdout = fp 106 | run.main([ 107 | "--stop-after-configfile-check", 108 | "--configfile", self.cfgfile, 109 | "--interactive", 110 | "example.test"]) 111 | self.assertTrue(os.path.exists(self.cfgfile)) 112 | config = configparser.ConfigParser() 113 | config.read(self.cfgfile) 114 | self.assertEqual(config.get("certificate", "type"), "letsencrypt") 115 | self.assertEqual( 116 | config.get("letsencrypt", "email"), "admin@example.test") 117 | 118 | @patch("modoboa_installer.utils.user_input") 119 | def test_configfile_loading(self, mock_user_input): 120 | """Check interactive mode.""" 121 | mock_user_input.side_effect = ["no"] 122 | out = StringIO() 123 | sys.stdout = out 124 | run.main([ 125 | "--configfile", self.cfgfile, 126 | "example.test"]) 127 | self.assertTrue(os.path.exists(self.cfgfile)) 128 | self.assertIn( 129 | "modoboa automx amavis clamav dovecot nginx razor postfix" 130 | " postwhite spamassassin uwsgi", 131 | out.getvalue() 132 | ) 133 | self.assertNotIn("It seems that your config file is outdated.", 134 | out.getvalue() 135 | ) 136 | 137 | @patch("modoboa_installer.utils.user_input") 138 | def test_upgrade_mode(self, mock_user_input): 139 | """Test upgrade mode launch.""" 140 | mock_user_input.side_effect = ["no"] 141 | # 1. Generate a config file 142 | with open(os.devnull, "w") as fp: 143 | sys.stdout = fp 144 | run.main([ 145 | "--stop-after-configfile-check", 146 | "--configfile", self.cfgfile, 147 | "example.test"]) 148 | # 2. Run upgrade 149 | out = StringIO() 150 | sys.stdout = out 151 | run.main([ 152 | "--configfile", self.cfgfile, 153 | "--upgrade", 154 | "example.test"]) 155 | self.assertIn( 156 | "Your mail server is about to be upgraded and the following " 157 | "components will be impacted:", 158 | out.getvalue() 159 | ) 160 | 161 | def test_upgrade_no_config_file(self): 162 | """Check config file existence check.""" 163 | out = StringIO() 164 | sys.stdout = out 165 | with self.assertRaises(SystemExit): 166 | run.main([ 167 | "--configfile", self.cfgfile, 168 | "--upgrade", 169 | "example.test" 170 | ]) 171 | self.assertIn( 172 | "You cannot upgrade an existing installation without a " 173 | "configuration file.", out.getvalue() 174 | ) 175 | 176 | 177 | if __name__ == "__main__": 178 | unittest.main() 179 | -------------------------------------------------------------------------------- /version.txt: -------------------------------------------------------------------------------- 1 | 0bc3a8367cd550aeea99520ca70bc00a5d5ae7c5 --------------------------------------------------------------------------------