Comment: | Improve email queue handling of recipients |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | dev |
Files: | files | file ages | folders |
SHA3-256: |
d9083efc3cee500c4585a81c1d95c1b7 |
User & Date: | bohwaz on 2023-05-09 19:24:00 |
Other Links: | branch diff | manifest | tags |
2023-05-10
| ||
12:46 | Fix file_path in module edit check-in: 4360ea6d42 user: bohwaz tags: dev | |
2023-05-09
| ||
19:24 | Improve email queue handling of recipients check-in: d9083efc3c user: bohwaz tags: dev | |
19:23 | Fix small UI issues check-in: a3d0d461d5 user: bohwaz tags: dev | |
Modified src/include/lib/Garradin/Email/Emails.php from [8c3a005ece] to [426d26c419].
︙ | ︙ | |||
39 40 41 42 43 44 45 | * When we reach that number of fails, the address is treated as permanently invalid, unless reset by a verification. */ const FAIL_LIMIT = 5; /** * Add a message to the sending queue using templates * @param int $context | | > > | > > | > > > > > > > | > | < | | > > | < | < | < | | | > > | | | | | > | > | | | 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | * When we reach that number of fails, the address is treated as permanently invalid, unless reset by a verification. */ const FAIL_LIMIT = 5; /** * Add a message to the sending queue using templates * @param int $context * @param array $recipients List of recipients, this accepts a wide range of types: * - a single e-mail address * - array of e-mail addresses as values ['a@b.c', 'd@e.f'] * - array of user entities * - array where each key is the email address, and the value is an array or a \stdClass containing * pgp_key, data and user items * @param string $sender * @param string $subject * @param UserTemplate|string $content * @return void */ static public function queue(int $context, $recipients, ?string $sender, string $subject, $content, ?string $render = null): void { if (DISABLE_EMAIL) { return; } if (is_string($recipients)) { $recipients = [$recipients]; } elseif (!is_iterable($recipients)) { throw new \InvalidArgumentException('Invalid recipients argument'); } $list = []; // Build email list foreach ($recipients as $key => $r) { $data = []; $emails = []; $user = null; $pgp_key = null; if (is_array($r)) { $user = $r['user'] ?? null; $data = $r['data'] ?? null; $pgp_key = $r['pgp_key'] ?? null; } elseif (is_object($r) && $r instanceof User) { $user = $r; $data = $r->asArray(); $pgp_key = $user->pgp_key ?? null; } elseif (is_object($r)) { $user = $r->user ?? null; $data = $r->data ?? null; $pgp_key = $user->pgp_key ?? ($r->pgp_key ?? null); } // Get e-mail address from key if (is_string($key) && false !== strpos($key, '@')) { $emails[] = $key; } // Get e-mail address from value elseif (is_string($r) && false !== strpos($r, '@')) { $emails[] = $r; } // Get email list from user object elseif ($user) { $emails = $user->getEmails(); } else { // E-mail not found continue; } // Filter out invalid addresses foreach ($emails as $key => $value) { if (!preg_match('/.+@.+\..+$/', $value)) { unset($emails[$key]); } } if (!count($emails)) { continue; } $data = compact('user', 'data', 'pgp_key'); foreach ($emails as $value) { $list[$value] = $data; } } if (!count($list)) { |
︙ | ︙ | |||
137 138 139 140 141 142 143 | $st = $db->prepare('INSERT INTO emails_queue (sender, subject, recipient, recipient_hash, recipient_pgp_key, content, content_html, context) VALUES (:sender, :subject, :recipient, :recipient_hash, :recipient_pgp_key, :content, :content_html, :context);'); if ($render) { $main_tpl = new UserTemplate('email.html'); } | | | | > | > | | > > > | | | | 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | $st = $db->prepare('INSERT INTO emails_queue (sender, subject, recipient, recipient_hash, recipient_pgp_key, content, content_html, context) VALUES (:sender, :subject, :recipient, :recipient_hash, :recipient_pgp_key, :content, :content_html, :context);'); if ($render) { $main_tpl = new UserTemplate('email.html'); } foreach ($recipients as $address => $recipient) { $data = $recipient['data']; // We won't try to reject invalid/optout recipients here, // it's done in the queue clearing (more efficient) $hash = Email::getHash($address); $content_html = null; // Replace placeholders: {{$name}}, etc. if ($template) { $template->assignArray((array) $data, null, false); // Disable HTML escaping for plaintext emails $template->setEscapeDefault(null); $content = $template->fetch(); if ($render) { $content_html = $template->fetch(); } } // Add Markdown rendering if ($render) { $content_html = Render::render($render, null, $content_html ?? $content); } if ($content_html) { // Wrap HTML content in the email skeleton $main_tpl->assignArray([ 'html' => $content_html, 'address' => $address, 'data' => $data, 'context' => $context, 'from' => $sender, ]); $content_html = $main_tpl->fetch(); } $recipient['email'] = $address; if (Plugins::fireSignal('email.queue.insert', compact('context', 'recipient', 'sender', 'subject', 'content', 'render', 'hash', 'content_html'))) { // queue insert was done by a plugin continue; } $st->bindValue(':sender', $sender); $st->bindValue(':subject', $subject); $st->bindValue(':context', $context); $st->bindValue(':recipient', $address); $st->bindValue(':recipient_pgp_key', $recipient['pgp_key']); $st->bindValue(':recipient_hash', $hash); $st->bindValue(':content', $content); $st->bindValue(':content_html', $content_html); $st->execute(); $st->reset(); $st->clear(); |
︙ | ︙ |
Modified src/include/lib/Garradin/Email/Templates.php from [b8273f469e] to [235d520659].
︙ | ︙ | |||
20 21 22 23 24 25 26 | $body = trim($tpl->fetch('emails/' . $template)); $subject = $tpl->getTemplateVars('subject'); if (!$subject) { throw new \LogicException('Template did not define a subject'); } | | | | 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | $body = trim($tpl->fetch('emails/' . $template)); $subject = $tpl->getTemplateVars('subject'); if (!$subject) { throw new \LogicException('Template did not define a subject'); } Emails::queue(Emails::CONTEXT_SYSTEM, $to, null, $subject, $body); } static public function loginChanged(User $user): void { $login_field = DynamicFields::getLoginField(); self::send($user, 'login_changed.tpl', ['new_login' => $user->$login_field]); } static public function passwordRecovery(string $email, string $recovery_url, ?string $pgp_key): void { self::send([$email => compact('pgp_key')], 'password_recovery.tpl', compact('recovery_url')); } static public function passwordChanged(User $user): void { $ip = Utils::getIP(); $login_field = DynamicFields::getLoginField(); $login = $user->$login_field; |
︙ | ︙ |
Modified src/include/lib/Garradin/Entities/Email/Mailing.php from [b077c20b3e] to [6abcc9d005].
︙ | ︙ | |||
130 131 132 133 134 135 136 | ]); } public function listRecipients(): \Generator { $db = DB::getInstance(); | | | > | 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | ]); } public function listRecipients(): \Generator { $db = DB::getInstance(); foreach ($db->iterate('SELECT email, extra_data AS data FROM mailings_recipients WHERE id_mailing = ? ORDER BY id;', $this->id) as $row) { $data = $row->data ? json_decode($row->data) : null; yield $row->email => ['data' => $data, 'pgp_key' => $data->pgp_key ?? null]; } } public function getRecipientsList(): DynamicList { $fields = DynamicFields::getNameFields(); $fields = array_map(fn($a) => sprintf('json_extract(r.extra_data, \'$.%s\')', $a), $fields); |
︙ | ︙ |
Modified src/include/lib/Garradin/Entities/Users/User.php from [03740e35d7] to [6afb934fc5].
︙ | ︙ | |||
427 428 429 430 431 432 433 | public function sendMessage(string $subject, string $message, bool $send_copy, ?User $from = null) { $config = Config::getInstance(); $email_field = DynamicFields::getFirstEmailField(); $from = $from ? $from->getNameAndEmail() : null; | | | | 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 | public function sendMessage(string $subject, string $message, bool $send_copy, ?User $from = null) { $config = Config::getInstance(); $email_field = DynamicFields::getFirstEmailField(); $from = $from ? $from->getNameAndEmail() : null; Emails::queue(Emails::CONTEXT_PRIVATE, [$this->{$email_field} => ['pgp_key' => $this->pgp_key]], $from, $subject, $message); if ($send_copy) { Emails::queue(Emails::CONTEXT_PRIVATE, [$config->org_email], null, $subject, $message); } } public function checkLoginFieldForUserEdit() { $session = Session::getInstance(); |
︙ | ︙ |
Modified src/include/lib/Garradin/Services/Reminders.php from [8c425b7b83] to [fa098c6ccf].
︙ | ︙ | |||
113 114 115 116 117 118 119 | 'delai' => $reminder->delay, ]; $subject = self::replaceTagsInContent($reminder->subject, $replace); $text = self::replaceTagsInContent($reminder->body, $replace); // Envoi du mail | | | 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | 'delai' => $reminder->delay, ]; $subject = self::replaceTagsInContent($reminder->subject, $replace); $text = self::replaceTagsInContent($reminder->body, $replace); // Envoi du mail Emails::queue(Emails::CONTEXT_PRIVATE, [$reminder->email => ['data' => $reminder->asArray()]], null, $subject, $text); $db = DB::getInstance(); $db->insert('services_reminders_sent', [ 'id_service' => $reminder->id_service, 'id_user' => $reminder->id_user, 'id_reminder' => $reminder->id_reminder, 'due_date' => $reminder->reminder_date, |
︙ | ︙ |
Modified src/include/lib/Garradin/Users/Users.php from [3f4e070ed1] to [abc3044933].
︙ | ︙ | |||
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | DynamicFields::getNameFieldsSQL(), $where); foreach (DB::getInstance()->iterate($sql) as $row) { yield $row->id => $row->name; } } /** * Return a list for all emails by category * @param int|null $id_category If NULL, then all categories except hidden ones will be returned */ static public function iterateEmailsByCategory(?int $id_category = null): iterable { $db = DB::getInstance(); $fields = DynamicFields::getEmailFields(); $sql = []; $where = $id_category ? sprintf('id_category = %d', $id_category) : 'id_category IN (SELECT id FROM users_categories WHERE hidden = 0)'; foreach ($fields as $field) { | > > > > > > > | | | 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | DynamicFields::getNameFieldsSQL(), $where); foreach (DB::getInstance()->iterate($sql) as $row) { yield $row->id => $row->name; } } static protected function iterateEmails(array $sql, string $email_column = '_email'): \Generator { foreach (DB::getInstance()->iterate(implode(' UNION ALL ', $sql)) as $row) { yield $row->$email_column => $row; } } /** * Return a list for all emails by category * @param int|null $id_category If NULL, then all categories except hidden ones will be returned */ static public function iterateEmailsByCategory(?int $id_category = null): iterable { $db = DB::getInstance(); $fields = DynamicFields::getEmailFields(); $sql = []; $where = $id_category ? sprintf('id_category = %d', $id_category) : 'id_category IN (SELECT id FROM users_categories WHERE hidden = 0)'; foreach ($fields as $field) { $sql[] = sprintf('SELECT *, %s AS _email, NULL AS preferences FROM users WHERE %s AND %1$s IS NOT NULL', $db->quoteIdentifier($field), $where); } return self::iterateEmails($sql); } /** * Return a list of all emails by service (user must be active) */ static public function iterateEmailsByActiveService(int $id_service): iterable { |
︙ | ︙ | |||
67 68 69 70 71 72 73 | DELETE FROM users_active_services WHERE id IN (SELECT id FROM users WHERE id_category IN (SELECT id FROM users_categories WHERE hidden =1));'); } $fields = DynamicFields::getEmailFields(); $sql = []; foreach ($fields as $field) { | | | | 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | DELETE FROM users_active_services WHERE id IN (SELECT id FROM users WHERE id_category IN (SELECT id FROM users_categories WHERE hidden =1));'); } $fields = DynamicFields::getEmailFields(); $sql = []; foreach ($fields as $field) { $sql[] = sprintf('SELECT u.*, u.%s AS _email, NULL AS preferences FROM users u INNER JOIN users_active_services s ON s.id = u.id WHERE s.service = %d AND %1$s IS NOT NULL', $db->quoteIdentifier($field), $id_service); } return self::iterateEmails($sql); } static public function iterateEmailsBySearch(int $id_search): iterable { $db = DB::getInstance(); $s = Search::get($id_search); |
︙ | ︙ | |||
104 105 106 107 108 109 110 | $db->exec(sprintf('INSERT INTO users_tmp_search SELECT %s FROM (%s)', $id_column, $s->SQL())); $fields = DynamicFields::getEmailFields(); $sql = []; foreach ($fields as $field) { | | | | 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | $db->exec(sprintf('INSERT INTO users_tmp_search SELECT %s FROM (%s)', $id_column, $s->SQL())); $fields = DynamicFields::getEmailFields(); $sql = []; foreach ($fields as $field) { $sql[] = sprintf('SELECT u.*, u.%s AS _email, NULL AS preferences FROM users u INNER JOIN users_tmp_search AS s ON s.id = u.id', $db->quoteIdentifier($field)); } return self::iterateEmails($sql); } static public function listByCategory(?int $id_category = null): DynamicList { $df = DynamicFields::getInstance(); $columns = [ |
︙ | ︙ |
Modified src/templates/users/mailing/index.tpl from [66d8105bd9] to [a078781ae8].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | {include file="_head.tpl" title="Messages collectifs" current="users/mailing"} <nav class="tabs"> <aside> {linkbutton shape="plus" label="Nouveau message" href="new.php" target="_dialog"} </aside> <ul> <li class="current"><a href="{$self_url}">Messages collectifs</a></li> <li><a href="rejected.php">Adresses rejetées</a></li> </ul> </nav> {if !$list->count()} <p class="alert block">Aucun message collectif n'a été écrit.<br /> {linkbutton shape="plus" label="Écrire un nouveau message" href="new.php" target="_dialog"} </p> {else} {include file="common/dynamic_list_head.tpl"} | > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | {include file="_head.tpl" title="Messages collectifs" current="users/mailing"} <nav class="tabs"> <aside> {linkbutton shape="plus" label="Nouveau message" href="new.php" target="_dialog"} </aside> <ul> <li class="current"><a href="{$self_url}">Messages collectifs</a></li> <li><a href="rejected.php">Adresses rejetées</a></li> </ul> </nav> {if $_GET.msg == 'DELETE'} <p class="confirm block">Le message a bien été supprimé.</p> {/if} {if !$list->count()} <p class="alert block">Aucun message collectif n'a été écrit.<br /> {linkbutton shape="plus" label="Écrire un nouveau message" href="new.php" target="_dialog"} </p> {else} {include file="common/dynamic_list_head.tpl"} |
︙ | ︙ |
Modified src/templates/users/mailing/rejected.tpl from [c1c2a3aa56] to [cf07adcc85].
1 2 3 4 5 6 7 8 9 10 | {include file="_head.tpl" title="Adresses rejetées" current="users/mailing"} <nav class="tabs"> <ul> <li><a href="./">Messages collectifs</a></li> <li class="current"><a href="rejected.php">Adresses rejetées</a></li> </ul> </nav> {if isset($_GET['sent'])} | > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 | {include file="_head.tpl" title="Adresses rejetées" current="users/mailing"} <nav class="tabs"> <aside> {exportmenu} </aside> <ul> <li><a href="./">Messages collectifs</a></li> <li class="current"><a href="rejected.php">Adresses rejetées</a></li> </ul> </nav> {if isset($_GET['sent'])} |
︙ | ︙ |
Modified src/templates/users/mailing/write.tpl from [a19e815125] to [a5ee5946c0].
1 2 3 4 5 6 7 8 9 10 11 12 | {include file="_head.tpl" title="Message collectif" current="users/mailing" hide_title=true} {form_errors} <form method="post" action="{$self_url}"> <fieldset class="header"> <legend>Modifier le message collectif</legend> <p> {input type="text" name="subject" required=true class="full-width" placeholder="Sujet du message…" source=$mailing} </p> <div> | | | | > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | {include file="_head.tpl" title="Message collectif" current="users/mailing" hide_title=true} {form_errors} <form method="post" action="{$self_url}"> <fieldset class="header"> <legend>Modifier le message collectif</legend> <p> {input type="text" name="subject" required=true class="full-width" placeholder="Sujet du message…" source=$mailing} </p> <div> <p class="sender_default {if $mailing.sender_name}hidden{/if}"> <strong>Expéditeur :</strong> {$config.org_name} <{$config.org_email}> {button label="Modifier" shape="edit" id="f_edit_sender"} </p> <dl class="sender_custom {if !$mailing.sender_name}hidden{/if}"> {input type="text" required=true name="sender_name" source=$mailing label="Nom de l'expéditeur" placeholder="Nom de l'expéditeur"} {input type="email" required=true name="sender_email" source=$mailing label="Adresse e-mail de l'expéditeur" placeholder="Adresse e-mail de l'expéditeur"} </dl> </div> </fieldset> <fieldset class="textEditor"> {input type="textarea" name="content" cols=35 rows=25 required=true class="full-width" data-attachments=0 data-savebtn=0 data-preview-url="!users/mailing/write.php?id=%s&preview"|local_url|args:$mailing.id data-format="markdown" placeholder="Contenu du message…" default=$mailing.body} </fieldset> <p class="submit"> {csrf_field key=$csrf_key} {button type="submit" name="save" label="Enregistrer" shape="right" class="main"} </p> </form> <script type="text/javascript"> {literal} $('#f_edit_sender').onclick = () => { g.toggle('.sender_default', false); g.toggle('.sender_custom', true); } {/literal} {if !$mailing.sender_name} g.toggle('.sender_custom', false); {/if} </script> {include file="_foot.tpl"} |