diff --git a/mail_environment/README.rst b/mail_environment/README.rst index ed0aa41a8..d9afe0be5 100644 --- a/mail_environment/README.rst +++ b/mail_environment/README.rst @@ -1,7 +1,3 @@ -.. image:: https://odoo-community.org/readme-banner-image - :target: https://odoo-community.org/get-involved?utm_source=readme - :alt: Odoo Community Association - ========================================== Mail configuration with server_environment ========================================== @@ -17,7 +13,7 @@ Mail configuration with server_environment .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--env-lightgray.png?logo=github @@ -103,8 +99,8 @@ file. Known issues / Roadmap ====================== -- Due to the special nature of this addon, you cannot test it on the OCA - runbot. +- Due to the special nature of this addon, you cannot test it on the + OCA runbot. Bug Tracker =========== @@ -127,12 +123,12 @@ Authors Contributors ------------ -- Nicolas Bessi -- Yannick Vaucher -- Guewen Baconnier -- Joël Grand-Guillaume -- Holger Brunn -- Alexandre Fayolle +- Nicolas Bessi +- Yannick Vaucher +- Guewen Baconnier +- Joël Grand-Guillaume +- Holger Brunn +- Alexandre Fayolle Maintainers ----------- diff --git a/mail_environment/__init__.py b/mail_environment/__init__.py index 0650744f6..071962a35 100644 --- a/mail_environment/__init__.py +++ b/mail_environment/__init__.py @@ -1 +1,2 @@ from . import models +from .hooks import uninstall_hook diff --git a/mail_environment/__manifest__.py b/mail_environment/__manifest__.py index 7bd19de9c..14a0f7075 100644 --- a/mail_environment/__manifest__.py +++ b/mail_environment/__manifest__.py @@ -10,4 +10,5 @@ "license": "AGPL-3", "website": "https://github.com/OCA/server-env", "depends": ["mail", "server_environment"], + "uninstall_hook": "uninstall_hook", } diff --git a/mail_environment/hooks.py b/mail_environment/hooks.py new file mode 100644 index 000000000..c1236356b --- /dev/null +++ b/mail_environment/hooks.py @@ -0,0 +1,38 @@ +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. + """ + mixin = env["server.env.mixin"] + mixin.restore_env_managed_columns( + "ir.mail_server", + [ + "smtp_host", + "smtp_port", + "smtp_user", + "smtp_pass", + "smtp_encryption", + "smtp_authentication", + ], + ) + mixin.restore_env_managed_columns( + "fetchmail.server", + [ + "server", + "port", + "server_type", + "user", + "password", + "is_ssl", + "attach", + "original", + ], + ) diff --git a/mail_environment/static/description/index.html b/mail_environment/static/description/index.html index be52611f6..d2571219a 100644 --- a/mail_environment/static/description/index.html +++ b/mail_environment/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +Mail configuration with server_environment -
+
+

Mail configuration with server_environment

- - -Odoo Community Association - -
-

Mail configuration with server_environment

-

Beta License: AGPL-3 OCA/server-env Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/server-env Translate me on Weblate Try me on Runboat

This module allows to configure the incoming and outgoing mail servers using the server_environment mechanism: you can then have different mail servers for the production and the test environment.

@@ -395,12 +390,12 @@

Mail configuration with server_environment

-

Installation

+

Installation

To install this module, you need to have the server_environment module installed and properly configured.

-

Configuration

+

Configuration

With this module installed, the incoming and outgoing mail servers are configured in the server_environment_files module (which is a module you should provide, see the documentation of server_environment for more @@ -440,20 +435,20 @@

Configuration

mail server with the field name set to “odoo_pop_mail1”.

-

Usage

+

Usage

Once configured, Odoo will read the mail servers values from the configuration file related to each environment defined in the main Odoo file.

-

Known issues / Roadmap

+

Known issues / Roadmap

    -
  • Due to the special nature of this addon, you cannot test it on the OCA -runbot.
  • +
  • Due to the special nature of this addon, you cannot test it on the +OCA runbot.
-

Bug Tracker

+

Bug Tracker

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -461,15 +456,15 @@

Bug Tracker

Do not contact contributors directly about support or help with technical issues.

-

Credits

+

Credits

-

Authors

+

Authors

  • Camptocamp
-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -493,6 +488,5 @@

Maintainers

-
diff --git a/mail_environment/tests/test_mail_environment.py b/mail_environment/tests/test_mail_environment.py index 44c396b2c..ea0f4eaed 100644 --- a/mail_environment/tests/test_mail_environment.py +++ b/mail_environment/tests/test_mail_environment.py @@ -2,6 +2,8 @@ # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) +from odoo.tools import sql + from odoo.addons.server_environment.tests.common import ServerEnvironmentCase fetchmail_config = """ @@ -75,3 +77,231 @@ def test_fetchmail_search_is_ssl(self): fetchmail2, self.env["fetchmail.server"].search([("is_ssl", "!=", True)]), ) + + +_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 +""" + +_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 TestRestoreEnvManagedColumns(ServerEnvironmentCase): + """Test column restoration performed by the uninstall hook.""" + + 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( # noqa: S608 + f'ALTER TABLE {model._table} DROP COLUMN "{field_name}"' + ) + + 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): + self.env["server.env.mixin"].restore_env_managed_columns( + "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. + self.env["server.env.mixin"].restore_env_managed_columns( + "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_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): + self.env["server.env.mixin"].restore_env_managed_columns( + "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) + + 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): + mixin = self.env["server.env.mixin"] + mixin.restore_env_managed_columns("ir.mail_server", field_names) + # Second call must not raise or corrupt data. + mixin.restore_env_managed_columns("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. + self.env["server.env.mixin"].restore_env_managed_columns( + "ir.mail_server", + field_names, + field_defaults={"smtp_host": "fallback.example.com"}, + ) + table = self.env["ir.mail_server"]._table + column_exists = sql.column_exists(self.env.cr, table, "smtp_host") + self.assertTrue(column_exists) + 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. + self.env["server.env.mixin"].restore_env_managed_columns( + "ir.mail_server", field_names + ) + table = self.env["ir.mail_server"]._table + column_exists = sql.column_exists(self.env.cr, table, "smtp_host") + self.assertTrue(column_exists) + 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. + self.env["server.env.mixin"].restore_env_managed_columns( + "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"' + ) diff --git a/test-requirements.txt b/test-requirements.txt index 4ad8e0ece..f432fe7f2 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1 +1,2 @@ odoo-test-helper +odoo-addon-server_environment @ git+https://github.com/OCA/server-env@refs/pull/261/head#subdirectory=server_environment