[TikiWiki-commits] [Git][tikiwiki/tiki][master] [NEW] Handle mail queue warning and Create admin page for manage mail queues

"SoftStart Code \(@softstartcode\) via TikiWiki-cvs" <[email protected]>
Newsgroups gmane.comp.cms.tiki.cvs
Message-ID <68c748902f101_2cde6d4551c@gitlab-sidekiq-low-urgency-cpu-bound-v2-669574cd86-lx69c.mail>

SoftStart Code pushed to branch master at Tiki Wiki CMS Groupware / Tiki


Commits:
930da0e7 by Sandeep D at 2025-09-14T22:50:11+00:00
[NEW] Handle mail queue warning and Create admin page for manage mail queues
---
* [NEW] Handle mail queue warning and Create admin page for manage mail queues

(1) Create a new preference “Number of retries to send emails when there are errors“ mailer_queue_max_retries with a default value of 10.
(2) Update the code that picks up emails from the email queue to ignore emails with a retry count bigger than the value in the setting.
(3) A table with the list of messages in the queue
(4) A summary of the current mail queue status

See merge request tikiwiki/tiki!8549

- - - - -


9 changed files:

- + lib/EmailQueue/EmailQueueLib.php
- lib/core/Tiki/Command/MailQueueSendCommand.php
- lib/prefs/mailer.php
- lib/webmail/tikimaillib.php
- templates/admin/include_general.tpl
- templates/tiki-admin.tpl
- + templates/tiki-admin_email_queue.tpl
- tiki-admin.php
- + tiki-admin_email_queue.php


Changes:

=====================================
lib/EmailQueue/EmailQueueLib.php
=====================================
@@ -0,0 +1,60 @@
+<?php
+
+// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
+//
+// All Rights Reserved. See copyright.txt for details and a complete list of authors.
+// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
+namespace Tiki\Lib\EmailQueue;
+
+use TikiLib;
+
+class EmailQueueLib
+{
+    public function listStalledEmailQueues($offset, $maxRecords, $sort_mode, $find)
+    {
+        $tikilib = TikiLib::lib('tiki');
+
+        if ($find) {
+            $mid = ' WHERE (`attempts` > ?)';
+            $bindvars = [$find];
+        } else {
+            $mid = '';
+            $bindvars = [];
+        }
+
+        $query = 'SELECT * FROM `tiki_mail_queue` ' . $mid . ' ORDER BY ' . $tikilib->convertSortMode($sort_mode);
+        $query_cant = 'SELECT COUNT(*) FROM `tiki_mail_queue` ' . $mid;
+        $total_cant = 'SELECT COUNT(*) FROM `tiki_mail_queue`';
+        $result = $tikilib->query($query, $bindvars, $maxRecords, $offset);
+        $cant = $tikilib->getOne($query_cant, $bindvars);
+        $total_cant = $tikilib->getOne($total_cant, []);
+        $ret = [];
+
+        while ($res = $result->fetchRow()) {
+            $ret[] = $res;
+        }
+
+        $retval = [];
+        $retval['data'] = $ret;
+        $retval['total_cant'] = $total_cant;
+        $retval['max_retries'] = $find;
+        $retval['cant'] = $cant;
+        return $retval;
+    }
+
+    public function resetAttemptsOfEmailQueue($queueId)
+    {
+        $tikilib = TikiLib::lib('tiki');
+
+        $query = 'UPDATE `tiki_mail_queue` SET `attempts`= 0 WHERE `messageId` = ? ';
+        return $tikilib->query($query, $queueId);
+    }
+
+    public function deleteEmailQueue($queueId)
+    {
+        $tikilib = TikiLib::lib('tiki');
+
+        $query = 'DELETE FROM `tiki_mail_queue` WHERE `messageId` = ? ';
+        return $tikilib->query($query, $queueId);
+    }
+}


=====================================
lib/core/Tiki/Command/MailQueueSendCommand.php
=====================================
@@ -30,12 +30,13 @@ class MailQueueSendCommand extends Command
 
         $output->writeln('Mail queue processor starting...');
 
-        $messages = \TikiDb::get()->fetchAll('SELECT messageId, message FROM tiki_mail_queue');
+        $where = ! empty($prefs['mailer_queue_max_retries']) ? 'where `attempts` <= ' . $prefs['mailer_queue_max_retries'] : '';
+        $messages = \TikiDb::get()->fetchAll('SELECT messageId, message FROM tiki_mail_queue ' . $where);
 
         foreach ($messages as $message) {
             $messageId = $message['messageId'];
             $serializedEmail = $message['message'];
-            $output->writeln('Sending message ' . $messageId . '...');
+            $output->writeln('Sending message ID: ' . $messageId . '...');
             $mail = unserialize($serializedEmail);
             $error = '';
             if ($mail instanceof Email) {


=====================================
lib/prefs/mailer.php
=====================================
@@ -198,5 +198,12 @@ function prefs_mailer_list()
             'default' => '',
             'keywords' => 'catchall',
         ],
+        'mailer_queue_max_retries' => [
+            'name'        => tra('Number of retries to send emails when there are errors'),
+            'description' => tra('Email queue process will not pick the email to retry after this number of attempts'),
+            'type'        => 'text',
+            'default'     => '10',
+            'units'       => tra('attempts'),
+        ],
     ];
 }


