14 Commits

Author SHA1 Message Date
github-actions[bot]
00d79f518d [GitHub Action] Updated version file 2025-11-21 15:12:39 +00:00
Antoine Nguyen
03b124501e Updated nginx config to redirect autodiscover requests to modoboa 2025-11-21 16:10:58 +01:00
github-actions[bot]
5f357aef42 [GitHub Action] Updated version file 2025-11-17 08:13:43 +00:00
Antoine Nguyen
e1aa0ab723 Removed legacy option for radicale socket 2025-11-17 09:12:32 +01:00
github-actions[bot]
88b2384fa8 [GitHub Action] Updated version file 2025-11-07 15:22:17 +00:00
Antoine Nguyen
bb02255c0f Merge pull request #613 from modoboa/fix/oauth2-client-secrets
Make sure to reuse same client secrets between runs.
2025-11-07 16:21:09 +01:00
Antoine Nguyen
7a38a535f8 Make sure to reuse same client secrets between runs. 2025-11-07 16:09:51 +01:00
github-actions[bot]
2121cfe267 [GitHub Action] Updated version file 2025-11-02 10:01:31 +00:00
Antoine Nguyen
36c8352223 Added missing config param
fix #605
2025-11-02 10:59:54 +01:00
github-actions[bot]
01ec9b406f [GitHub Action] Updated version file 2025-11-02 09:33:05 +00:00
Antoine Nguyen
f2c7423296 Merge pull request #610 from modoboa/mailbox-auto-creation
Added mailbox file and activate the auto subscribe
2025-11-02 10:32:00 +01:00
Spitap
9276953f87 Created 15-mailboxes.conf.tpl 2025-11-01 10:42:20 +01:00
Spitap
11f6f646d1 Fixed typos 2025-10-31 22:51:01 +01:00
Spitap
46d7c6acca Added mailbox file and activate the auto subscribe 2025-10-31 16:36:08 +01:00
9 changed files with 193 additions and 53 deletions

View File

