Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Available addons
----------------
addon | version | maintainers | summary
--- | --- | --- | ---
[mail_environment](mail_environment/) | 19.0.1.0.0 | | Configure mail servers with server_environment_files
[mail_environment](mail_environment/) | 19.0.1.1.0 | | Configure mail servers with server_environment_files
[server_environment](server_environment/) | 19.0.1.0.2 | | move some configurations out of the database
[server_environment_ir_config_parameter](server_environment_ir_config_parameter/) | 19.0.1.0.0 | | Override System Parameters from server environment file

Expand Down
2 changes: 1 addition & 1 deletion mail_environment/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Mail configuration with server_environment
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:208c499ec47166945b5e8d261910bdda9e73ca34e19db6506601669ce1f65eba
!! source digest: sha256:540d18be34fa5a40f352c0b236ccc87d37573462d2766adcbbc4996302590a86
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
Expand Down
1 change: 1 addition & 0 deletions mail_environment/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from . import models
from .hooks import uninstall_hook
3 changes: 2 additions & 1 deletion mail_environment/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@

{
"name": "Mail configuration with server_environment",
"version": "19.0.1.0.0",
"version": "19.0.1.1.0",
"category": "Tools",
"summary": "Configure mail servers with server_environment_files",
"author": "Camptocamp, Odoo Community Association (OCA)",
"license": "AGPL-3",
"website": "https://github.com/OCA/server-env",
"depends": ["mail", "server_environment"],
"uninstall_hook": "uninstall_hook",
}
43 changes: 43 additions & 0 deletions mail_environment/hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Copyright 2026 Camptocamp (https://www.camptocamp.com)
from odoo.addons.server_environment.uninstall import restore_env_managed_columns


def uninstall_hook(env):
"""Restore database columns that server.env.mixin dropped for mail models.

When mail_environment is uninstalled, ``ir.mail_server`` and
``fetchmail.server`` would be left without the columns that the ORM
dropped when this addon was first installed. This hook recreates those
columns and repopulates them with the current effective values so the
database remains usable after removal.

After uninstalling this module, an Odoo server restart is required.
Field definitions referencing the compute methods of the mixin persist in memory
until the Python process restarts.
"""
restore_env_managed_columns(
env,
"ir.mail_server",
[
"smtp_host",
"smtp_port",
"smtp_user",
"smtp_pass",
"smtp_encryption",
"smtp_authentication",
],
)
restore_env_managed_columns(
env,
"fetchmail.server",
[
"server",
"port",
"server_type",
"user",
"password",
"is_ssl",
"attach",
"original",
],
)
2 changes: 1 addition & 1 deletion mail_environment/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ <h1>Mail configuration with server_environment</h1>
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:208c499ec47166945b5e8d261910bdda9e73ca34e19db6506601669ce1f65eba
!! source digest: sha256:540d18be34fa5a40f352c0b236ccc87d37573462d2766adcbbc4996302590a86
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/server-env/tree/19.0/mail_environment"><img alt="OCA/server-env" src="https://img.shields.io/badge/github-OCA%2Fserver--env-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/server-env-19-0/server-env-19-0-mail_environment"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/server-env&amp;target_branch=19.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module allows to configure the incoming and outgoing mail servers
Expand Down
20 changes: 20 additions & 0 deletions mail_environment/tests/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from psycopg2 import sql as pg_sql

from odoo.tools import sql

from odoo.addons.server_environment.tests.common import ServerEnvironmentCase


class MailEnvironmentCase(ServerEnvironmentCase):
def _drop_columns(self, model_name, field_names):
"""Drop columns created by restore_env_managed_columns (test cleanup)."""
model = self.env[model_name]
cr = self.env.cr
for field_name in field_names:
if sql.column_exists(cr, model._table, field_name):
cr.execute(
pg_sql.SQL("ALTER TABLE {} DROP COLUMN {}").format(
pg_sql.Identifier(model._table),
pg_sql.Identifier(field_name),
)
)
59 changes: 59 additions & 0 deletions mail_environment/tests/test_incoming_mail.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# Copyright 2018 Camptocamp (https://www.camptocamp.com).
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html)

from odoo.tools import sql

from odoo.addons.mail_environment.tests.common import MailEnvironmentCase
from odoo.addons.server_environment.tests.common import ServerEnvironmentCase
from odoo.addons.server_environment.uninstall import restore_env_managed_columns

