diff --git a/mail_ux/__init__.py b/mail_ux/__init__.py
index d0337769..173091eb 100644
--- a/mail_ux/__init__.py
+++ b/mail_ux/__init__.py
@@ -3,3 +3,4 @@
# directory
##############################################################################
from . import models
+from . import wizard
diff --git a/mail_ux/__manifest__.py b/mail_ux/__manifest__.py
index 0fb49370..2febe4ec 100644
--- a/mail_ux/__manifest__.py
+++ b/mail_ux/__manifest__.py
@@ -36,6 +36,9 @@
"mail",
],
"data": [
+ "security/ir.model.access.csv",
+ "wizard/mail_server_test_wizard_views.xml",
+ "views/ir_mail_server_views.xml",
"views/res_users_views.xml",
],
"demo": [],
diff --git a/mail_ux/models/__init__.py b/mail_ux/models/__init__.py
index dd002ae6..c27049e8 100644
--- a/mail_ux/models/__init__.py
+++ b/mail_ux/models/__init__.py
@@ -2,6 +2,7 @@
# For copyright and license notices, see __manifest__.py file in module root
# directory
##############################################################################
+from . import ir_http
+from . import ir_mail_server
from . import mail_compose_message
from . import res_users
-from . import ir_http
diff --git a/mail_ux/models/ir_mail_server.py b/mail_ux/models/ir_mail_server.py
new file mode 100644
index 00000000..82d27a6d
--- /dev/null
+++ b/mail_ux/models/ir_mail_server.py
@@ -0,0 +1,29 @@
+##############################################################################
+# For copyright and license notices, see __manifest__.py file in module root
+# directory
+##############################################################################
+from odoo import _, models
+
+
+class IrMailServer(models.Model):
+ _inherit = "ir.mail_server"
+
+ def action_send_test_mail(self):
+ """Test the SMTP connection and, if successful, open the test mail wizard.
+
+ Raises the native UserError if the connection test fails so the user
+ sees the standard Odoo connection-error message.
+ """
+ self.ensure_one()
+ # Test connection first; any UserError propagates natively to the UI
+ self.test_smtp_connection()
+ return {
+ "type": "ir.actions.act_window",
+ "name": _("Enviar mail de prueba"),
+ "res_model": "mail.server.test.wizard",
+ "view_mode": "form",
+ "target": "new",
+ "context": {
+ "default_mail_server_id": self.id,
+ },
+ }
diff --git a/mail_ux/security/ir.model.access.csv b/mail_ux/security/ir.model.access.csv
new file mode 100644
index 00000000..1f0ce645
--- /dev/null
+++ b/mail_ux/security/ir.model.access.csv
@@ -0,0 +1,2 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_mail_server_test_wizard_user,mail.server.test.wizard user,model_mail_server_test_wizard,base.group_system,1,1,1,0
diff --git a/mail_ux/views/ir_mail_server_views.xml b/mail_ux/views/ir_mail_server_views.xml
new file mode 100644
index 00000000..f64e7df3
--- /dev/null
+++ b/mail_ux/views/ir_mail_server_views.xml
@@ -0,0 +1,18 @@
+
+
+
+ ir.mail_server.form.send.test
+ ir.mail_server
+
+
+
+
+
+
diff --git a/mail_ux/wizard/__init__.py b/mail_ux/wizard/__init__.py
new file mode 100644
index 00000000..6b7de349
--- /dev/null
+++ b/mail_ux/wizard/__init__.py
@@ -0,0 +1,5 @@
+##############################################################################
+# For copyright and license notices, see __manifest__.py file in module root
+# directory
+##############################################################################
+from . import mail_server_test_wizard
diff --git a/mail_ux/wizard/mail_server_test_wizard.py b/mail_ux/wizard/mail_server_test_wizard.py
new file mode 100644
index 00000000..e31d3f18
--- /dev/null
+++ b/mail_ux/wizard/mail_server_test_wizard.py
@@ -0,0 +1,104 @@
+import smtplib
+import ssl
+from socket import gaierror
+
+from odoo import _, fields, models
+from odoo.exceptions import UserError
+
+
+class MailServerTestWizard(models.TransientModel):
+ _name = "mail.server.test.wizard"
+ _description = "Wizard de prueba de servidor de correo saliente"
+
+ mail_server_id = fields.Many2one(
+ comodel_name="ir.mail_server",
+ string="Servidor de correo saliente",
+ required=True,
+ context={"active_test": False},
+ )
+ email_to = fields.Char(
+ string="Correo destinatario",
+ required=True,
+ )
+
+ def action_send_test_mail(self):
+ """Build and send a test email directly via SMTP, bypassing all
+ Python-level sending guards (server_mode, mail neutralization, etc.).
+
+ The bypass is achieved by calling ``ir.mail_server.connect()`` with
+ ``allow_archived=True`` and then invoking ``smtp.send_message()``
+ directly — never going through ``send_email()``, which is the layer
+ where restrictions such as ``server_mode.allow_send_mail`` operate.
+ """
+ self.ensure_one()
+ IrMailServer = self.env["ir.mail_server"]
+
+ # Verify the user has read access on ir.mail_server before elevating.
+ IrMailServer.check_access_rights("read")
+
+ # Browse with active_test=False so archived servers (neutralized DBs)
+ # are accessible, but without bypassing ACLs / record rules.
+ mail_server = IrMailServer.with_context(active_test=False).browse(self.mail_server_id.id)
+ if not mail_server.exists():
+ raise UserError(
+ _("No se encontró el servidor de correo. " "Por favor, recargue la página e intente de nuevo.")
+ )
+ mail_server.check_access_rule("read")
+ mail_server = mail_server.sudo()
+
+ email_from = mail_server._get_test_email_from()
+ message = IrMailServer.build_email(
+ email_from=email_from,
+ email_to=[self.email_to],
+ subject=_("Prueba de mail desde Odoo"),
+ body=_("Mail de prueba enviado desde mi base de Odoo. " "Por favor no responder."),
+ subtype="plain",
+ )
+
+ smtp = None
+ try:
+ # allow_archived=True is critical for neutralized databases where
+ # all real servers are deactivated by the neutralize process.
+ smtp = IrMailServer.connect(
+ mail_server_id=mail_server.id,
+ allow_archived=True,
+ )
+ if smtp:
+ smtp.send_message(message)
+ except (gaierror, TimeoutError) as e:
+ raise UserError(
+ _(
+ "No se pudo enviar el mail: Sin respuesta del servidor. " "Verifique la dirección y el puerto.\n%s",
+ e,
+ )
+ ) from e
+ except smtplib.SMTPRecipientsRefused as e:
+ raise UserError(
+ _(
+ "No se pudo enviar el mail: El servidor rechazó la dirección destinataria.\n%s",
+ e,
+ )
+ ) from e
+ except smtplib.SMTPException as e:
+ raise UserError(_("No se pudo enviar el mail: %s", e)) from e
+ except ssl.SSLError as e:
+ raise UserError(_("No se pudo enviar el mail: Error de SSL.\n%s", e)) from e
+ except Exception as e:
+ raise UserError(_("No se pudo enviar el mail: %s", e)) from e
+ finally:
+ if smtp:
+ try:
+ smtp.quit()
+ except Exception:
+ pass
+
+ return {
+ "type": "ir.actions.client",
+ "tag": "display_notification",
+ "params": {
+ "message": _("¡Mail de prueba enviado con éxito! " "Por favor, revise su casilla de correo."),
+ "type": "success",
+ "sticky": False,
+ "next": {"type": "ir.actions.act_window_close"},
+ },
+ }
diff --git a/mail_ux/wizard/mail_server_test_wizard_views.xml b/mail_ux/wizard/mail_server_test_wizard_views.xml
new file mode 100644
index 00000000..80e6f337
--- /dev/null
+++ b/mail_ux/wizard/mail_server_test_wizard_views.xml
@@ -0,0 +1,29 @@
+
+
+
+ mail.server.test.wizard.form
+ mail.server.test.wizard
+
+
+
+
+