@@ -1,5 +1,6 @@
import random import random
import string import string
import uuid
from .constants import DEFAULT_BACKUP_DIRECTORY from .constants import DEFAULT_BACKUP_DIRECTORY
@@ -11,6 +12,10 @@ def make_password(length=16):
string.ascii_letters + string.digits) for _ in range(length)) string.ascii_letters + string.digits) for _ in range(length))
def make_client_secret():
return str(uuid.uuid4())
# Validators should return a tuple bool, error message # Validators should return a tuple bool, error message
def is_email(user_input): def is_email(user_input):
"""Return True in input is a valid email""" """Return True in input is a valid email"""
@@ -351,6 +356,10 @@ ConfigDictTemplate = [
"option": "move_spam_to_junk", "option": "move_spam_to_junk",
"default": "true", "default": "true",
}, },
{
"option": "oauth2_client_secret",
"default": make_client_secret
},
] ]
}, },
{ {
@@ -480,7 +489,11 @@ ConfigDictTemplate = [
{ {
"option": "venv_path", "option": "venv_path",
"default": "%(home_dir)s/env", "default": "%(home_dir)s/env",
} },
{
"option": "oauth2_client_secret",
"default": make_client_secret
},
] ]
}, },
{ {

View File

@@ -23,10 +23,9 @@ class Dovecot(base.Installer):
"dovecot-imapd", "dovecot-imapd",
"dovecot-lmtpd", "dovecot-lmtpd",
"dovecot-managesieved", "dovecot-managesieved",
"dovecot-sieve" "dovecot-sieve",
], ],
"rpm": [ "rpm": ["dovecot", "dovecot-pigeonhole"],
"dovecot", "dovecot-pigeonhole"]
} }
per_version_config_files = { per_version_config_files = {
"2.3": [ "2.3": [
@@ -44,9 +43,10 @@ class Dovecot(base.Installer):
"conf.d/10-master.conf", "conf.d/10-master.conf",
"conf.d/10-ssl.conf", "conf.d/10-ssl.conf",
"conf.d/10-ssl-keys.try", "conf.d/10-ssl-keys.try",
"conf.d/15-mailboxes.conf",
"conf.d/20-lmtp.conf", "conf.d/20-lmtp.conf",
"conf.d/auth-oauth2.conf.ext", "conf.d/auth-oauth2.conf.ext",
] ],
} }
with_user = True with_user = True
@@ -73,7 +73,7 @@ class Dovecot(base.Installer):
else: else:
files += [ files += [
f"dovecot-sql-{self.dbengine}.conf.ext=dovecot-sql.conf.ext", f"dovecot-sql-{self.dbengine}.conf.ext=dovecot-sql.conf.ext",
f"dovecot-sql-master-{self.dbengine}.conf.ext=dovecot-sql-master.conf.ext" f"dovecot-sql-master-{self.dbengine}.conf.ext=dovecot-sql-master.conf.ext",
] ]
result = [] result = []
for path in files: for path in files:
@@ -107,7 +107,9 @@ class Dovecot(base.Installer):
packages += super().get_packages() packages += super().get_packages()
backports_codename = getattr(self, "backports_codename", None) backports_codename = getattr(self, "backports_codename", None)
if backports_codename: if backports_codename:
packages = [f"{package}/{backports_codename}-backports" for package in packages] packages = [
f"{package}/{backports_codename}-backports" for package in packages
]
return packages return packages
def install_packages(self): def install_packages(self):
@@ -118,7 +120,8 @@ class Dovecot(base.Installer):
package.backend.enable_backports("bookworm") package.backend.enable_backports("bookworm")
self.backports_codename = "bookworm" self.backports_codename = "bookworm"
package.backend.preconfigure( package.backend.preconfigure(
"dovecot-core", "create-ssl-cert", "boolean", "false") "dovecot-core", "create-ssl-cert", "boolean", "false"
)
super().install_packages() super().install_packages()
def get_template_context(self): def get_template_context(self):
@@ -127,11 +130,17 @@ class Dovecot(base.Installer):
pw_mailbox = pwd.getpwnam(self.mailboxes_owner) pw_mailbox = pwd.getpwnam(self.mailboxes_owner)
dovecot_package = {"deb": "dovecot-core", "rpm": "dovecot"} dovecot_package = {"deb": "dovecot-core", "rpm": "dovecot"}
ssl_protocol_parameter = "ssl_protocols" ssl_protocol_parameter = "ssl_protocols"
if package.backend.get_installed_version(dovecot_package[package.backend.FORMAT]) > "2.3": if (
package.backend.get_installed_version(
dovecot_package[package.backend.FORMAT]
)
> "2.3"
):
ssl_protocol_parameter = "ssl_min_protocol" ssl_protocol_parameter = "ssl_min_protocol"
ssl_protocols = "!SSLv2 !SSLv3" ssl_protocols = "!SSLv2 !SSLv3"
if package.backend.get_installed_version("openssl").startswith("1.1") \ if package.backend.get_installed_version("openssl").startswith(
or package.backend.get_installed_version("openssl").startswith("3"): "1.1"
) or package.backend.get_installed_version("openssl").startswith("3"):
ssl_protocols = "!SSLv3" ssl_protocols = "!SSLv3"
if ssl_protocol_parameter == "ssl_min_protocol": if ssl_protocol_parameter == "ssl_min_protocol":
ssl_protocols = "TLSv1.2" ssl_protocols = "TLSv1.2"
@@ -145,14 +154,19 @@ class Dovecot(base.Installer):
protocols = "" protocols = ""
oauth2_client_id, oauth2_client_secret = utils.create_oauth2_app( oauth2_client_id, oauth2_client_secret = utils.create_oauth2_app(
"Dovecot", "dovecot", self.config) "Dovecot",
"dovecot",
self.config.get("dovecot", "oauth2_client_secret"),
self.config
)
hostname = self.config.get("general", "hostname") hostname = self.config.get("general", "hostname")
oauth2_introspection_url = ( oauth2_introspection_url = (
f"https://{oauth2_client_id}:{oauth2_client_secret}" f"https://{oauth2_client_id}:{oauth2_client_secret}"
f"@{hostname}/api/o/introspect/" f"@{hostname}/api/o/introspect/"
) )
context.update({ context.update(
{
"db_driver": self.db_driver, "db_driver": self.db_driver,
"mailboxes_owner_uid": pw_mailbox[2], "mailboxes_owner_uid": pw_mailbox[2],
"mailboxes_owner_gid": pw_mailbox[3], "mailboxes_owner_gid": pw_mailbox[3],
@@ -165,11 +179,16 @@ class Dovecot(base.Installer):
"ssl_protocols": ssl_protocols, "ssl_protocols": ssl_protocols,
"ssl_protocol_parameter": ssl_protocol_parameter, "ssl_protocol_parameter": ssl_protocol_parameter,
"modoboa_2_2_or_greater": "" if self.modoboa_2_2_or_greater else "#", "modoboa_2_2_or_greater": "" if self.modoboa_2_2_or_greater else "#",
"not_modoboa_2_2_or_greater": "" if not self.modoboa_2_2_or_greater else "#", "not_modoboa_2_2_or_greater": (
"do_move_spam_to_junk": "" if self.app_config["move_spam_to_junk"] else "#", "" if not self.modoboa_2_2_or_greater else "#"
),
"do_move_spam_to_junk": (
"" if self.app_config["move_spam_to_junk"] else "#"
),
"oauth2_introspection_url": oauth2_introspection_url, "oauth2_introspection_url": oauth2_introspection_url,
"radicale_user": self.config.get("radicale", "user"), "radicale_user": self.config.get("radicale", "user"),
}) }
)
return context return context
def install_config_files(self): def install_config_files(self):
@@ -177,9 +196,13 @@ class Dovecot(base.Installer):
if self.app_config["move_spam_to_junk"]: if self.app_config["move_spam_to_junk"]:
utils.mkdir_safe( utils.mkdir_safe(
f"{self.config_dir}/conf.d/custom_after_sieve", f"{self.config_dir}/conf.d/custom_after_sieve",
stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IRWXU
stat.S_IROTH | stat.S_IXOTH, | stat.S_IRGRP
0, 0 | stat.S_IXGRP
| stat.S_IROTH
| stat.S_IXOTH,
0,
0,
) )
super().install_config_files() super().install_config_files()
@@ -191,12 +214,16 @@ class Dovecot(base.Installer):
dbpassword = self.config.get("modoboa", "dbpassword") dbpassword = self.config.get("modoboa", "dbpassword")
backend = database.get_backend(self.config) backend = database.get_backend(self.config)
backend.load_sql_file( backend.load_sql_file(
dbname, dbuser, dbpassword, dbname,
self.get_file_path("install_modoboa_postgres_trigger.sql") dbuser,
dbpassword,
self.get_file_path("install_modoboa_postgres_trigger.sql"),
) )
backend.load_sql_file( backend.load_sql_file(
dbname, dbuser, dbpassword, dbname,
self.get_file_path("fix_modoboa_postgres_schema.sql") dbuser,
dbpassword,
self.get_file_path("fix_modoboa_postgres_schema.sql"),
) )
for f in glob.glob(f"{self.get_file_path(f'{self.version}/conf.d')}/*"): for f in glob.glob(f"{self.get_file_path(f'{self.version}/conf.d')}/*"):
if os.path.isfile(f): if os.path.isfile(f):
@@ -210,9 +237,11 @@ class Dovecot(base.Installer):
# See https://github.com/modoboa/modoboa/issues/2157. # See https://github.com/modoboa/modoboa/issues/2157.
if self.app_config["move_spam_to_junk"]: if self.app_config["move_spam_to_junk"]:
# Compile sieve script # Compile sieve script
sieve_file = f"{self.config_dir}/conf.d/custom_after_sieve/spam-to-junk.sieve" sieve_file = (
f"{self.config_dir}/conf.d/custom_after_sieve/spam-to-junk.sieve"
)
utils.exec_cmd(f"/usr/bin/sievec {sieve_file}") utils.exec_cmd(f"/usr/bin/sievec {sieve_file}")
system.add_user_to_group(self.mailboxes_owner, 'dovecot') system.add_user_to_group(self.mailboxes_owner, "dovecot")
def restart_daemon(self): def restart_daemon(self):
"""Restart daemon process. """Restart daemon process.
@@ -226,7 +255,8 @@ class Dovecot(base.Installer):
action = "start" if code else "restart" action = "start" if code else "restart"
utils.exec_cmd( utils.exec_cmd(
"service {} {} > /dev/null 2>&1".format(self.appname, action), "service {} {} > /dev/null 2>&1".format(self.appname, action),
capture_output=False) capture_output=False,
)
system.enable_service(self.get_daemon_name()) system.enable_service(self.get_daemon_name())
def backup(self, path): def backup(self, path):
@@ -234,8 +264,10 @@ class Dovecot(base.Installer):
home_dir = self.config.get("dovecot", "home_dir") home_dir = self.config.get("dovecot", "home_dir")
utils.printcolor("Backing up mails", utils.MAGENTA) utils.printcolor("Backing up mails", utils.MAGENTA)
if not os.path.exists(home_dir) or os.path.isfile(home_dir): if not os.path.exists(home_dir) or os.path.isfile(home_dir):
utils.error("Error backing up emails, provided path " utils.error(
f" ({home_dir}) seems not right...") "Error backing up emails, provided path "
f" ({home_dir}) seems not right..."
)
return return
dst = os.path.join(path, "mails/") dst = os.path.join(path, "mails/")
@@ -257,10 +289,13 @@ class Dovecot(base.Installer):
for dirpath, dirnames, filenames in os.walk(home_dir): for dirpath, dirnames, filenames in os.walk(home_dir):
shutil.chown(dirpath, self.mailboxes_owner, self.mailboxes_owner) shutil.chown(dirpath, self.mailboxes_owner, self.mailboxes_owner)
for filename in filenames: for filename in filenames:
shutil.chown(os.path.join(dirpath, filename), shutil.chown(
self.mailboxes_owner, self.mailboxes_owner) os.path.join(dirpath, filename),
self.mailboxes_owner,
self.mailboxes_owner,
)
else: else:
utils.printcolor( utils.printcolor(
"It seems that emails were not backed up, skipping restoration.", "It seems that emails were not backed up, skipping restoration.",
utils.MAGENTA utils.MAGENTA,
) )

