Compare commits

...

5 Commits

Author SHA1 Message Date
Priit Jõerüüt
7424564074
Translated using Weblate (Estonian)
Currently translated at 1.3% (25 of 1854 strings)
2025-07-20 20:01:57 +02:00
109247019824
2cf88e5f53
Translated using Weblate (Bulgarian)
Currently translated at 55.5% (1029 of 1854 strings)
2025-07-20 20:01:55 +02:00
Sunil Mohan Adapa
38b3962bbc
email: Start servers during re-setup if they are not running
- This helps during distribution upgrade from dovecot 2.3 to 2.4. Dovecot will
stop running due to dovecot server 2.4 not understanding version 2.3
configuration files. When setup is re-run, starting the daemons again is the
right thing to do.

Tests:

- With email app installed, upgrade from bookworm to trixie. Dovecot is stopped
during distribution upgrade but after freedombox service runs, it recovers and
starts running again.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
2025-07-20 07:54:09 -07:00
Benedek Nagy
271603a435
email: Add support for Dovecot 2.4
Sunil:

- When dovecot package is upgrade from 2.3 to 2.4 during distribution upgrade,
automatically re-run setup.

- Upgrade existing setups to new scheme by re-running setup with incremented app
version.

- Don't query dovecot version during app initialization. Instead overwrite the
DropinConfigs component to query dovecot version during setup and enable
operations.

- Use apt.Cache() to retrieve the installed version of dovecot package. Use
plinth.utils.Version to parse the version and perform a comparison.

- Split even configuration files that have not changed for simplicity.

- Add/update links in Dovecot configuration files.

Tests:

- Install email app on a testing container. Ensure that all files in
/etc/dovecot/conf.d/ are linked properly to 2.4 versions. TLS configuration is
accurate. Use Sogo to test login and sending mails.

  - User with LDAP account and correct password is able to login.

  - User without LDAP account or incorrect password is unable to login.

  - Send mail with Sogo to another account on the server. Notice that mails are
  stored in /var/mail/{user}/mail/ with mail:mail ownership in mbox format.

  - Logging in with email such as user@example.com works. Capital letters are
  allowed.

  - "Archive", "Drafts", "Sent", "Junk", "Trash" folders are automatically
  created and are marked with special flags. Creating additional folders such
  as "Sent Items" also results in them having special flags.

  - Thunderbird is able to connect via SSL with a self-signed certificate
  exception.

  - When an example spam message is sent, it is automatically moved to "Junk"
  folder after getting marked by rspamd.

  - When a message is moved to Junk folder, it is learned as spam by rspamd as
  seen in its admin console.

  - When a message is moved out of Junk folder (to other than "Trash" folder),
  it is learned as not-spam by rspamd as seen in its admin console.

- Install email app on a stable container with patches. Ensure that all files in
/etc/dovecot/conf.d/ are linked properly to 2.3 versions. TLS configuration is
accurate. Use Sogo to test login and sending mails.

- Install email app on a stable container without patches. Apply patches. Ensure
that all files in /etc/dovecot/conf.d/ are linked properly to 2.3 versions. TLS
configuration is accurate for dovecot 2.3. Use Sogo to test login and sending
mails. Perform distribution upgrade to testing. Ensure that all files in
/etc/dovecot/conf.d/ are linked properly to 2.3 versions. TLS configuration is
accurate for dovecot 2.4. Use Sogo to test login and sending mails.

Signed-off-by: Benedek Nagy <contact@nbenedek.me>
Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: Sunil Mohan Adapa <sunil@medhas.org>
2025-07-20 07:54:05 -07:00
Sunil Mohan Adapa
cc0a02ad1c
config: Allow overriding target path in dropin config component
- To be used when configuration has to change based on the package version.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
2025-07-20 07:54:02 -07:00
31 changed files with 376 additions and 99 deletions

View File

@ -108,14 +108,12 @@ class DropinConfigs(app_module.FollowerComponent):
return results
@staticmethod
def get_target_path(path):
def get_target_path(self, path):
"""Return Path object for a target path."""
target = pathlib.Path(DropinConfigs.ROOT)
target /= DropinConfigs.DROPIN_CONFIG_ROOT.lstrip('/')
target = pathlib.Path(self.ROOT)
target /= self.DROPIN_CONFIG_ROOT.lstrip('/')
return target / path.lstrip('/')
@staticmethod
def get_etc_path(path):
def get_etc_path(self, path):
"""Return Path object for etc path."""
return pathlib.Path(DropinConfigs.ROOT) / path.lstrip('/')
return pathlib.Path(self.ROOT) / path.lstrip('/')

