Overview
Comment: | Add handle bounce script |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | emails |
Files: | files | file ages | folders |
SHA3-256: |
a3a07e43187653f054e1c841eba68932 |
User & Date: | bohwaz on 2022-05-31 01:43:28 |
Other Links: | branch diff | manifest | tags |
Context
2022-05-31
| ||
13:11 | Handle Brindille errors in mailing check-in: d69d7b6df6 user: bohwaz tags: emails | |
01:43 | Add handle bounce script check-in: a3a07e4318 user: bohwaz tags: emails | |
2022-05-30
| ||
21:46 | Move POST unsubscribe to index.php as POST does not always handle redirects check-in: d2eaaa7d7b user: bohwaz tags: emails | |
Changes
Modified src/include/lib/Garradin/Users/Emails.php from [2bec96d971] to [42cc96f10d].
︙ | ︙ | |||
22 23 24 25 26 27 28 29 30 31 32 33 | { 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; /** | > > > | | 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | { const RENDER_FORMATS = [ null => 'Texte brut', Render::FORMAT_SKRIV => 'SkrivML', Render::FORMAT_MARKDOWN => 'MarkDown', ]; /** * Email sending contexts */ const CONTEXT_BULK = 1; const CONTEXT_PRIVATE = 2; const CONTEXT_SYSTEM = 0; /** * 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, 'From' email address as the key, and an array as a value, that contains variables to be used in the email template |
︙ | ︙ | |||
107 108 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 | // If no crontab is used, then the queue should be run now if (!USE_CRON) { self::runQueue(); } } static public function getEmailFromOptout(string $code): ?Email { $hash = base64_decode(str_pad(strtr($code, '-_', '+/'), strlen($code) % 4, '=', STR_PAD_RIGHT)); if (!$hash) { return null; } $hash = bin2hex($hash); return EM::findOne(Email::class, 'SELECT * FROM @TABLE WHERE hash = ?;', $hash); } static public function markAddressAsInvalid(string $address): void { $e = self::getEmail($address); if (!$e) { return; } $e->set('invalid', true); $e->save(); } static public function getEmail(string $address): ?Email { | > > > > > > > > > > > | > > > > > > > | > > | 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 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 | // If no crontab is used, then the queue should be run now if (!USE_CRON) { self::runQueue(); } } /** * Return an Email entity from the optout code */ static public function getEmailFromOptout(string $code): ?Email { $hash = base64_decode(str_pad(strtr($code, '-_', '+/'), strlen($code) % 4, '=', STR_PAD_RIGHT)); if (!$hash) { return null; } $hash = bin2hex($hash); return EM::findOne(Email::class, 'SELECT * FROM @TABLE WHERE hash = ?;', $hash); } /** * Sets the address as invalid (no email can be sent to this address ever) */ static public function markAddressAsInvalid(string $address): void { $e = self::getEmail($address); if (!$e) { return; } $e->set('invalid', true); $e->set('optout', false); $e->set('verified', false); $e->save(); } /** * Return an Email entity from an email address */ static public function getEmail(string $address): ?Email { return EM::findOne(Email::class, 'SELECT * FROM @TABLE WHERE hash = ?;', Email::getHash(strtolower($address))); } /** * Return or create a new email entity */ static public function getOrCreateEmail(string $address): Email { $address = strtolower($address); $e = self::getEmail($address); if (!$e) { $e = new Email; $e->added = new \DateTime; $e->hash = $e::getHash($address); $e->validate($address); $e->save(); } return $e; } /** * Run the queue of emails that are waiting to be sent */ static public function runQueue(): void { $db = DB::getInstance(); $queue = self::listQueueAndMarkAsSending(); $ids = []; // listQueue nettoie déjà la queue foreach ($queue as $row) { // Don't send emails to opt-out address, unless it's a password reminder // Invalid and failed-too-many addresses are purged from the queue before processing, no need to handle them here // We still allow emails to be sent to failed or optout addresses if it's a system email if ($row->context != self::CONTEXT_SYSTEM && $row->optout) { self::deleteFromQueue($row->id); continue; } // Create email address in database if (!$row->email_hash) { |
︙ | ︙ | |||
200 201 202 203 204 205 206 | UPDATE %2$s SET sent_count = sent_count + 1, last_sent = datetime() WHERE hash IN (SELECT recipient_hash FROM emails_queue WHERE sending = 2); DELETE FROM emails_queue WHERE sending = 2; END;', $db->where('id', $ids), Email::TABLE)); } /** | | | | < < > | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 | UPDATE %2$s SET sent_count = sent_count + 1, last_sent = datetime() WHERE hash IN (SELECT recipient_hash FROM emails_queue WHERE sending = 2); DELETE FROM emails_queue WHERE sending = 2; END;', $db->where('id', $ids), Email::TABLE)); } /** * Lists the queue, marks listed elements as "sending" * @return array */ static protected function listQueueAndMarkAsSending(): array { $queue = self::listQueue(); if (!count($queue)) { return $queue; } $ids = []; foreach ($queue as $row) { $ids[] = $row->id; } $db = DB::getInstance(); $db->update('emails_queue', ['sending' => 1, 'sending_started' => new \DateTime], $db->where('id', $ids)); return $queue; } /** * Returns the lits of emails waiting to be sent, except invalid ones and emails that haved failed too much * * DO NOT USE for sending, use listQueueAndMarkAsSending instead, or there might be multiple processes sending * the same email over and over. * @return array */ static protected function listQueue(): array { // Clean-up the queue from reject emails self::purgeQueueFromRejected(); // Reset messages that failed during the queue run self::resetFailed(); return DB::getInstance()->get('SELECT q.*, e.optout, e.verified, e.hash AS email_hash FROM emails_queue q LEFT JOIN emails e ON e.hash = q.recipient_hash WHERE q.sending = 0;'); } static public function countQueue(): int { return DB::getInstance()->count('emails_queue'); } /** * Supprime de la queue les messages liés à des adresses invalides * ou qui ne souhaitent plus recevoir de message * @return boolean */ static protected function purgeQueueFromRejected(): void { DB::getInstance()->delete('emails_queue', 'recipient_hash IN (SELECT hash FROM emails WHERE invalid = 1 OR fail_count >= ?)', self::FAIL_LIMIT); } /** * If emails have been marked as sending but sending failed, mark them for resend after a while */ static protected function resetFailed(): void { $sql = 'UPDATE emails_queue SET sending = 0, sending_started = NULL WHERE sending = 1 AND sending_started < datetime(\'now\', \'-3 hours\');'; DB::getInstance()->exec($sql); } /** * Supprime un message de la queue d'envoi * @param integer $id * @return boolean */ static protected function deleteFromQueue($id) { return DB::getInstance()->delete('emails_queue', 'id = ?', (int)$id); } static public function listRejectedUsers(): DynamicList { $db = DB::getInstance(); $columns = [ 'identity' => [ |
︙ | ︙ | |||
298 299 300 301 302 303 304 | $conditions = sprintf('e.optout = 1 OR e.invalid = 1 OR e.fail_count >= %d', self::FAIL_LIMIT); $list = new DynamicList($columns, $tables, $conditions); $list->orderBy('last_sent', true); return $list; } | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 355 356 357 358 359 360 361 362 363 364 365 366 367 368 | $conditions = sprintf('e.optout = 1 OR e.invalid = 1 OR e.fail_count >= %d', self::FAIL_LIMIT); $list = new DynamicList($columns, $tables, $conditions); $list->orderBy('last_sent', true); return $list; } static protected function send(int $context, string $recipient_hash, array $headers, string $content, ?string $content_html): void { $config = Config::getInstance(); $message = new Mail_Message; $message->setHeaders($headers); |
︙ | ︙ | |||
441 442 443 444 445 446 447 448 449 450 451 452 453 454 | 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)) { | > > > | 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 | return; } $email->hasFailed($return); $email->save(); } /** * Create a mass mailing */ 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)) { |
︙ | ︙ | |||
491 492 493 494 495 496 497 498 499 500 501 502 503 504 | '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)) { | > > > | 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 | 'subject' => $subject, 'html' => $html, ]; return $message; } /** * Send a mass mailing */ 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)) { |
︙ | ︙ |
Added src/scripts/handle_bounce.php version [5eda369c22].
> > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <?php namespace Garradin; use Garradin\Users\Emails; require_once __DIR__ . '/../include/init.php'; if (PHP_SAPI != 'cli') { echo "This command can only be called from the command-line.\n"; exit(1); } $message = file_get_contents('php://stdin'); if (empty($message)) { echo "No STDIN content was provided.\nPlease provide the email message on STDIN.\n"; exit(2); } Emails::handleBounce($message); |