View File

@@ -149,7 +149,6 @@ service auth {
%{radicale_enabled} mode = 0666 %{radicale_enabled} mode = 0666
%{radicale_enabled} user = %{radicale_user} %{radicale_enabled} user = %{radicale_user}
%{radicale_enabled} group = %{radicale_user} %{radicale_enabled} group = %{radicale_user}
%{radicale_enabled} type = auth-legacy
%{radicale_enabled}} %{radicale_enabled}}
# Auth process is run as this user. # Auth process is run as this user.

View File

@@ -0,0 +1,90 @@
##
## Mailbox definitions
##
# Each mailbox is specified in a separate mailbox section. The section name
# specifies the mailbox name. If it has spaces, you can put the name
# "in quotes". These sections can contain the following mailbox settings:
#
# auto:
# Indicates whether the mailbox with this name is automatically created
# implicitly when it is first accessed. The user can also be automatically
# subscribed to the mailbox after creation. The following values are
# defined for this setting:
#
# no - Never created automatically.
# create - Automatically created, but no automatic subscription.
# subscribe - Automatically created and subscribed.
#
# special_use:
# A space-separated list of SPECIAL-USE flags (RFC 6154) to use for the
# mailbox. There are no validity checks, so you could specify anything
# you want in here, but it's not a good idea to use flags other than the
# standard ones specified in the RFC:
#
# \All - This (virtual) mailbox presents all messages in the
# user's message store.
# \Archive - This mailbox is used to archive messages.
# \Drafts - This mailbox is used to hold draft messages.
# \Flagged - This (virtual) mailbox presents all messages in the
# user's message store marked with the IMAP \Flagged flag.
# \Important - This (virtual) mailbox presents all messages in the
# user's message store deemed important to user.
# \Junk - This mailbox is where messages deemed to be junk mail
# are held.
# \Sent - This mailbox is used to hold copies of messages that
# have been sent.
# \Trash - This mailbox is used to hold messages that have been
# deleted.
#
# comment:
# Defines a default comment or note associated with the mailbox. This
# value is accessible through the IMAP METADATA mailbox entries
# "/shared/comment" and "/private/comment". Users with sufficient
# privileges can override the default value for entries with a custom
# value.
# NOTE: Assumes "namespace inbox" has been defined in 10-mail.conf.
namespace inbox {
# These mailboxes are widely used and could perhaps be created automatically:
mailbox Drafts {
auto = subscribe
special_use = \Drafts
}
mailbox Junk {
auto = subscribe
special_use = \Junk
}
mailbox Trash {
auto = subscribe
special_use = \Trash
}
# For \Sent mailboxes there are two widely used names. We'll mark both of
# them as \Sent. User typically deletes one of them if duplicates are created.
mailbox Sent {
auto = subscribe
special_use = \Sent
}
# mailbox "Sent Messages" {
# special_use = \Sent
# }
# If you have a virtual "All messages" mailbox:
#mailbox virtual/All {
# special_use = \All
# comment = All my messages
#}
# If you have a virtual "Flagged" mailbox:
#mailbox virtual/Flagged {
# special_use = \Flagged
# comment = All my flagged messages
#}
# If you have a virtual "Important" mailbox:
#mailbox virtual/Important {
# special_use = \Important
# comment = All my important messages
#}
}

