diff --git a/config/routes.php b/config/routes.php
index 76f360b57..75dea7db1 100644
--- a/config/routes.php
+++ b/config/routes.php
@@ -535,6 +535,13 @@
['_name' => 'lock:remove'],
);
+ // SendMail
+ $routes->connect(
+ '/sendmail',
+ ['controller' => 'SendMail', 'action' => 'index'],
+ ['_name' => 'sendmail:index'],
+ );
+
// Session
$routes->get(
'/session/{name}',
diff --git a/locales/default.pot b/locales/default.pot
index 9199b4de5..2d3c04601 100644
--- a/locales/default.pot
+++ b/locales/default.pot
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Project-Id-Version: BEdita 4 \n"
-"POT-Creation-Date: 2025-08-26 08:17:45 \n"
+"POT-Creation-Date: 2025-10-29 11:08:16 \n"
"MIME-Version: 1.0 \n"
"Content-Transfer-Encoding: 8bit \n"
"Language-Team: BEdita I18N & I10N Team \n"
@@ -1765,6 +1765,9 @@ msgstr ""
msgid "View original"
msgstr ""
+msgid "Send"
+msgstr ""
+
msgid "Address"
msgstr ""
diff --git a/locales/en_US/default.po b/locales/en_US/default.po
index 4fea704f5..04a352a16 100644
--- a/locales/en_US/default.po
+++ b/locales/en_US/default.po
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Project-Id-Version: BEdita Manager \n"
-"POT-Creation-Date: 2025-08-26 08:17:45 \n"
+"POT-Creation-Date: 2025-10-29 11:08:16 \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: BEdita I18N & I10N Team \n"
@@ -1768,6 +1768,9 @@ msgstr ""
msgid "View original"
msgstr ""
+msgid "Send"
+msgstr ""
+
msgid "Address"
msgstr ""
diff --git a/locales/it_IT/default.po b/locales/it_IT/default.po
index 9ab7d401b..bf1b9d702 100644
--- a/locales/it_IT/default.po
+++ b/locales/it_IT/default.po
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Project-Id-Version: BEdita Manager \n"
-"POT-Creation-Date: 2025-08-26 08:17:45 \n"
+"POT-Creation-Date: 2025-10-29 11:08:16 \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: BEdita I18N & I10N Team \n"
@@ -1791,6 +1791,9 @@ msgstr ""
msgid "View original"
msgstr "Vedi originale"
+msgid "Send"
+msgstr "Invia"
+
msgid "Address"
msgstr "Indirizzo"
diff --git a/resources/js/app/app.js b/resources/js/app/app.js
index 746b02120..23488317e 100644
--- a/resources/js/app/app.js
+++ b/resources/js/app/app.js
@@ -117,6 +117,7 @@ const _vueInstance = new Vue({
AddRelatedById: () => import(/* webpackChunkName: "add-related-by-id" */'app/components/add-related-by-id/add-related-by-id'),
UploadedObject: () => import(/* webpackChunkName: "uploaded-object" */'app/components/uploaded-object/uploaded-object.vue'),
RibbonItem: () => import(/* webpackChunkName: "ribbon-item" */'./components/ribbon-item/ribbon-item.vue'),
+ MailPreview: () => import(/* webpackChunkName: "mail-preview" */'./components/mail-preview/mail-preview.vue'),
AppIcon,
},
diff --git a/resources/js/app/components/mail-preview/mail-preview.vue b/resources/js/app/components/mail-preview/mail-preview.vue
new file mode 100644
index 000000000..32f41e8b6
--- /dev/null
+++ b/resources/js/app/components/mail-preview/mail-preview.vue
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/js/app/components/property-view/property-view.js b/resources/js/app/components/property-view/property-view.js
index 101f1c6d6..ce61517e4 100644
--- a/resources/js/app/components/property-view/property-view.js
+++ b/resources/js/app/components/property-view/property-view.js
@@ -48,6 +48,7 @@ export default {
ObjectCategories: () => import(/* webpackChunkName: "object-categories" */'app/components/object-categories/object-categories'),
PlaceholderList: () => import(/* webpackChunkName: "placeholder-list" */'app/components/placeholder-list/placeholder-list'),
MediaItem: () => import(/* webpackChunkName: "media-item" */'app/components/media-item/media-item'),
+ MailPreview: () => import(/* webpackChunkName: "mail-preview" */'app/components/mail-preview/mail-preview.vue'),
},
props: {
diff --git a/resources/js/app/pages/modules/view.vue b/resources/js/app/pages/modules/view.vue
index a76daee3b..6695d1e6a 100644
--- a/resources/js/app/pages/modules/view.vue
+++ b/resources/js/app/pages/modules/view.vue
@@ -16,6 +16,7 @@ export default {
KeyValueList: () => import(/* webpackChunkName: "key-value-list" */'app/components/json-fields/key-value-list'),
StringList: () => import(/* webpackChunkName: "string-list" */'app/components/json-fields/string-list'),
LanguageSelector:() => import(/* webpackChunkName: "language-selector" */'app/components/language-selector/language-selector'),
+ MailPreview: () => import(/* webpackChunkName: "mail-preview" */'app/components/mail-preview/mail-preview.vue'),
},
props: {
diff --git a/src/Controller/SendMailController.php b/src/Controller/SendMailController.php
new file mode 100644
index 000000000..2abdd2ba7
--- /dev/null
+++ b/src/Controller/SendMailController.php
@@ -0,0 +1,48 @@
+FormProtection->setConfig('unlockedActions', ['index']);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function index(): void
+ {
+ $this->getRequest()->allowMethod(['post']);
+ $this->viewBuilder()->setClassName('Json');
+ try {
+ $payload = $this->getRequest()->getData();
+ foreach ($payload['data'] as $key => $value) {
+ if (strpos($key, '.') !== false) {
+ unset($payload['data'][$key]);
+ $payload['data'] = Hash::insert($payload['data'], $key, $value);
+ }
+ }
+ ApiClientProvider::getApiClient()->post('/placeholders/send', (string)json_encode($payload));
+ $response = ['message' => 'Email sent successfully'];
+ $this->set('response', $response);
+ $this->setSerialize(['response']);
+ } catch (Exception $e) {
+ $this->set('error', $e->getMessage());
+ $this->setSerialize(['error']);
+ }
+ }
+}
diff --git a/templates/Element/Form/mail_preview.twig b/templates/Element/Form/mail_preview.twig
new file mode 100644
index 000000000..080d29233
--- /dev/null
+++ b/templates/Element/Form/mail_preview.twig
@@ -0,0 +1,2 @@
+
+
diff --git a/tests/TestCase/Controller/SendMailControllerTest.php b/tests/TestCase/Controller/SendMailControllerTest.php
new file mode 100644
index 000000000..78c11aef0
--- /dev/null
+++ b/tests/TestCase/Controller/SendMailControllerTest.php
@@ -0,0 +1,60 @@
+ [
+ 'REQUEST_METHOD' => 'POST',
+ ],
+ 'post' => [
+ 'data' => [
+ 'user.name' => 'John',
+ 'user.surname' => 'Doe',
+ ],
+ ],
+ ];
+ $request = new ServerRequest($config);
+ $controller = new SendMailController($request);
+ $controller->index();
+ $expected = '[404] Not Found';
+ $actual = $controller->viewBuilder()->getVar('error');
+ static::assertEquals($expected, $actual);
+
+ // mock /placeholders/send to simulate success response
+ $safeClient = ApiClientProvider::getApiClient();
+ $apiClient = $this->getMockBuilder(BEditaClient::class)
+ ->setConstructorArgs(['https://api.example.org'])
+ ->getMock();
+ $apiClient->method('post')
+ ->willReturn(null);
+ ApiClientProvider::setApiClient($apiClient);
+ $controller->index();
+ $expected = 'Email sent successfully';
+ $actual = $controller->viewBuilder()->getVar('response')['message'];
+ static::assertEquals($expected, $actual);
+ ApiClientProvider::setApiClient($safeClient);
+ }
+}