=====================================
lib/webmail/tikimaillib.php
=====================================
@@ -247,7 +247,7 @@ class TikiMail
             }
         }
 
-        if ($prefs['mailer_queue'] == 'y') {
+        if ($prefs['mailer_queue'] == 'y' || $prefs['mailer_handler'] === 'smtp') {
             $query = "INSERT INTO `tiki_mail_queue` (message) VALUES (?)";
             $bindvars = [serialize($this->mail)];
             $tikilib->query($query, $bindvars, -1, 0);


=====================================
templates/admin/include_general.tpl
=====================================
@@ -73,6 +73,7 @@
                     {preference name=http_sslverifypeer}
                     {preference name=mailer_smtp_helo}
                 </div>
+                {preference name=mailer_queue_max_retries}
                 {preference name=mailer_queue}
                 {preference name=mailer_redirect}
                 <div class="adminoptionbox mb-3 row clearfix">


=====================================
templates/tiki-admin.tpl
=====================================
@@ -52,6 +52,14 @@
                 {breadcrumbs type="pagetitle" loc="page" crumbs=$crumbs}
             {/if}
             *}
+            {if $mailer_queue_requires_update}
+                {remarksbox type="error" title="{tr}Email Queue Problem{/tr}"}
+                    <p>{tr}There are messages in the email queue that exceeded the number of retries and will not be retried anymore.{/tr}</p>
+                    <p>{tr}You can manage the queue.{/tr}
+                        {tr _0='<a class="alert-link" href="tiki-admin_email_queue.php">' _1="</a>"}%0Manage the queue.%1{/tr}
+                    </p>
+                {/remarksbox}
+            {/if}
             {if $ProblemsLoadingCacheSubSystem}
                 {remarksbox type="warning" title="{tr _0=$ProblemsLoadingCacheSubSystem}%0 did not load properly{/tr}"}
                 {tr _0=$ProblemsLoadingCacheSubSystem}Tiki is configured to use <strong>%0 cache system</strong>, however, it was not loaded properly. Check if the extension has been properly loaded.{/tr}<br />


=====================================
templates/tiki-admin_email_queue.tpl
=====================================
@@ -0,0 +1,86 @@
+{* $Id$ *}
+{title help="Mail queues"}{tr}Mail queues{/tr}{/title}
+
+{if empty($prefs.sender_email)}
+    {remarksbox type="warning" title="{tr}Warning{/tr}"}
+        {tr}You need to set <a class="alert-link" href="tiki-admin.php?page=general">Sender Email</a> before creating email queue{/tr}.
+    {/remarksbox}
+{/if}
+
+<br>
+<h2>{tr}Summary of the current mail queue status{/tr}</h2>
+<p>{tr}Total messages in queue: {/tr}<strong>{$total_cant}</strong></p>
+<p>{tr}Total messages stalled: {/tr}<strong>{$cant}</strong></p>
+<p>{tr}Current value for max retries in case of error: {/tr}<strong>{$max_retries}</strong></p>
+<h2>{tr}Mail queues{/tr}</h2>
+
+<form method="get" action="tiki-admin_email_queue.php">
+    {ticket}
+    <div class="table-responsive email-queue-table">
+        <table class="table table-striped table-hover">
+            <tr>
+                <th>
+                    {if $mailQueues}
+                        {select_all label='Select All' checkbox_names='checked[]'}
+                    {/if}
+                </th>
+                <th>{tr}Date{/tr}</th>
+                <th>{tr}Destination{/tr}</th>
+                <th>{tr}Subject{/tr}</th>
+                <th>{tr}Message{/tr}</th>
+                <th></th>
+            </tr>
+            {section name=queue loop=$mailQueues}
+                <tr>
+                    <td class="checkbox-cell">
+                        <div class="form-check">
+                            <input type="checkbox" name="checked[]" value="{$mailQueues[queue].messageId|escape}">
+                        </div>
+                    </td>
+                    <td class="text">{$mailQueues[queue].date|escape}</td>
+                    <td class="text">{$mailQueues[queue].destination|escape}</td>
+                    <td class="email">{$mailQueues[queue].subject|escape}</td>
+                    <td class="text">{$mailQueues[queue].body|truncate:500|escape}</td>
+                    <td class="actions">
+                        {actions}
+                            {strip}
+                                <action>
+                                    <a href="{$smarty.server.SCRIPT_NAME}?{query redeliver={$mailQueues[queue].messageId}}" onclick="confirmPopup('{tr}Redeliver mail queue?{/tr}', '{ticket mode=get}')">
+                                        {icon name='undo' _menu_text='y' _menu_icon='y' alt="{tr}Redeliver{/tr}"}
+                                    </a>
+                                </action>
+                                <action>
+                                    <a href="{$smarty.server.SCRIPT_NAME}?{query remove={$mailQueues[queue].messageId}}" onclick="confirmPopup('{tr}Delete mail queue?{/tr}', '{ticket mode=get}')">
+                                        {icon name='remove' _menu_text='y' _menu_icon='y' alt="{tr}Delete{/tr}"}
+                                    </a>
+                                </action>
+                            {/strip}
+                        {/actions}
+                    </td>
+                </tr>
+            {sectionelse}
+                {norecords _colspan=6}
+            {/section}
+        </table>
+    </div>
+    {if $mailQueues}
+        <div class="input-group col-sm-8">
+            <select class="form-select" name="action">
+                <option value="" selected="selected">
+                    {tr}Select action to perform with checked{/tr}...
+                </option>
+                <option value="redeliver" class="confirm-popup" data-confirm-text="{tr}Redeliver selected queues?{/tr}">
+                    {tr}Redeliver{/tr}
+                </option>
+                <option value="delete" class="confirm-popup" data-confirm-text="{tr}Delete selected queues?{/tr}">
+                    {tr}Delete{/tr}
+                </option>
+            </select>
+            <button type="submit" class="btn btn-primary" onclick="confirmPopup()">
+                {tr}OK{/tr}
+            </button>
+        </div>
+    {/if}
+</form>
+
+{pagination_links cant=$cant step=$maxRecords offset=$offset}{/pagination_links}


=====================================
tiki-admin.php
=====================================
@@ -440,6 +440,16 @@ $smarty->assign('missing_patches', $installer->missingPatches());
 $smarty->assign('installer_not_locked', $installer->checkInstallerLocked());
 $smarty->assign('db_engine_type', getCurrentEngine());
 
+if (empty($_GET)) {
+    $where = ! empty($prefs['mailer_queue_max_retries']) ? 'where attempts > ' . $prefs['mailer_queue_max_retries'] : '';
+    if (! empty($where)) {
+        $maxRetries = count(\TikiDb::get()->fetchAll('SELECT messageId, message FROM tiki_mail_queue ' . $where));
+        if ($maxRetries) {
+            $smarty->assign('mailer_queue_requires_update', $maxRetries);
+        }
+    }
+}
+
 $composerWrapper = new ComposerCli($tikipath);
 $smarty->assign('composer_options_diff', $composerWrapper->compareDistRequiredOptions());
 


=====================================
tiki-admin_email_queue.php
=====================================
@@ -0,0 +1,163 @@
+<?php
+
+// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
+//
+// All Rights Reserved. See copyright.txt for details and a complete list of authors.
+// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
+use Tiki\Lib\EmailQueue\EmailQueueLib;
+
+$inputConfiguration = [
+    [
+        'staticKeyFilters' => [
+            'offset' => 'digits',
+            'maxRecords' => 'digits',
+            'removeevent' => 'digits',
+            'find' => 'striptags',
+        ],
+        'staticKeyFiltersForArrays' => [
+            'checked' => 'alnum',
+        ],
+    ]
+];
+// Initialization
+require_once('tiki-setup.php');
+$access->check_permission(['tiki_p_admin']);
+
+$emailQueuelib = new EmailQueueLib();
+
+$auto_query_args = [
+    'offset',
+    'sort_mode',
+    'find',
+    'maxRecords'
+];
+
+if (! empty($_REQUEST['remove']) && $access->checkCsrf(true)) {
+    $result = $emailQueuelib->deleteEmailQueue($_REQUEST['remove']);
+    if ($result && $result->numRows()) {
+        Feedback::success(tr('Mail queue event deleted'));
+    } else {
+        Feedback::error(tr('Mail queue event not deleted'));
+    }
+}
+if (! empty($_REQUEST['redeliver']) && $access->checkCsrf(true)) {
+    $result = $emailQueuelib->resetAttemptsOfEmailQueue($_REQUEST['redeliver']);
+    if ($result && $result->numRows()) {
+        Feedback::success(tr('One mail queue has been scheduled to redeliver.'));
+    } else {
+        Feedback::error(tr('One mail queue has not been scheduled to redeliver.'));
+    }
+}
+
+if (
+    isset($_REQUEST['action'])
+    && $_REQUEST['action'] === 'redeliver'
+    && isset($_REQUEST['checked'])
+    && $access->checkCsrf(true)
+) {
+    $i = 0;
+    foreach ($_REQUEST['checked'] as $id) {
+        $result = $emailQueuelib->resetAttemptsOfEmailQueue($id);
+        if ($result && $result->numRows()) {
+            $i++;
+        }
+    }
+    $checkedCount = count($_REQUEST['checked']);
+    if ($checkedCount === $i) {
+        $msg = $i == 1 ? tr('One mail queue has been scheduled to redeliver.') : tr('%0 mail queues has been scheduled to redeliver.', $i);
+        Feedback::success($msg);
+    } elseif ($i < $checkedCount) {
+        Feedback::error(tr('%0 of %1 selected mail queue has been scheduled to redeliver', $i, $checkedCount));
+    }
+}
+
+if (
+    isset($_REQUEST['action'])
+    && $_REQUEST['action'] === 'delete'
+    && isset($_REQUEST['checked'])
+    && $access->checkCsrf(true)
+) {
+    $i = 0;
+    foreach ($_REQUEST['checked'] as $id) {
+        $result = $emailQueuelib->deleteEmailQueue($id);
+        if ($result && $result->numRows()) {
+            $i++;
+        }
+    }
+    $checkedCount = count($_REQUEST['checked']);
+    if ($checkedCount === $i) {
+        $msg = $i == 1 ? tr('One mail queue events deleted') : tr('%0 mail queues deleted', $i);
+        Feedback::success($msg);
+    } elseif ($i < $checkedCount) {
+        Feedback::error(tr('%0 of %1 selected mail queue deleted', $i, $checkedCount));
+    }
+}
+
+if (! isset($_REQUEST['offset'])) {
+    $offset = 0;
+} else {
+    $offset = $_REQUEST['offset'];
+}
+$smarty->assign_by_ref('offset', $offset);
+if (! empty($prefs['zend_email_queue_max_retries'])) {
+    $find = $prefs['zend_email_queue_max_retries'];
+} else {
+    $find = '';
+}
+$smarty->assign_by_ref('find', $find);
+if (! empty($_REQUEST['maxRecords'])) {
+    $maxRecords = $_REQUEST['maxRecords'];
+} else {
+    $maxRecords = 100;
+}
+$sort_mode = 'messageId_desc';
+$smarty->assign_by_ref('maxRecords', $maxRecords);
+$mailQueues = $emailQueuelib->listStalledEmailQueues($offset, $maxRecords, $sort_mode, $find);
+$smarty->assign_by_ref('cant', $mailQueues['cant']);
+$smarty->assign_by_ref('total_cant', $mailQueues['total_cant']);
+$smarty->assign_by_ref('max_retries', $mailQueues['max_retries']);
+
+foreach ($mailQueues['data'] as $key => $message) {
+    try {
+        $mail = unserialize($message['message']);
+        $headers = $mail->getHeaders();
+        $date = '';
+        $subject = '';
+        $destinations = [];
+        foreach ($mail->getTo() as $destination) {
+            $destinations[] = $destination->getAddress();
+        }
+        if ($headers->has('date')) {
+            $date = date('Y-m-d H:i:s', strtotime($headers->get('date')->getFieldValue()));
+        }
+        if ($headers->has('subject')) {
+            $subject = $mail->getSubject();
+        }
+
+        // Ensure body is a string for template truncate modifier
+        $textBody = '';
+        $htmlBody = '';
+        if (method_exists($mail, 'getTextBody')) {
+            $textBody = $mail->getTextBody() ?: '';
+        }
+        if (method_exists($mail, 'getHtmlBody')) {
+            $htmlBody = ! empty($mail->getHtmlBody()) ? trim(strip_tags($mail->getHtmlBody())) : '';
+        }
+        $message['date'] = $date;
+        $message['destination'] = implode(',', $destinations);
+        $message['subject'] = $subject;
+        $message['body'] = $textBody . $htmlBody;
+        $mailQueues['data'][$key] = $message;
+    } catch (Exception $e) {
+        Feedback::error($e->getMessage(), $i, $checkedCount);
+    }
+}
+
+$smarty->assign_by_ref('mailQueues', $mailQueues['data']);
+
+// disallow robots to index page:
+$smarty->assign('metatag_robots', 'NOINDEX, NOFOLLOW');
+
+// Display the template
+$smarty->assign('mid', 'tiki-admin_email_queue.tpl');
+$smarty->display('tiki.tpl');



View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/930da0e75fc7ccabd5149811ab7dd1d63281f849

-- 
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/930da0e75fc7ccabd5149811ab7dd1d63281f849
You're receiving this email because of your account on gitlab.com.

_______________________________________________
TikiWiki-cvs mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/tikiwiki-cvs
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.