View File

@@ -9,6 +9,7 @@ server {
location ~ ^/(mail/config-v1.1.xml|mobileconfig) { location ~ ^/(mail/config-v1.1.xml|mobileconfig) {
include uwsgi_params; include uwsgi_params;
uwsgi_param UWSGI_SCRIPT instance.wsgi:application;
uwsgi_pass modoboa; uwsgi_pass modoboa;
} }
} }

View File

@@ -44,7 +44,7 @@ server {
%{rspamd_enabled} proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; %{rspamd_enabled} proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
%{rspamd_enabled} } %{rspamd_enabled} }
location ~ ^/(api|accounts) { location ~ ^/(api|accounts|autodiscover) {
include uwsgi_params; include uwsgi_params;
uwsgi_param UWSGI_SCRIPT instance.wsgi:application; uwsgi_param UWSGI_SCRIPT instance.wsgi:application;
uwsgi_pass modoboa; uwsgi_pass modoboa;

View File

@@ -41,7 +41,11 @@ class Radicale(base.Installer):
"""Additional variables.""" """Additional variables."""
context = super().get_template_context() context = super().get_template_context()
oauth2_client_id, oauth2_client_secret = utils.create_oauth2_app( oauth2_client_id, oauth2_client_secret = utils.create_oauth2_app(
"Radicale", "radicale", self.config) "Radicale",
"radicale",
self.config.get("radicale", "oauth2_client_secret"),
self.config
)
hostname = self.config.get("general", "hostname") hostname = self.config.get("general", "hostname")
oauth2_introspection_url = ( oauth2_introspection_url = (
f"https://{oauth2_client_id}:{oauth2_client_secret}" f"https://{oauth2_client_id}:{oauth2_client_secret}"

View File

@@ -13,7 +13,6 @@ import stat
import string import string
import subprocess import subprocess
import sys import sys
import uuid
from . import config_dict_template from . import config_dict_template
from .compatibility_matrix import APP_INCOMPATIBILITY from .compatibility_matrix import APP_INCOMPATIBILITY
@@ -515,14 +514,13 @@ def validate_backup_path(path: str, silent_mode: bool):
return backup_path return backup_path
def create_oauth2_app(app_name: str, client_id: str, config) -> tuple[str, str]: def create_oauth2_app(app_name: str, client_id: str, client_secret: str, config) -> tuple[str, str]:
"""Create a application for Oauth2 authentication.""" """Create a application for Oauth2 authentication."""
# FIXME: how can we check that application already exists ? # FIXME: how can we check that application already exists ?
venv_path = config.get("modoboa", "venv_path") venv_path = config.get("modoboa", "venv_path")
python_path = os.path.join(venv_path, "bin", "python") python_path = os.path.join(venv_path, "bin", "python")
instance_path = config.get("modoboa", "instance_path") instance_path = config.get("modoboa", "instance_path")
script_path = os.path.join(instance_path, "manage.py") script_path = os.path.join(instance_path, "manage.py")
client_secret = str(uuid.uuid4())
cmd = ( cmd = (
f"{python_path} {script_path} createapplication " f"{python_path} {script_path} createapplication "
f"--name={app_name} --skip-authorization " f"--name={app_name} --skip-authorization "

View File

@@ -1 +1 @@
1d701353d900f4b6e2f7ffba6f6b7a46d304f58b 03b124501ec1a61eaa3063ac9fb839fdbc64f00c