View File

@ -8,9 +8,9 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-21 20:08-0400\n"
"PO-Revision-Date: 2025-02-25 21:04+0000\n"
"Last-Translator: 109247019824 <109247019824@users.noreply.hosted.weblate."
"org>\n"
"PO-Revision-Date: 2025-07-20 18:01+0000\n"
"Last-Translator: 109247019824 <109247019824@users.noreply.hosted.weblate.org>"
"\n"
"Language-Team: Bulgarian <https://hosted.weblate.org/projects/freedombox/"
"freedombox/bg/>\n"
"Language: bg\n"
@ -18,7 +18,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.10.2-dev\n"
"X-Generator: Weblate 5.13-dev\n"
#: config.py:103
#, python-brace-format
@ -199,7 +199,7 @@ msgstr "Домейн в местната мрежа"
#: modules/avahi/manifest.py:14
msgid "Auto-discovery"
msgstr ""
msgstr "Автоматично откриване"
#: modules/avahi/manifest.py:14 modules/backups/manifest.py:17
msgid "Local"
@ -207,7 +207,7 @@ msgstr "Местно"
#: modules/avahi/manifest.py:14
msgid "mDNS"
msgstr ""
msgstr "mDNS"
#: modules/backups/__init__.py:24
msgid "Backups allows creating and managing backup archives."
@ -8203,10 +8203,8 @@ msgstr ""
#: modules/upgrades/templates/upgrades-dist-upgrade-notification.html:9
#: modules/upgrades/templates/upgrades-dist-upgrade.html:11
#: modules/upgrades/templates/upgrades_configure.html:16
#, fuzzy
#| msgid "Distribution update started"
msgid "Distribution Update"
msgstr "Започнато е обновяване на дистрибуцията"
msgstr "Начало на бновяване на дистрибуцията"
#: modules/upgrades/__init__.py:396
msgid "Check for package holds"
@ -8267,10 +8265,8 @@ msgid "Next"
msgstr "Напред"
#: modules/upgrades/templates/upgrades-dist-upgrade-confirm.html:11
#, fuzzy
#| msgid "Could not start distribution update"
msgid "Confirm Distribution Update?"
msgstr "Обновяването на дистрибуцията не може да бъде стартирано"
msgstr "Потвърждавате ли бновяване на дистрибуцията?"
#: modules/upgrades/templates/upgrades-dist-upgrade-confirm.html:21
#, python-format
@ -8303,10 +8299,8 @@ msgid "If the process is interrupted, you should be able to continue it."
msgstr ""
#: modules/upgrades/templates/upgrades-dist-upgrade-confirm.html:66
#, fuzzy
#| msgid "Could not start distribution update"
msgid "Confirm & Start Distribution Update"
msgstr "Обновяването на дистрибуцията не може да бъде стартирано"
msgstr "Потвърждаване и обновяване на дистрибуцията"
#: modules/upgrades/templates/upgrades-dist-upgrade-notification.html:15
msgid ""
@ -8336,10 +8330,8 @@ msgid ""
msgstr ""
#: modules/upgrades/templates/upgrades-dist-upgrade-notification.html:42
#, fuzzy
#| msgid "Test Distribution Upgrade"
msgid "Go to Distribution Update"
msgstr "Надграждане на дистрибуцията до тестова"
msgstr "Към обновяване на дистрибуцията"
#: modules/upgrades/templates/upgrades-dist-upgrade-notification.html:46
#: modules/upgrades/templates/upgrades-new-release.html:22
@ -8368,16 +8360,12 @@ msgid ""
msgstr ""
#: modules/upgrades/templates/upgrades-dist-upgrade.html:50
#, fuzzy
#| msgid "Frequent feature updates are activated."
msgid "Automatic updates are disabled."
msgstr "Честото обновяване на пакети е включено."
msgstr "Автоматичното обновяване е изключено."
#: modules/upgrades/templates/upgrades-dist-upgrade.html:54
#, fuzzy
#| msgid "Distribution update started"
msgid "Distribution upgrades are disabled."
msgstr "Започнато е обновяване на дистрибуцията"
msgstr "Обновяванията на дистрибуцията са изключени."
#: modules/upgrades/templates/upgrades-dist-upgrade.html:58
msgid ""
@ -8390,10 +8378,8 @@ msgid "Your current distribution is mixed or not understood."
msgstr ""
#: modules/upgrades/templates/upgrades-dist-upgrade.html:72
#, fuzzy
#| msgid "Test Distribution Upgrade"
msgid "Current Distribution:"
msgstr "Надграждане на дистрибуцията до тестова"
msgstr "Текуща дистрибуция:"
#: modules/upgrades/templates/upgrades-dist-upgrade.html:74
msgid "Unknown or mixed"
@ -8409,10 +8395,8 @@ msgid "Released: %(date)s."
msgstr ""
#: modules/upgrades/templates/upgrades-dist-upgrade.html:91
#, fuzzy
#| msgid "Test Distribution Upgrade"
msgid "Next Stable Distribution:"
msgstr "Надграждане на дистрибуцията до тестова"
msgstr "Следваща стабилна дистрибуция:"
#: modules/upgrades/templates/upgrades-dist-upgrade.html:93
msgid "Unknown"
@ -8467,22 +8451,16 @@ msgstr ""
#: modules/upgrades/templates/upgrades-dist-upgrade.html:157
#: modules/upgrades/templates/upgrades-dist-upgrade.html:172
#, fuzzy
#| msgid "Test Distribution Upgrade"
msgid "Start Distribution Update"
msgstr "Надграждане на дистрибуцията до тестова"
msgstr "Начало на обновяване на дистрибуцията"
#: modules/upgrades/templates/upgrades-dist-upgrade.html:162
#, fuzzy
#| msgid "Test Distribution Upgrade"
msgid "Continue Distribution Update"
msgstr "Надграждане на дистрибуцията до тестова"
msgstr "Продължаване обновяването на дистрибуцията"
#: modules/upgrades/templates/upgrades-dist-upgrade.html:167
#, fuzzy
#| msgid "Starting distribution upgrade test."
msgid "Start Distribution Update (for testing)"
msgstr "Начало на опит за обновяване на дистрибуцията."
msgstr "Начало на обновяване на дистрибуцията (за проба)"
#: modules/upgrades/templates/upgrades-new-release.html:9
#, python-format
@ -8562,10 +8540,8 @@ msgid "Error when configuring unattended-upgrades"
msgstr "Грешка при настройка на unattended-upgrades"
#: modules/upgrades/views.py:117
#, fuzzy
#| msgid "Starting distribution upgrade test."
msgid "Started distribution update."
msgstr "Начало на опит за обновяване на дистрибуцията."
msgstr "Обновяване на дистрибуцията е започнато."
#: modules/upgrades/views.py:153
msgid "Upgrade process started."

