Comment: | Implement Skriv/Markdown emails + HTML + templates |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | emails |
Files: | files | file ages | folders |
SHA3-256: |
1cda3002a9a3c7dd2b814cfa66c4cc06 |
User & Date: | bohwaz on 2022-05-30 20:52:43 |
Other Links: | branch diff | manifest | tags |
2022-05-30
| ||
21:00 | Add more variables for the email template check-in: fcf4b2a9d2 user: bohwaz tags: emails | |
20:52 | Implement Skriv/Markdown emails + HTML + templates check-in: 1cda3002a9 user: bohwaz tags: emails | |
20:51 | Move emails queue run to a separate task check-in: 922fd4f0a6 user: bohwaz tags: emails | |
Modified src/include/data/1.1.0_schema.sql from [89a0b562ed] to [0a44b3a9cc].
︙ | ︙ | |||
389 390 391 392 393 394 395 | ); CREATE TABLE IF NOT EXISTS emails ( -- List of emails addresses -- We are not storing actual email addresses here for privacy reasons -- So that we can keep the record (for opt-out reasons) even when the -- email address has been removed from the users table | | | 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 | ); CREATE TABLE IF NOT EXISTS emails ( -- List of emails addresses -- We are not storing actual email addresses here for privacy reasons -- So that we can keep the record (for opt-out reasons) even when the -- email address has been removed from the users table id INTEGER NOT NULL PRIMARY KEY, hash TEXT NOT NULL, verified INTEGER NOT NULL DEFAULT 0, optout INTEGER NOT NULL DEFAULT 0, invalid INTEGER NOT NULL DEFAULT 0, fail_count INTEGER NOT NULL DEFAULT 0, sent_count INTEGER NOT NULL DEFAULT 0, fail_log TEXT NULL, |
︙ | ︙ |
Modified src/include/data/schema.sql from [89a0b562ed] to [0a44b3a9cc].
︙ | ︙ | |||
389 390 391 392 393 394 395 | ); CREATE TABLE IF NOT EXISTS emails ( -- List of emails addresses -- We are not storing actual email addresses here for privacy reasons -- So that we can keep the record (for opt-out reasons) even when the -- email address has been removed from the users table | | | 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 | ); CREATE TABLE IF NOT EXISTS emails ( -- List of emails addresses -- We are not storing actual email addresses here for privacy reasons -- So that we can keep the record (for opt-out reasons) even when the -- email address has been removed from the users table id INTEGER NOT NULL PRIMARY KEY, hash TEXT NOT NULL, verified INTEGER NOT NULL DEFAULT 0, optout INTEGER NOT NULL DEFAULT 0, invalid INTEGER NOT NULL DEFAULT 0, fail_count INTEGER NOT NULL DEFAULT 0, sent_count INTEGER NOT NULL DEFAULT 0, fail_log TEXT NULL, |
︙ | ︙ |
Modified src/include/lib/Garradin/Entities/Users/Email.php from [26439d5e99] to [6c0166112c].
︙ | ︙ | |||
69 70 71 72 73 74 75 | throw new UserException('Adresse email inconnue'); } $message = "Bonjour,\n\nPour vérifier votre adresse e-mail pour notre association,\ncliquez sur le lien ci-dessous :\n\n"; $message.= self::getOptoutURL($this->hash) . '&v=' . $this->getVerificationCode(); $message.= "\n\nSi vous n'avez pas demandé à recevoir ce message, ignorez-le."; | | | 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | throw new UserException('Adresse email inconnue'); } $message = "Bonjour,\n\nPour vérifier votre adresse e-mail pour notre association,\ncliquez sur le lien ci-dessous :\n\n"; $message.= self::getOptoutURL($this->hash) . '&v=' . $this->getVerificationCode(); $message.= "\n\nSi vous n'avez pas demandé à recevoir ce message, ignorez-le."; Emails::queue(Emails::CONTEXT_SYSTEM, [$email => null], null, 'Confirmez votre adresse e-mail', $message); } public function verify(string $code): bool { if ($code !== $this->getVerificationCode()) { return false; } |
︙ | ︙ |
Modified src/include/lib/Garradin/Membres.php from [91973ae324] to [8be04b1af9].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <?php namespace Garradin; use KD2\Security; use KD2\SMTP; use Garradin\Membres\Session; use Garradin\Files\Files; use Garradin\Entities\Files\File; use Garradin\Users\Emails; class Membres { const ITEMS_PER_PAGE = 50; // Gestion des données /////////////////////////////////////////////////////// | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <?php namespace Garradin; use KD2\Security; use KD2\SMTP; use Garradin\Membres\Session; use Garradin\Files\Files; use Garradin\Entities\Files\File; use Garradin\Users\Emails; use Garradin\UserTemplate\UserTemplate; class Membres { const ITEMS_PER_PAGE = 50; // Gestion des données /////////////////////////////////////////////////////// |
︙ | ︙ | |||
329 330 331 332 333 334 335 | $operator = 'LIKE ? ESCAPE \'\\\''; } $sql = sprintf('SELECT id, numero, %s AS identite FROM membres WHERE %s %s ORDER BY %1$s LIMIT 50;', $identity, $column, $operator); return DB::getInstance()->get($sql, $query); } | < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | | | 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 | $operator = 'LIKE ? ESCAPE \'\\\''; } $sql = sprintf('SELECT id, numero, %s AS identite FROM membres WHERE %s %s ORDER BY %1$s LIMIT 50;', $identity, $column, $operator); return DB::getInstance()->get($sql, $query); } public function listAllButHidden(): array { return DB::getInstance()->get('SELECT * FROM membres WHERE id_category IN (SELECT id FROM users_categories WHERE hidden = 0) AND email IS NOT NULL AND email != \'\';'); } public function listAllByCategory($id_category, $only_with_email = false) { $where = $only_with_email ? ' AND email IS NOT NULL' : ''; return DB::getInstance()->get('SELECT * FROM membres WHERE id_category = ?' . $where, (int)$id_category); } public function listByCategory(?int $id_category): DynamicList { $config = Config::getInstance(); $db = DB::getInstance(); $identity = $config->get('champ_identite'); |
︙ | ︙ |
Modified src/include/lib/Garradin/Membres/Session.php from [7fdbb21d3f] to [710cbbf9c2].
︙ | ︙ | |||
260 261 262 263 264 265 266 | $message.= ADMIN_URL . 'password.php?c=' . $query; $message.= "\n\nSi vous n'avez pas demandé à recevoir ce message, ignorez-le, votre mot de passe restera inchangé."; if ($membre->clef_pgp) { $content = Security::encryptWithPublicKey($membre->clef_pgp, $message); } | | | 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 | $message.= ADMIN_URL . 'password.php?c=' . $query; $message.= "\n\nSi vous n'avez pas demandé à recevoir ce message, ignorez-le, votre mot de passe restera inchangé."; if ($membre->clef_pgp) { $content = Security::encryptWithPublicKey($membre->clef_pgp, $message); } Emails::queue(Emails::CONTEXT_SYSTEM, [$membre->email => null], null, 'Mot de passe perdu ?', $message); return true; } public function recoverPasswordCheck($code, &$membre = null) { if (substr_count($code, '.') !== 2) { |
︙ | ︙ | |||
330 331 332 333 334 335 336 | $message = "Bonjour,\n\nLe mot de passe de votre compte a bien été modifié.\n\n"; $message.= "Votre adresse email : ".$membre->email."\n"; $message.= "La demande émanait de l'adresse IP : ".Utils::getIP()."\n\n"; $message.= "Si vous n'avez pas demandé à changer votre mot de passe, merci de nous le signaler."; DB::getInstance()->update('membres', ['passe' => $password], 'id = :id', ['id' => (int)$membre->id]); | | | 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 | $message = "Bonjour,\n\nLe mot de passe de votre compte a bien été modifié.\n\n"; $message.= "Votre adresse email : ".$membre->email."\n"; $message.= "La demande émanait de l'adresse IP : ".Utils::getIP()."\n\n"; $message.= "Si vous n'avez pas demandé à changer votre mot de passe, merci de nous le signaler."; DB::getInstance()->update('membres', ['passe' => $password], 'id = :id', ['id' => (int)$membre->id]); return Emails::queue(Emails::CONTEXT_SYSTEM, [$membre->email => null], null, 'Mot de passe changé', $message); } public function editUser($data) { (new Membres)->edit($this->user->id, $data, false); $this->refresh(); |
︙ | ︙ | |||
397 398 399 400 401 402 403 | $user = $this->getUser(); $content = "Ce message vous a été envoyé par :\n"; $content.= sprintf("%s\n%s\n\n", $user->identite, $user->email); $content.= str_repeat('=', 70) . "\n\n"; $content.= $message; | | | 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 | $user = $this->getUser(); $content = "Ce message vous a été envoyé par :\n"; $content.= sprintf("%s\n%s\n\n", $user->identite, $user->email); $content.= str_repeat('=', 70) . "\n\n"; $content.= $message; $dest = $copie ? [$dest => null, $user->email => null] : [$dest => null]; return Emails::queue(Emails::CONTEXT_PRIVATE, $dest, null, $sujet, $content); } public function editSecurity(Array $data = []) { $allowed_fields = ['passe', 'clef_pgp', 'secret_otp']; |
︙ | ︙ |
Modified src/include/lib/Garradin/Services/Reminders.php from [d684207af3] to [0a0fd5e832].
︙ | ︙ | |||
112 113 114 115 116 117 118 | 'delai' => $reminder->delay, ]; $subject = self::replaceTagsInContent($reminder->subject, $replace); $text = self::replaceTagsInContent($reminder->body, $replace); // Envoi du mail | | | 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | '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 => $reminder], 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/UserTemplate/UserTemplate.php from [47bb397359] to [0ec906a8b9].
︙ | ︙ | |||
17 18 19 20 21 22 23 | use Garradin\UserTemplate\Functions; use Garradin\UserTemplate\Sections; use const Garradin\{WWW_URL, ADMIN_URL, SHARED_USER_TEMPLATES_CACHE_ROOT, USER_TEMPLATES_CACHE_ROOT, DATA_ROOT}; class UserTemplate extends Brindille { | | | > > | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | use Garradin\UserTemplate\Functions; use Garradin\UserTemplate\Sections; use const Garradin\{WWW_URL, ADMIN_URL, SHARED_USER_TEMPLATES_CACHE_ROOT, USER_TEMPLATES_CACHE_ROOT, DATA_ROOT}; class UserTemplate extends Brindille { protected $path = null; protected $modified; protected $file = null; protected $code = null; protected $cache_path = USER_TEMPLATES_CACHE_ROOT; static protected $root_variables; static public function getRootVariables() { if (null !== self::$root_variables) { return self::$root_variables; |
︙ | ︙ | |||
107 108 109 110 111 112 113 114 115 116 117 118 119 | foreach (Sections::SECTIONS_LIST as $name) { $this->registerSection($name, [Sections::class, $name]); } } public function setSource(string $path) { $this->path = $path; $this->modified = filemtime($path); } public function display(): void { | > > > > > > > > > > > > > > > > > > > < < | < < < < < > > > > | > > > > | 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | foreach (Sections::SECTIONS_LIST as $name) { $this->registerSection($name, [Sections::class, $name]); } } public function setSource(string $path) { $this->file = null; $this->path = $path; $this->modified = filemtime($path); // Use shared cache for default templates $this->cache_path = SHARED_USER_TEMPLATES_CACHE_ROOT; } public function setCode(string $code) { $this->code = $code; $this->file = null; $this->path = null; $this->modified = time(); // Use custom cache for user templates $this->cache_path = USER_TEMPLATES_CACHE_ROOT; } protected function _getCachePath() { $hash = sha1($this->file ? $this->file->path : ($this->code ?: $this->path)); return sprintf('%s/%s.php', $this->cache_path, $hash); } public function display(): void { $compiled_path = $this->_getCachePath(true); if (!is_dir(dirname($compiled_path))) { // Force cache directory mkdir Utils::safe_mkdir(dirname($compiled_path), 0777, true); } if (file_exists($compiled_path) && filemtime($compiled_path) >= $this->modified) { require $compiled_path; return; } $tmp_path = $compiled_path . '.tmp'; if ($this->code) { $source = $this->code; } elseif ($this->file) { $source = $this->file->fetch(); } else { $source = file_get_contents($this->path); } try { $code = $this->compile($source); file_put_contents($tmp_path, $code); require $tmp_path; } |
︙ | ︙ |
Modified src/include/lib/Garradin/Users/Emails.php from [5b79111d94] to [b04ac7886f].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <?php namespace Garradin\Users; use Garradin\Config; use Garradin\DB; use Garradin\DynamicList; use Garradin\Plugin; use Garradin\Entities\Users\Email; use const Garradin\{USE_CRON}; use const Garradin\{SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_SECURITY}; use KD2\SMTP; use KD2\Mail_Message; use KD2\DB\EntityManager as EM; class Emails { | > > > > > > > > > | | | | < | | | | > > > > | | | < < > | > > > > > > > > | > | > > > | > > > > < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 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 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 | <?php namespace Garradin\Users; use Garradin\Config; use Garradin\DB; use Garradin\DynamicList; use Garradin\Plugin; use Garradin\Entities\Users\Email; use Garradin\UserTemplate\UserTemplate; use Garradin\Web\Render\Render; use Garradin\Web\Skeleton; use const Garradin\{USE_CRON}; use const Garradin\{SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_SECURITY}; use KD2\SMTP; use KD2\Mail_Message; use KD2\DB\EntityManager as EM; class Emails { const RENDER_FORMATS = [ null => 'Texte brut', Render::FORMAT_SKRIV => 'SkrivML', Render::FORMAT_MARKDOWN => 'MarkDown', ]; const CONTEXT_BULK = 1; const CONTEXT_PRIVATE = 2; const CONTEXT_SYSTEM = 0; /** * Seuil à partir duquel on n'essaye plus d'envoyer de message à cette adresse */ const FAIL_LIMIT = 5; /** * Add a message to the sending queue using templates * @param int $context * @param array $recipients List of recipients, 'From' email address as the key, and an array as a value, that contains variables to be used in the email template * @param string $sender * @param string $subject * @param UserTemplate|string $content * @return void */ static public function queue(int $context, array $recipients, ?string $sender, string $subject, $content, ?string $render = null): void { // Remove duplicates due to case changes $recipients = array_change_key_case($recipients, CASE_LOWER); $template = ($content instanceof UserTemplate) ? $content : null; $skel = null; $content_html = null; $db = DB::getInstance(); $db->begin(); $st = $db->prepare('INSERT INTO emails_queue (sender, subject, recipient, recipient_hash, content, content_html, context) VALUES (:sender, :subject, :recipient, :recipient_hash, :content, :content_html, :context);'); if ($render) { $skel = new Skeleton('email.html'); } foreach ($recipients as $to => $variables) { // Ignore invalid addresses if (!preg_match('/.+@.+\..+$/', $to)) { continue; } // We won't try to reject invalid/optout recipients here, // it's done in the queue clearing (more efficient) $hash = Email::getHash($to); if ($template) { $template->assignArray((array) $variables); $content = $template->fetch(); } if ($render) { $content_html = Render::render($render, null, $content); $content_html = $skel->fetch(['html' => $content_html]); } $st->bindValue(':sender', $sender); $st->bindValue(':subject', $subject); $st->bindValue(':context', $context); $st->bindValue(':recipient', $to); $st->bindValue(':recipient_hash', $hash); $st->bindValue(':content', $content); $st->bindValue(':content_html', $content_html); $st->execute(); $st->reset(); $st->clear(); } $db->commit(); Plugin::fireSignal('email.queue.added'); // If no crontab is used, then the queue should be run now if (!USE_CRON) { self::runQueue(); } |
︙ | ︙ | |||
360 361 362 363 364 365 366 | if ($context != self::CONTEXT_SYSTEM) { $url = Email::getOptoutURL($recipient_hash); // RFC 8058 $message->setHeader('List-Unsubscribe', sprintf('<%s>', $url)); $message->setHeader('List-Unsubscribe-Post', 'Unsubscribe=Yes'); | | | | | 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 | if ($context != self::CONTEXT_SYSTEM) { $url = Email::getOptoutURL($recipient_hash); // RFC 8058 $message->setHeader('List-Unsubscribe', sprintf('<%s>', $url)); $message->setHeader('List-Unsubscribe-Post', 'Unsubscribe=Yes'); $optout_text = "Vous recevez ce message car vous êtes inscrit comme membre de l'association.\n" . "Pour ne plus jamais recevoir de message de notre part cliquez sur le lien suivant :\n"; $content .= "\n\n-- \n" . $optout_text . $url; if (null !== $content_html) { $optout_text = '<hr style="border-top: 2px solid #999; background: none;" /><p style="color: #000; background: #fff; padding: 10px; text-align: center; font-size: 9pt">' . nl2br(htmlspecialchars($optout_text)); $optout_text.= sprintf('<br /><a href="%s" style="color: blue; text-decoration: underline; padding: 5px; border-radius: 5px; background: #ddd;">Me désinscrire</a></p>', $url); if (stripos($content_html, '</body>') !== false) { $content_html = str_ireplace('</body>', $optout_text . '</body>', $content_html); } else { $content_html .= $optout_text; } |
︙ | ︙ | |||
449 450 451 452 453 454 455 | if (!$email) { return; } $email->hasFailed($return); $email->save(); } | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 | if (!$email) { return; } $email->hasFailed($return); $email->save(); } static public function createMailing(array $recipients, string $subject, string $message, bool $send_copy, ?string $render): \stdClass { $config = Config::getInstance(); $list = []; foreach ($recipients as $recipient) { if (empty($recipient->email)) { continue; } $list[$recipient->email] = $recipient; } if (!count($list)) { throw new UserException('Aucun destinataire de la liste ne possède d\'adresse email.'); } $html = $message; $tpl = null; $random = array_rand($list); if (false !== strpos($message, '{{')) { $tpl = new UserTemplate; $tpl->setCode($message); $tpl->assignArray((array)$list[$random]); $html = $tpl->fetch(); } if ($render) { $html = Render::render($render, null, $html); } else { $html = '<pre>' . htmlspecialchars($html) . '</pre>'; } $recipients = $list; $sender = sprintf('"%s" <%s>', $config->nom_asso, $config->email_asso); $message = (object) compact('recipients', 'subject', 'message', 'sender', 'tpl', 'send_copy', 'render'); $message->preview = (object) [ 'to' => $random, 'from' => $sender, 'subject' => $subject, 'html' => $html, ]; return $message; } static public function sendMailing(\stdClass $mailing): void { if (!isset($mailing->recipients, $mailing->subject, $mailing->message, $mailing->send_copy)) { throw new \InvalidArgumentException('Invalid $mailing object'); } if (!count($mailing->recipients)) { throw new UserException('Aucun destinataire de la liste ne possède d\'adresse email.'); } Emails::queue(Emails::CONTEXT_BULK, $mailing->recipients, null, // Default sender $mailing->subject, $mailing->tpl ?? $mailing->message, $mailing->render ?? null ); if ($mailing->send_copy) { $config = Config::getInstance(); Emails::queue(Emails::CONTEXT_BULK, [$config->get('email_asso') => null], null, $mailing->subject, $mailing->message); } } } |
Modified src/include/lib/Garradin/Web/Render/Render.php from [e445f0f98d] to [dac2d2f09d].
︙ | ︙ | |||
9 10 11 12 13 14 15 | const FORMAT_SKRIV = 'skriv'; const FORMAT_ENCRYPTED = 'skriv/encrypted'; const FORMAT_MARKDOWN = 'markdown'; const FORMAT_BLOCKS = 'blocks'; static protected $attachments = []; | | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | const FORMAT_SKRIV = 'skriv'; const FORMAT_ENCRYPTED = 'skriv/encrypted'; const FORMAT_MARKDOWN = 'markdown'; const FORMAT_BLOCKS = 'blocks'; static protected $attachments = []; static public function render(string $format, ?File $file, string $content = null, string $link_prefix = null) { return self::getRenderer($format, $file, $link_prefix)->render($content); } static public function getRenderer(string $format, ?File $file, string $link_prefix = null) { if ($format == self::FORMAT_SKRIV) { return new Skriv($file, $link_prefix); } else if ($format == self::FORMAT_ENCRYPTED) { return new EncryptedSkriv($file, $link_prefix); } |
︙ | ︙ |
Modified src/templates/admin/membres/_details.tpl from [2d6b70a94a] to [cae6c2a26a].
︙ | ︙ | |||
44 45 46 47 48 49 50 | {/if} {/foreach} </ul> {else} {$value|display_champ_membre:$c_config|raw} {/if} </dd> | | | 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | {/if} {/foreach} </ul> {else} {$value|display_champ_membre:$c_config|raw} {/if} </dd> {if $c_config.type == 'email' && $value && ($email = Users\Emails::getEmail($value))} <dd class="help"> {if $email.optout} <b class="alert">A demandé à ne plus recevoir de messages</b> {elseif $email.invalid} <b class="error">Adresse invalide</b> | {$email.fail_log|escape|nl2br} {elseif $email->hasReachedFailLimit()} <b class="error">Trop d'erreurs</b> | {$email.fail_log|escape|nl2br} |
︙ | ︙ |
Modified src/templates/admin/membres/emails.tpl from [d64ed6b92f] to [e255deaf2b].
1 2 3 | {include file="admin/_head.tpl" title="Adresses rejetées" current="membres/message"} <nav class="tabs"> | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | {include file="admin/_head.tpl" title="Adresses rejetées" current="membres/message"} <nav class="tabs"> <ul> <li><a href="message_collectif.php">Envoyer</a></li> <li class="current"><a href="emails.php">Adresses rejetées</a></li> </ul> </nav> {if isset($_GET['sent'])} <p class="confirm block"> Un message de demande de confirmation a bien été envoyé. Le destinataire doit désormais cliquer sur le lien dans ce message. </p> {/if} |
︙ | ︙ | |||
46 47 48 49 50 51 52 53 54 55 56 57 58 59 | </table> {pagination url=$list->paginationURL() page=$list.page bypage=$list.per_page total=$list->count()} <div class="block help"> <h3>Statuts possibles d'une adresse e-mail :</h3> <dl class="cotisation"> <dt>Vérifiée</dt> <dd>L'adresse a déjà reçu un message et a été vérifiée manuellement par le destinataire.</dd> <dt>Désinscription</dt> <dd>Le destinataire a demandé à être désinscrit et ne recevra plus de messages.</dd> <dt>Invalide</dt> <dd>L'adresse n'existe pas ou plus. Il n'est pas possible de lui envoyer des messages.</dd> <dt>Trop de tentatives</dt> | > > | | 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | </table> {pagination url=$list->paginationURL() page=$list.page bypage=$list.per_page total=$list->count()} <div class="block help"> <h3>Statuts possibles d'une adresse e-mail :</h3> <dl class="cotisation"> {* <dt>Vérifiée</dt> <dd>L'adresse a déjà reçu un message et a été vérifiée manuellement par le destinataire.</dd> *} <dt>Désinscription</dt> <dd>Le destinataire a demandé à être désinscrit et ne recevra plus de messages.</dd> <dt>Invalide</dt> <dd>L'adresse n'existe pas ou plus. Il n'est pas possible de lui envoyer des messages.</dd> <dt>Trop de tentatives</dt> <dd>Le service destinataire a renvoyé une erreur temporaire plus de {$max_fail_count} fois.<br />Cela arrive par exemple si vos messages sont vus comme du spam trop souvent, ou si la boîte mail destinataire est pleine. Cette adresse ne recevra plus de message.</dd> </dl> </div> {/if} {include file="admin/_foot.tpl"} |
Modified src/templates/admin/membres/message.tpl from [43ce250d36] to [29a83a20df].
1 2 3 4 5 | {include file="admin/_head.tpl" title="Contacter un membre" current="membres"} {form_errors} <form method="post" action="{$self_url}"> | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | {include file="admin/_head.tpl" title="Contacter un membre" current="membres"} {form_errors} <form method="post" action="{$self_url}"> <fieldset class="mailing"> <legend>Message</legend> <dl> <dt>Expéditeur</dt> <dd>{$user.identite} <{$user.email}></dd> <dt>Destinataire</dt> <dd>{$membre.identite} ({$categorie.name})</dd> <dt><label for="f_subject">Sujet</label> <b title="(Champ obligatoire)">obligatoire</b></dt> <dd><input type="text" name="sujet" id="f_subject" value="{form_field name=sujet}" required="required" /></dd> <dt><label for="f_message">Message</label> <b title="(Champ obligatoire)">obligatoire</b></dt> <dd><textarea name="message" id="f_message" cols="72" rows="25" required="required">{form_field name=message}</textarea></dd> <dd> <input type="checkbox" name="copie" id="f_copie" value="1" /> <label for="f_copie">Recevoir par e-mail une copie du message envoyé</label> </dd> </dl> |
︙ | ︙ |
Modified src/templates/admin/membres/message_collectif.tpl from [49ced21e88] to [d2963da0b5].
|
| | > > > > | > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | | | | | | < < < < < < < < | < < < | | < > | | > | 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 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 | {include file="admin/_head.tpl" title="Envoyer un message collectif" current="membres/message" custom_css=["!web/css.php"]} <nav class="tabs"> <ul> <li class="current"><a href="{$self_url}">Envoyer</a></li> <li><a href="emails.php">Adresses rejetées</a></li> </ul> </nav> {if $sent} <p class="block confirm">Votre message a été envoyé.</p> {/if} {form_errors} <form method="post" action="{$self_url_no_qs}"> {if $preview} <fieldset class="mailing"> <legend>Prévisualisation du message</legend> <p class="help"> Ce message sera envoyé à <strong>{$recipients_count}</strong> destinataires.<br /> Voici un exemple du message pour un de ces destinataires. </p> <dl> <dt>Expéditeur</dt> <dd>{$preview.from}</dd> <dt>Destinataire</dt> <dd> {$preview.to} </dd> <dt>Sujet</dt> <dd>{$preview.subject}</dd> <dt>Message</dt> <dd class="preview">{$preview.html|raw}</dd> </dl> </fieldset> <p class="submit"> {input type="hidden" name="subject"} {input type="hidden" name="message"} {input type="hidden" name="target"} {input type="hidden" name="send_copy"} {input type="hidden" name="render"} {csrf_field key=$csrf_key} {button type="submit" name="back" label="Retour à l'édition" shape="left"} {button type="submit" name="send" label="Envoyer" shape="right" class="main"} </p> {else} <fieldset class="mailing"> <legend>Message</legend> <dl> <dt>Expéditeur</dt> <dd>{$config.nom_asso} <{$config.email_asso}></dd> <dt><label for="f_target">Destinataires</label></dt> <dd> <select name="target" id="f_target" required="required"> <option value="all_">Tous les membres (sauf ceux appartenant à une catégorie cachée)</option> <optgroup label="Catégorie de membres"> {foreach from=$categories key="id" item="label"} <option value="category_{$id}" {form_field name="target" selected="category_%d"|args:$id}>{$label}</option> {/foreach} </optgroup> <optgroup label="Recherches enregistrées"> {foreach from=$search_list item="s"} <option value="search_{$s.id}" {form_field name="target" selected="search_%d"|args:$s.id}>{$s.intitule}</option> {/foreach} </optgroup> </select> </dd> <dd class="help"> Vous pouvez cibler précisément des membres en créant une <a href="{$admin_url}membres/recherche.php">recherche enregistrée</a>. Les recherches enregistrées apparaîtront dans ce formulaire. </dd> {input type="text" name="subject" required=true label="Sujet"} {input type="textarea" name="message" cols=35 rows=25 required=true label="Message"} {input type="checkbox" name="send_copy" value=1 label="Recevoir par e-mail une copie du message envoyé"} {input type="select" name="render" label="Format de rendu" options=$render_formats help="Pour enrichir le contenu du mail, inclure des liens, du gras, des titres, etc."} </dl> </fieldset> <p class="submit"> {csrf_field key=$csrf_key} {button type="submit" name="preview" label="Prévisualiser" shape="right" class="main"} </p> {/if} </form> {include file="admin/_foot.tpl"} |
Modified src/www/admin/membres/message_collectif.php from [c1205018b1] to [e8bbbf3b36].
1 2 3 4 5 6 7 8 9 10 | <?php namespace Garradin; use Garradin\Users\Categories; require_once __DIR__ . '/_inc.php'; $session->requireAccess($session::SECTION_USERS, $session::ACCESS_WRITE); $recherche = new Recherche; | > | < | | > | < < < | | < > > | < | | < < > | < | < < > | > | < | | > > | | < | | > > | < | > | | < < < < | | < | < < | < < < > > > > | > > > > > > | 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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | <?php namespace Garradin; use Garradin\Users\Categories; use Garradin\Users\Emails; require_once __DIR__ . '/_inc.php'; $session->requireAccess($session::SECTION_USERS, $session::ACCESS_WRITE); $recherche = new Recherche; $csrf_key = 'send_mailing'; $form->runIf(f('send') || f('subject'), function () use ($membres, &$mailing, $recherche) { if (!trim(f('subject'))) { throw new UserException('Le sujet ne peut rester vide.'); } if (!trim(f('message'))) { throw new UserException('Le message ne peut rester vide.'); } if (!f('target')) { throw new UserException('Aucun destinataire sélectionné.'); } $target = explode('_', f('target')); if (count($target) !== 2) { throw new UserException('Destinataire invalide'); } if ($target[0] == 'all') { $recipients = $membres->listAllButHidden(); } elseif ($target[0] == 'category') { $recipients = $membres->listAllByCategory($target[1], true); } elseif ($target[0] == 'search') { $recipients = $recherche->search($target[1], ['membres.*'], true); } if (!count($recipients)) { throw new UserException('La liste de destinataires sélectionnée ne comporte aucun membre, ou aucun avec une adresse e-mail renseignée.'); } $mailing = Emails::createMailing($recipients, f('subject'), f('message'), (bool) f('send_copy'), f('render') ?: null); }, $csrf_key); $form->runIf('send', function () use ($membres, $mailing) { Emails::sendMailing($mailing); }, $csrf_key, '!membres/message_collectif.php?sent'); $tpl->assign('categories', Categories::listNotHidden()); $tpl->assign('preview', f('preview') && $mailing ? $mailing->preview : null); $tpl->assign('recipients_count', $mailing ? count($mailing->recipients) : 0); $tpl->assign('search_list', $recherche->getList($user->id, 'membres')); $tpl->assign('render_formats', Emails::RENDER_FORMATS); $tpl->assign(compact('csrf_key')); $tpl->assign('sent', null !== qg('sent')); $tpl->display('admin/membres/message_collectif.tpl'); |
Modified src/www/admin/static/styles/03-forms.css from [2ce7e6ae49] to [e17ff4b17b].
︙ | ︙ | |||
532 533 534 535 536 537 538 | } .datepicker tbody input:hover { background: #ccf; color: darkred; } | | | > > > > > | > | 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 | } .datepicker tbody input:hover { background: #ccf; color: darkred; } fieldset.mailing { max-width: 40em; } fieldset.mailing dd.preview > * { border-radius: .5em; background: var(--gLightBackgroundColor); padding: 1em; } #queryBuilderForm textarea, fieldset.mailing textarea, fieldset.mailing #f_subject, fieldset.mailing #f_target { width: calc(100% - 2em); } #queryBuilder .column select, #queryBuilderForm .actions select { max-width: 15em; } |
︙ | ︙ |
Modified src/www/skel-dist/content.css from [fa05be07ee] to [ce4188bfc6].
1 2 3 4 5 6 7 8 9 | /** * Ce fichier contient les styles CSS qui s'appliquent au contenu des articles et catégorie, * que ce soit sur le site public ou dans la prévisualisation de l'administration. * * Généralement il n'est pas nécessaire de le modifier. */ .web-content p, .web-content h1, .web-content h2, .web-content h3, .web-content h4, .web-content h5, .web-content h6, .web-content ul, .web-content ol, .web-content table, .web-content blockquote, .web-content pre { | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | /** * Ce fichier contient les styles CSS qui s'appliquent au contenu des articles et catégorie, * que ce soit sur le site public ou dans la prévisualisation de l'administration. * * Généralement il n'est pas nécessaire de le modifier. */ .web-content p, .web-content h1, .web-content h2, .web-content h3, .web-content h4, .web-content h5, .web-content h6, .web-content ul, .web-content ol, .web-content table, .web-content blockquote, .web-content pre { margin: 0; margin-bottom: .8em; } .web-content ul, .web-content ol, .web-content dd { margin-left: 2em; } .web-content ul { |
︙ | ︙ |
Added src/www/skel-dist/email.html version [855f91c8f8].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 50 | {{* Ce squelette est utilisé pour l'envoi d'un e-mail au format Skriv ou Markdown *}} <!DOCTYPE html> <html> <head> <style type="text/css"> * { margin: 0; padding: 0; } body { background: #eee; color: #000; } a { color: #009; text-decoration: underline; } .web-content, .footer { padding: 10px; background: #fff; max-width: 750px; margin: 10px auto; border-radius: 5px; } .footer { background: #ddd; } .footer h3, .footer h4, .footer h5 { text-align: center; margin: 5px 0; } {{* Inclure le contenu de content.css pour s'assurer que le style du texte Markdown/Skriv est correct *}} {{:include file="content.css"}} </style> </head> <body> {{* Le contenu du mail est dans la variable $html, ne pas supprimer sinon le message sera vide ! *}} {{$html|raw}} <div class="footer"> <h3><a href="{{$config.site_asso}}">{{$config.nom_asso}}</a></h3> {{if $config.adresse_asso}} <h4>{{$config.adresse_asso}}</h4> {{/if}} {{if $config.telephone_asso}} <h5><a href="tel:{{$config.telephone_asso}}">{{$config.telephone_asso|raw}}</a></h5> {{/if}} </div> {{* Le lien de désinscription sera ajouté automatiquement en bas du message, il n'est pas possible de le modifier ou le supprimer. *}} </body> </html> |