fetchmail_config = """
[incoming_mail.fetchmail1]
Expand Down Expand Up @@ -32,6 +35,18 @@
active = 1
"""

_incoming_config = """
[incoming_mail.test_incoming]
server = imap.example.com
port = 993
server_type = imap
is_ssl = 1
user = imap_user
password = imap_pass
attach = 1
original = 0
"""


class TestFetchMailEnvironment(ServerEnvironmentCase):
@classmethod
Expand Down Expand Up @@ -116,3 +131,47 @@ def test_fetchmail_search_server_type(self):
self.fetchmail1,
self.env["fetchmail.server"].search([("server_type", "ilike", "POP")]),
)


class TestRestoreIncomingMailColumns(MailEnvironmentCase):
"""Test restoration of incoming mail columns during uninstall."""

def test_restore_fetchmail_server_columns(self):
"""Incoming mail columns are recreated and populated with config values."""
field_names = [
"server",
"port",
"server_type",
"is_ssl",
"user",
"password",
"attach",
"original",
]
fetchmail = self.env["fetchmail.server"].create({"name": "test_incoming"})
try:
with self.load_config(public=_incoming_config):
restore_env_managed_columns(self.env, "fetchmail.server", field_names)
table = self.env["fetchmail.server"]._table
for field_name in field_names:
self.assertTrue(
sql.column_exists(self.env.cr, table, field_name),
f"Column {field_name} was not created",
)
self.env.cr.execute(
'SELECT server, port, server_type, is_ssl, "user",'
" password, attach, original"
" FROM fetchmail_server WHERE id = %s",
[fetchmail.id],
)
row = self.env.cr.dictfetchone()
self.assertEqual(row["server"], "imap.example.com")
self.assertEqual(row["port"], 993)
self.assertEqual(row["server_type"], "imap")
self.assertTrue(row["is_ssl"])
self.assertEqual(row["user"], "imap_user")
self.assertEqual(row["password"], "imap_pass")
self.assertTrue(row["attach"])
self.assertFalse(row["original"])
finally:
self._drop_columns("fetchmail.server", field_names)
157 changes: 157 additions & 0 deletions mail_environment/tests/test_outgoing_mail.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
from odoo.tools import sql

from odoo.addons.mail_environment.tests.common import MailEnvironmentCase
from odoo.addons.server_environment.tests.common import ServerEnvironmentCase
from odoo.addons.server_environment.uninstall import restore_env_managed_columns

mail_server_config = """
[outgoing_mail]
Expand All @@ -17,6 +21,16 @@
smtp_pass = password2
"""

_outgoing_config = """
[outgoing_mail.test_outgoing]
smtp_host = smtp.example.com
smtp_port = 587
smtp_encryption = starttls
smtp_authentication = login
smtp_user = testuser
smtp_pass = testpass
"""


class TestMailServerEnvironment(ServerEnvironmentCase):
@classmethod
Expand Down Expand Up @@ -97,3 +111,146 @@ def test_mail_server_search_smtp_host(self):
self.mail_server2,
self.env["ir.mail_server"].search([("smtp_host", "ilike", "myserver")]),
)


class TestRestoreOutgoingMailColumns(MailEnvironmentCase):
"""Test restoration of outgoing mail columns during uninstall."""

def test_restore_ir_mail_server_columns(self):
"""Outgoing mail columns are recreated and populated with config values."""
field_names = [
"smtp_host",
"smtp_port",
"smtp_encryption",
"smtp_authentication",
"smtp_user",
"smtp_pass",
]
server = self.env["ir.mail_server"].create({"name": "test_outgoing"})
try:
with self.load_config(public=_outgoing_config):
restore_env_managed_columns(self.env, "ir.mail_server", field_names)
table = self.env["ir.mail_server"]._table
for field_name in field_names:
self.assertTrue(
sql.column_exists(self.env.cr, table, field_name),
f"Column {field_name} was not created",
)
self.env.cr.execute(
"SELECT smtp_host, smtp_port, smtp_encryption,"
" smtp_authentication, smtp_user, smtp_pass"
" FROM ir_mail_server WHERE id = %s",
[server.id],
)
row = self.env.cr.dictfetchone()
self.assertEqual(row["smtp_host"], "smtp.example.com")
self.assertEqual(row["smtp_port"], 587)
self.assertEqual(row["smtp_encryption"], "starttls")
self.assertEqual(row["smtp_authentication"], "login")
self.assertEqual(row["smtp_user"], "testuser")
self.assertEqual(row["smtp_pass"], "testpass")
finally:
self._drop_columns("ir.mail_server", field_names)