View File

@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-21 20:08-0400\n"
"PO-Revision-Date: 2025-06-19 22:01+0000\n"
"Last-Translator: Priit Jõerüüt <hwlate@joeruut.com>\n"
"PO-Revision-Date: 2025-07-20 18:01+0000\n"
"Last-Translator: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>\n"
"Language-Team: Estonian <https://hosted.weblate.org/projects/freedombox/"
"freedombox/et/>\n"
"Language: et\n"
@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.12.1\n"
"X-Generator: Weblate 5.13-dev\n"
#: config.py:103
#, python-brace-format
@ -921,7 +921,7 @@ msgstr ""
#: modules/miniflux/forms.py:14 modules/networks/forms.py:282
#: modules/shadowsocks/forms.py:32 modules/shadowsocksserver/forms.py:37
msgid "Password"
msgstr ""
msgstr "Salasõna"
#: modules/bepasty/views.py:19
msgid "admin"

View File

@ -20,7 +20,7 @@ from plinth.privileged import service as service_privileged
from plinth.signals import domain_added, domain_removed
from plinth.utils import format_lazy, gettext_noop
from . import aliases, manifest, privileged
from . import aliases, dovecot, manifest, privileged
_description = [
_('This is a complete email server solution using Postfix, Dovecot, '
@ -52,7 +52,7 @@ class EmailApp(plinth.app.App):
app_id = 'email'
_version = 6
_version = 7
def __init__(self) -> None:
"""Initialize the email app."""
@ -95,21 +95,12 @@ class EmailApp(plinth.app.App):
'dovecot-lmtpd', 'dovecot-managesieved', 'dovecot-ldap',
'rspamd', 'redis-server', 'openssl'
], conflicts=['exim4-base', 'exim4-config', 'exim4-daemon-light'],
conflicts_action=Packages.ConflictsAction.REMOVE)
conflicts_action=Packages.ConflictsAction.REMOVE,
rerun_setup_on_upgrade=True)
self.add(packages)
dropin_configs = DropinConfigs('dropin-configs-email', [
'/etc/apache2/conf-available/email-freedombox.conf',
'/etc/dovecot/conf.d/05-freedombox-passdb.conf',
'/etc/dovecot/conf.d/05-freedombox-userdb.conf',
'/etc/dovecot/conf.d/15-freedombox-auth.conf',
'/etc/dovecot/conf.d/15-freedombox-mail.conf',
'/etc/dovecot/conf.d/90-freedombox-imap.conf',
'/etc/dovecot/conf.d/90-freedombox-lmtp.conf',
'/etc/dovecot/conf.d/90-freedombox-mailboxes.conf',
'/etc/dovecot/conf.d/90-freedombox-master.conf',
'/etc/dovecot/conf.d/90-freedombox-tls.conf',
'/etc/dovecot/conf.d/freedombox-ldap.conf.ext',
'/etc/fail2ban/jail.d/dovecot-freedombox.conf',
'/etc/postfix/freedombox-aliases.cf',
'/etc/rspamd/local.d/freedombox-logging.inc',
@ -121,10 +112,24 @@ class EmailApp(plinth.app.App):
dropin_configs_sieve = DropinConfigs('dropin-configs-email-sieve', [
'/etc/dovecot/freedombox-sieve/learn-ham.sieve',
'/etc/dovecot/freedombox-sieve/learn-spam.sieve',
'/etc/dovecot/freedombox-sieve-after/sort-spam.sieve',
'/etc/dovecot/conf.d/95-freedombox-sieve.conf'
'/etc/dovecot/freedombox-sieve-after/sort-spam.sieve'
])
self.add(dropin_configs_sieve)
dropin_configs_dovecot = DovecotDropinConfigs(
'dropin-configs-email-dovecot', [
'/etc/dovecot/conf.d/05-freedombox-passdb.conf',
'/etc/dovecot/conf.d/05-freedombox-userdb.conf',
'/etc/dovecot/conf.d/15-freedombox-auth.conf',
'/etc/dovecot/conf.d/15-freedombox-mail.conf',
'/etc/dovecot/conf.d/90-freedombox-imap.conf',
'/etc/dovecot/conf.d/90-freedombox-lmtp.conf',
'/etc/dovecot/conf.d/90-freedombox-mailboxes.conf',
'/etc/dovecot/conf.d/90-freedombox-master.conf',
'/etc/dovecot/conf.d/90-freedombox-tls.conf',
'/etc/dovecot/conf.d/95-freedombox-sieve.conf',
'/etc/dovecot/conf.d/freedombox-ldap.conf.ext'
])
self.add(dropin_configs_dovecot)
listen_ports = [(25, 'tcp4'), (25, 'tcp6'), (465, 'tcp4'),
(465, 'tcp6'), (587, 'tcp4'), (587, 'tcp6')]
@ -212,13 +217,15 @@ class EmailApp(plinth.app.App):
# Enable drop-in configuration files component for sieve (temporarily)
# to ensure that sievec can compile.
self.get_component('dropin-configs-email-sieve').enable()
self.get_component('dropin-configs-email-dovecot').enable()
service_privileged.try_restart('dovecot')
privileged.setup_spam()
# Restart daemons
service_privileged.try_restart('postfix')
service_privileged.try_restart('dovecot')
service_privileged.try_restart('rspamd')
if self.is_enabled():
service_privileged.restart('postfix')
service_privileged.restart('dovecot')
service_privileged.restart('rspamd')
# Expose to public internet
if old_version == 0:
@ -228,6 +235,20 @@ class EmailApp(plinth.app.App):
service_privileged.try_restart('rspamd')
class DovecotDropinConfigs(DropinConfigs):
"""Configure dovecot based on its package version."""
def get_target_path(self, path):
"""Return Path object for a target path."""
version = '2.3'
if dovecot.is_version_24():
version = '2.4'
target_path = super().get_target_path(path)
target_path = target_path.parent / version / target_path.name
return target_path
def _get_first_admin():
"""Return an admin user in the system or None if non exist."""
from django.contrib.auth.models import User

View File

@ -0,0 +1,20 @@
# Do not edit this file. Manage your settings on FreedomBox.
# See:
# https://doc.dovecot.org/main/core/config/auth/passdb.html
# https://doc.dovecot.org/main/howto/active_directory.html
#
# For passdb, the passwd driver looks up using NSS. In FreedomBox, NSS is
# configured to lookup LDAP with the help of libnss-ldapd. Lookup using passdb
# would have been sufficient if FreedomBox allowed all its users to login using
# pam. However, by default, FreedomBox disallows all users but 'admin' group to
# login. Hence, the need for LDAP lookup.
#
passdb freedombox-ldap {
driver = ldap
ldap_uris = ldapi:///
ldap_base = dc=thisbox
ldap_bind = yes
ldap_bind_userdn = uid=%{user},ou=users,dc=thisbox
ldap_filter = (&(objectClass=posixAccount)(uid=%{user}))
}

View File

@ -0,0 +1,32 @@
# Do not edit this file. Manage your settings on FreedomBox.
# See:
# https://doc.dovecot.org/main/core/config/auth/userdb.html
#
# Users in FreedomBox are not expected to access mail by logging into the
# system. Storing the mail in single location instead of home directories and
# with single UID/GID simplifies security reasoning and backup/restore
# operations.
#
# When FreedomBox has multiple domains a user is expected to get a mailbox that
# is same across the domains. Changing an domain name is not uncommon in
# FreedomBox. So, authenticate and store mails based on username only instead of
# including domain names in storage path.
#
# Directories are created under /var/mail as necessary by dovecot. Permissions
# for newly created directories are inherited from parent directory. FreedomBox
# will remove all permissions for 'others' from /var/mail to ensure that mail is
# not read by non-root users.
#
# userdb provides lookup for three parameters after authentication of a user.
# These parameters are uid, gid, and home directory of the user. If these do not
# change from user to user, a 'static' database type with fixed values is
# sufficient as userdb.
userdb freedombox-static {
driver = static
fields {
uid=mail
gid=mail
home=/var/mail/%{user | username | lower}
}
}

View File

@ -0,0 +1,10 @@
# Do not edit this file. Manage your settings on FreedomBox.
# See:
# https://doc.dovecot.org/main/core/config/auth/basic.html
# https://doc.dovecot.org/main/core/config/auth/databases/ldap.html#username
# Outlook and Windows Mail work only with LOGIN mechanism, not the standard PLAIN
auth_mechanisms = plain login
auth_username_format = %{user | lower}

View File

@ -0,0 +1,18 @@
# Do not edit this file. Manage your settings on FreedomBox.
# See: https://doc.dovecot.org/main/core/config/mail_location.html
# Use sdbox, a format specific to dovecot, for storing mails. The format allows
# better performance with some IMAP queries. When this is combined with Full
# Text Search (FTS), users will get optimal web and desktop mail experience.
# Don't pick mdbox format because is requires regular expunge maintenance. We
# have enabled btrfs filesystem compression by default.
mail_driver = sdbox
mail_path = ~/mail
# We try to deliver all mail using a single UID 'mail' and a single GID 'mail'.
# In Debian, UID of mail user is 8 and GID of mail user is 8 as set in
# /usr/share/base-passwd/{passwd|group}.master. By default first valid UID in
# dovecot is 500.
first_valid_uid = 8
last_valid_uid = 8

View File

@ -0,0 +1,11 @@
# Do not edit this file. Manage your settings on FreedomBox.
# Make rspamd learn spam/ham when the user marks mails as junk or not junk.
# https://doc.dovecot.org/main/core/config/sieve/overview.html
# https://doc.dovecot.org/main/core/plugins/sieve.html
protocol imap {
mail_plugins {
imap_sieve = yes
}
}

View File

@ -0,0 +1,12 @@
# Do not edit this file. Manage your settings on FreedomBox.
# See:
# https://doc.dovecot.org/main/core/config/sieve/overview.html
# https://doc.dovecot.org/main/core/plugins/sieve.html
# Enable the sieve plugin to sort mail during delivery using sieve scripts.
protocol lmtp {
mail_plugins {
sieve = yes
}
}

View File

@ -0,0 +1,72 @@
# Do not edit this file. Manage your settings on FreedomBox.
# Mark various mailboxes with special use flags (RFC 6154). Various names used
# in mail clients for mailboxes: https://www.imapwiki.org/SpecialUse
# See:
# https://doc.dovecot.org/main/core/config/mail_location.html#custom-namespace-location
namespace inbox {
# Archive
mailbox Archive {
auto = subscribe
special_use = \Archive
}
mailbox Archives { # Thunderbird
auto = no
special_use = \Archive
}
# Drafts
mailbox Drafts {
auto = subscribe
special_use = \Drafts
}
# Sent
mailbox Sent {
auto = subscribe
special_use = \Sent
}
mailbox "Sent Items" { # Outlook 2010/2013
auto = no
special_use = \Sent
}
mailbox "Sent Messages" { # iOS
auto = no
special_use = \Sent
}
# Junk
mailbox Junk {
auto = subscribe
autoexpunge = 60d
special_use = \Junk
}
mailbox Spam { # KMail, K-9 Mail
auto = no
autoexpunge = 60d
special_use = \Junk
}
mailbox "Junk E-mail" { # Outlook 2010
auto = no
autoexpunge = 60d
special_use = \Junk
}
mailbox INBOX.Junk {
auto = no
autoexpunge = 60d
special_use = \Junk
}
# Trash
mailbox Trash {
auto = subscribe
autoexpunge = 60d
special_use = \Trash
}
mailbox INBOX.Trash {
auto = no
autoexpunge = 60d
special_use = \Trash
}
}

View File

@ -0,0 +1,21 @@
# Do not edit this file. Manage your settings on FreedomBox.
# Listen on Unix domain sockets for postfix to use dovecot SASL authentication
# and for postfix to deliver mail using dovecot to local mailboxes. See:
# https://doc.dovecot.org/main/howto/sasl/postfix.html#postfix-and-dovecot-sasl
service auth {
unix_listener /var/spool/postfix/private/auth {
mode = 0600
user = postfix
group = postfix
}
}
service lmtp {
unix_listener /var/spool/postfix/private/dovecot-lmtp {
mode = 0600
user = postfix
group = postfix
}
}

View File

@ -0,0 +1,12 @@
# Do not edit this file. Manage your settings on FreedomBox.
# Mozilla Guideline v5.7, Dovecot 2.3.21, OpenSSL 3.4.0, intermediate.
# Generated 2025-07-16: https://ssl-config.mozilla.org/
# See: https://doc.dovecot.org/main/core/config/ssl.html
ssl = required
ssl_min_protocol = TLSv1.2
ssl_server_prefer_ciphers = client
ssl_curve_list = X25519:prime256v1:secp384r1
ssl_cipher_list = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305

View File

@ -0,0 +1,43 @@
# Do not edit this file. Manage your settings on FreedomBox.
# Default sieve scripts applied for delivery to all users. To move mail to Junk
# folder based on classification headers set by rspamd. See:
# https://doc.dovecot.org/main/core/plugins/sieve.html
sieve_script freedombox-after {
type = after
driver = file
path = /etc/dovecot/freedombox-sieve-after
}
sieve_plugins {
sieve_imapsieve = yes
sieve_extprograms = yes
}
sieve_global_extensions {
vnd.dovecot.pipe = yes
vnd.dovecot.environment = yes
}
# Make rspamd learn spam/ham when the user marks mails as junk or not junk.
# https://doc.dovecot.org/main/core/config/spam_reporting.html
sieve_pipe_bin_dir = /usr/bin
# When moving a mail from to Junk folder from elsewhere
mailbox Junk {
sieve_script learn-spam {
type = before
cause = copy
path = /etc/dovecot/freedombox-sieve/learn-spam.sieve
}
}
# When moving a mail from from Junk folder to elsewhere
imapsieve_from Junk {
sieve_script learn-ham {
type = before
cause = copy
path = /etc/dovecot/freedombox-sieve/learn-ham.sieve
}
}

View File

@ -0,0 +1,4 @@
# Do not edit this file. Manage your settings on FreedomBox.
# This file is not needed for Dovecot >= 2.4. It is only needed for simplifying
# compatibility with Dovecot 2.3.

View File

@ -0,0 +1,17 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Utilities to configure Dovecot."""
import apt
from plinth.utils import Version
def is_version_24():
"""Return the currently installed version of Dovecot."""
cache = apt.Cache()
try:
version = cache['dovecot-core'].installed.version
except KeyError:
return True
return Version(version) >= Version('1:2.4')

View File

@ -2,8 +2,7 @@
"""Provides privileged actions that run as root."""
from .aliases import setup_aliases
from .dkim import (get_dkim_public_key, setup_dkim,
fix_incorrect_key_ownership)
from .dkim import fix_incorrect_key_ownership, get_dkim_public_key, setup_dkim
from .domain import set_domains
from .home import setup_home
from .postfix import setup_postfix

View File

@ -10,6 +10,7 @@ See: https://doc.dovecot.org/configuration_manual/dovecot_ssl_configuration/
import pathlib
from .. import postfix
from ..dovecot import is_version_24
# Mozilla Guideline v5.6, Postfix 1.17.7, OpenSSL 1.1.1d, intermediate
# Generated 2021-08
@ -68,15 +69,27 @@ def set_postfix_config(primary_domain, all_domains):
def set_dovecot_config(primary_domain, all_domains):
"""Set dovecot configuration for TLS certificates."""
is_new_version = is_version_24()
# Determine whether to prefix file paths with '<' based on version
prefix = ''
cert_naming = 'ssl_server_cert_file'
key_naming = 'ssl_server_key_file'
if not is_new_version:
prefix = '<'
cert_naming = 'ssl_cert'
key_naming = 'ssl_key'
content = f'''# This file is managed by FreedomBox
ssl_cert = </etc/dovecot/letsencrypt/{primary_domain}/cert.pem
ssl_key = </etc/dovecot/letsencrypt/{primary_domain}/privkey.pem
{cert_naming} = {prefix}/etc/dovecot/letsencrypt/{primary_domain}/cert.pem
{key_naming} = {prefix}/etc/dovecot/letsencrypt/{primary_domain}/privkey.pem
'''
for domain in all_domains:
content += f'''
local_name {domain} {{
ssl_cert = </etc/dovecot/letsencrypt/{domain}/cert.pem
ssl_key = </etc/dovecot/letsencrypt/{domain}/privkey.pem
{cert_naming} = {prefix}/etc/dovecot/letsencrypt/{domain}/cert.pem
{key_naming} = {prefix}/etc/dovecot/letsencrypt/{domain}/privkey.pem
}}
'''
cert_config = pathlib.Path('/etc/dovecot/conf.d/91-freedombox-tls.conf')

View File

@ -10,7 +10,7 @@ from plinth import module_loader
from plinth.actions import privileged
def _assert_managed_dropin_config(app_id: str, path: str):
def _get_managed_dropin_config(app_id: str, path: str):
"""Check that this is a path managed by the specified app."""
module_path = module_loader.get_module_import_path(app_id)
module = importlib.import_module(module_path)
@ -25,7 +25,7 @@ def _assert_managed_dropin_config(app_id: str, path: str):
components = app.get_components_of_type(DropinConfigs)
for component in components:
if path in component.etc_paths:
return
return component
raise AssertionError('Not a managed drop-in config')
@ -37,10 +37,9 @@ def dropin_is_valid(app_id: str, path: str, copy_only: bool,
Optionally, drop the link if it is invalid.
"""
_assert_managed_dropin_config(app_id, path)
from plinth.config import DropinConfigs
etc_path = DropinConfigs.get_etc_path(path)
target = DropinConfigs.get_target_path(path)
component = _get_managed_dropin_config(app_id, path)
etc_path = component.get_etc_path(path)
target = component.get_target_path(path)
if etc_path.exists() or etc_path.is_symlink():
if (not copy_only and etc_path.is_symlink()
and etc_path.readlink() == target):
@ -59,10 +58,9 @@ def dropin_is_valid(app_id: str, path: str, copy_only: bool,
@privileged
def dropin_link(app_id: str, path: str, copy_only: bool):
"""Create a symlink from /etc/ to /usr/share/freedombox/etc."""
_assert_managed_dropin_config(app_id, path)
from plinth.config import DropinConfigs
target = DropinConfigs.get_target_path(path)
etc_path = DropinConfigs.get_etc_path(path)
component = _get_managed_dropin_config(app_id, path)
target = component.get_target_path(path)
etc_path = component.get_etc_path(path)
etc_path.parent.mkdir(parents=True, exist_ok=True)
if copy_only:
shutil.copyfile(target, etc_path)
@ -73,7 +71,6 @@ def dropin_link(app_id: str, path: str, copy_only: bool):
@privileged
def dropin_unlink(app_id: str, path: str, missing_ok: bool = False):
"""Remove a symlink in /etc/."""
_assert_managed_dropin_config(app_id, path)
from plinth.config import DropinConfigs
etc_path = DropinConfigs.get_etc_path(path)
component = _get_managed_dropin_config(app_id, path)
etc_path = component.get_etc_path(path)
etc_path.unlink(missing_ok=missing_ok)

View File

@ -31,9 +31,10 @@ def fixture_dropin_configs():
@pytest.fixture(autouse=True)
def fixture_assert_dropin_config():
def fixture_assert_dropin_config(dropin_configs):
"""Mock asserting dropin config path."""
with patch('plinth.privileged.config._assert_managed_dropin_config'):
with patch('plinth.privileged.config._get_managed_dropin_config') as mock:
mock.return_value = dropin_configs
yield
@ -95,7 +96,7 @@ def test_dropin_configs_enable_disable_symlinks(dropin_configs, tmp_path):
# Enable when a file already exists
dropin_configs.disable()
etc_path = DropinConfigs.get_etc_path('/etc/test/path1')
etc_path = dropin_configs.get_etc_path('/etc/test/path1')
etc_path.touch()
dropin_configs.enable()
_assert_symlinks(dropin_configs, tmp_path, should_exist=True)
@ -108,7 +109,7 @@ def test_dropin_configs_enable_disable_symlinks(dropin_configs, tmp_path):
# When symlink already exists to correct location
dropin_configs.disable()
target_path = DropinConfigs.get_target_path('/etc/test/path1')
target_path = dropin_configs.get_target_path('/etc/test/path1')
etc_path.symlink_to(target_path)
dropin_configs.enable()
_assert_symlinks(dropin_configs, tmp_path, should_exist=True)
@ -119,7 +120,7 @@ def test_dropin_configs_enable_disable_copy_only(dropin_configs, tmp_path):
with patch('plinth.config.DropinConfigs.ROOT', new=tmp_path):
dropin_configs.copy_only = True
for path in ['/etc/test/path1', '/etc/path2']:
target = DropinConfigs.get_target_path(path)
target = dropin_configs.get_target_path(path)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text('test-config-content')
@ -135,7 +136,7 @@ def test_dropin_configs_enable_disable_copy_only(dropin_configs, tmp_path):
# Enable when a file already exists with wrong content
dropin_configs.disable()
etc_path = DropinConfigs.get_etc_path('/etc/test/path1')
etc_path = dropin_configs.get_etc_path('/etc/test/path1')
etc_path.write_text('x-invalid-content')
dropin_configs.enable()
_assert_symlinks(dropin_configs, tmp_path, should_exist=True,
@ -182,7 +183,7 @@ def test_dropin_config_diagnose_symlinks(dropin_configs, tmp_path):
# A file exists instead of symlink
dropin_configs.disable()
etc_path = DropinConfigs.get_etc_path('/etc/test/path1')
etc_path = dropin_configs.get_etc_path('/etc/test/path1')
etc_path.touch()
results = dropin_configs.diagnose()
assert results[0].result == 'failed'
@ -204,7 +205,7 @@ def test_dropin_config_diagnose_copy_only(dropin_configs, tmp_path):
with patch('plinth.config.DropinConfigs.ROOT', new=tmp_path):
dropin_configs.copy_only = True
for path in ['/etc/test/path1', '/etc/path2']:
target = DropinConfigs.get_target_path(path)
target = dropin_configs.get_target_path(path)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text('test-config-content')
@ -221,7 +222,7 @@ def test_dropin_config_diagnose_copy_only(dropin_configs, tmp_path):
# A symlink exists instead of a copied file
dropin_configs.disable()
etc_path = DropinConfigs.get_etc_path('/etc/test/path1')
etc_path = dropin_configs.get_etc_path('/etc/test/path1')
etc_path.symlink_to('/blah')
results = dropin_configs.diagnose()
assert results[0].result == 'failed'