def test_restore_ir_mail_server_columns_with_default(self):
"""Columns are populated with default values when no config is loaded."""
field_names = ["smtp_host", "smtp_port"]
# Write via the inverse to set the x_smtp_host_env_default sparse field.
server = self.env["ir.mail_server"].create(
{"name": "test_default_outgoing", "smtp_host": "default.example.com"}
)
try:
# No config loaded — values come from x_smtp_host_env_default.
restore_env_managed_columns(self.env, "ir.mail_server", field_names)
table = self.env["ir.mail_server"]._table
self.assertTrue(sql.column_exists(self.env.cr, table, "smtp_host"))
self.env.cr.execute(
"SELECT smtp_host FROM ir_mail_server WHERE id = %s",
[server.id],
)
self.assertEqual(self.env.cr.fetchone()[0], "default.example.com")
finally:
self._drop_columns("ir.mail_server", field_names)

def test_restore_env_managed_columns_idempotent(self):
"""Calling restore_env_managed_columns twice is safe and idempotent."""
field_names = ["smtp_host", "smtp_port"]
self.env["ir.mail_server"].create({"name": "test_idempotent"})
try:
with self.load_config(public=_outgoing_config):
restore_env_managed_columns(self.env, "ir.mail_server", field_names)
# Second call must not raise or corrupt data.
restore_env_managed_columns(self.env, "ir.mail_server", field_names)
table = self.env["ir.mail_server"]._table
for field_name in field_names:
self.assertTrue(sql.column_exists(self.env.cr, table, field_name))
finally:
self._drop_columns("ir.mail_server", field_names)

def test_restore_env_managed_columns_with_fallback_defaults(self):
"""Fields with no value can be restored with fallback values
via field_defaults."""

field_names = ["smtp_host"]
server = self.env["ir.mail_server"].create({"name": "test_fallback"})
try:
# smtp_host has no config, no ORM default, and no env_default.
# With field_defaults, it should be populated with the fallback value.
restore_env_managed_columns(
self.env,
"ir.mail_server",
field_names,
field_defaults={"smtp_host": "fallback.example.com"},
)
table = self.env["ir.mail_server"]._table
self.assertTrue(sql.column_exists(self.env.cr, table, "smtp_host"))
self.env.cr.execute(
"SELECT smtp_host FROM ir_mail_server WHERE id = %s",
[server.id],
)
self.assertEqual(self.env.cr.fetchone()[0], "fallback.example.com")
finally:
self._drop_columns("ir.mail_server", field_names)

def test_restore_env_managed_columns_no_fallback(self):
"""Fields with no value and no fallback are set to NULL."""

field_names = ["smtp_host"]
server = self.env["ir.mail_server"].create({"name": "test_no_value"})
try:
# smtp_host has no config, no default, and no field_defaults.
# The column should be created and set to NULL.
restore_env_managed_columns(self.env, "ir.mail_server", field_names)
table = self.env["ir.mail_server"]._table
self.assertTrue(sql.column_exists(self.env.cr, table, "smtp_host"))
self.env.cr.execute(
"SELECT smtp_host FROM ir_mail_server WHERE id = %s",
[server.id],
)
self.assertIsNone(self.env.cr.fetchone()[0])
finally:
self._drop_columns("ir.mail_server", field_names)

def test_restore_env_managed_columns_required_field_uses_model_default(self):
"""Fields with no value can be restored from the model default (if available)
when no fallback is provided."""

field_names = ["smtp_authentication"]
server = self.env["ir.mail_server"].create({"name": "test_no_fallback"})
try:
# On supported Odoo versions smtp_authentication has a model default
# ('login'). Restoring without field_defaults should therefore succeed by
# using that effective value.
restore_env_managed_columns(self.env, "ir.mail_server", field_names)
self.env.cr.execute(
"SELECT smtp_authentication FROM ir_mail_server WHERE id = %s",
[server.id],
)
self.assertEqual(self.env.cr.fetchone()[0], "login")
finally:
# Manually drop in case the column was created.
model = self.env["ir.mail_server"]
if sql.column_exists(self.env.cr, model._table, "smtp_authentication"):
self.env.cr.execute( # noqa: S608
f'ALTER TABLE {model._table} DROP COLUMN "smtp_authentication"'
)
Loading