From 71c5f8d7738602adcf93ac6d6b5ab8bb5d12e205 Mon Sep 17 00:00:00 2001 From: Philip Rogge Date: Fri, 5 Nov 2021 08:52:24 +0100 Subject: [PATCH 1/4] Added possibility for CC and BCC --- README.md | 2 + ...20211105000000_AddCCAndBccToEmailQueue.php | 43 +++++++++++++++++++ src/Database/Type/JsonType.php | 35 ++++++++++----- src/Database/Type/SerializeType.php | 7 ++- src/EmailQueue.php | 3 +- src/Model/Table/EmailQueueTable.php | 6 +++ src/Shell/PreviewShell.php | 10 +++++ src/Shell/SenderShell.php | 10 +++++ tests/Fixture/EmailQueueFixture.php | 2 + tests/TestCase/Model/Table/EmailQueueTest.php | 26 +++++++++++ 10 files changed, 132 insertions(+), 12 deletions(-) create mode 100644 config/Migrations/20211105000000_AddCCAndBccToEmailQueue.php diff --git a/README.md b/README.md index 33552d2..b262fa3 100755 --- a/README.md +++ b/README.md @@ -62,6 +62,8 @@ and queue a new one by storing the correct data: email template - Third arguments is an array of options, possible options are * `subject`: Email's subject + * `cc`: Email's carbon copy (string with email, Array with email as key, name as value or email as value (without name)) + * `bcc`: Email's blind carbon copy (string with email, Array with email as key, name as value or email as value (without name)) * `send_at`: date time sting representing the time this email should be sent at (in UTC) * `template`: the name of the element to use as template for the email message. (maximum supported length is 100 chars) * `layout`: the name of the layout to be used to wrap email message diff --git a/config/Migrations/20211105000000_AddCCAndBccToEmailQueue.php b/config/Migrations/20211105000000_AddCCAndBccToEmailQueue.php new file mode 100644 index 0000000..f0c7827 --- /dev/null +++ b/config/Migrations/20211105000000_AddCCAndBccToEmailQueue.php @@ -0,0 +1,43 @@ +table('email_queue'); + $table->addColumn('cc', 'string', [ + 'default' => null, + 'limit' => 129, + 'null' => true, + 'after' => 'email' + ]); + $table->addColumn('bcc', 'string', [ + 'default' => null, + 'limit' => 129, + 'null' => true, + 'after' => 'cc' + ]); + $table->addIndex([ + 'cc', + ], [ + 'name' => 'BY_CC', + 'unique' => false, + ]); + $table->addIndex([ + 'bcc', + ], [ + 'name' => 'BY_BCC', + 'unique' => false, + ]); + $table->update(); + } +} diff --git a/src/Database/Type/JsonType.php b/src/Database/Type/JsonType.php index 09ccc4b..e783401 100755 --- a/src/Database/Type/JsonType.php +++ b/src/Database/Type/JsonType.php @@ -18,11 +18,7 @@ class JsonType extends BaseType implements OptionalConvertInterface */ public function toPHP($value, DriverInterface $driver) { - if ($value === null) { - return; - } - - return json_decode($value, true); + return $this->decodeJson($value); } /** @@ -33,11 +29,7 @@ public function toPHP($value, DriverInterface $driver) */ public function marshal($value) { - if (is_array($value) || $value === null) { - return $value; - } - - return json_decode($value, true); + return $this->decodeJson($value); } /** @@ -61,4 +53,27 @@ public function requiresToPhpCast(): bool { return true; } + + /** + * Returns the given value as an array (if it is json or already an array) + * or as a string (if it is already a string) + * + * @param array|string|null $value json string, array or string to decode + * @return array|string|null depending on the input, see description + */ + private function decodeJson($value) + { + if (is_array($value) || $value === null) { + return $value; + } + + $jsonDecode = json_decode($value, true); + + // check, if the value is null after json_decode to handle plain strings + if ($jsonDecode === null) { + return $value; + } + + return $jsonDecode; + } } diff --git a/src/Database/Type/SerializeType.php b/src/Database/Type/SerializeType.php index 773d785..8a7d5af 100755 --- a/src/Database/Type/SerializeType.php +++ b/src/Database/Type/SerializeType.php @@ -22,7 +22,12 @@ public function toPHP($value, DriverInterface $driver) return null; } - return unserialize($value); + try { + return unserialize($value); + } catch (\Exception $e) { + // return value, if the value isn't serialized + return $value; + } } /** diff --git a/src/EmailQueue.php b/src/EmailQueue.php index 232c97a..b3cd616 100755 --- a/src/EmailQueue.php +++ b/src/EmailQueue.php @@ -15,6 +15,8 @@ class EmailQueue * @param array $options list of options for email sending. Possible keys: * * - subject : Email's subject + * - cc: array of carbon copy + * - bcc: array of blind carbon copy * - send_at : date time sting representing the time this email should be sent at (in UTC) * - template : the name of the element to use as template for the email message * - layout : the name of the layout to be used to wrap email message @@ -22,7 +24,6 @@ class EmailQueue * - headers: Key => Value list of extra headers for the email * - theme: The View Theme to find the email templates * - config : the name of the email config to be used for sending - * * @return bool */ public static function enqueue($to, array $data, array $options = []) diff --git a/src/Model/Table/EmailQueueTable.php b/src/Model/Table/EmailQueueTable.php index bb408f7..cb58b68 100755 --- a/src/Model/Table/EmailQueueTable.php +++ b/src/Model/Table/EmailQueueTable.php @@ -48,6 +48,8 @@ public function initialize(array $config = []): void * @param array $options list of options for email sending. Possible keys: * * - subject : Email's subject + * - cc: array of carbon copy + * - bcc: array of blind carbon copy * - send_at : date time sting representing the time this email should be sent at (in UTC) * - template : the name of the element to use as template for the email message * - layout : the name of the layout to be used to wrap email message @@ -66,6 +68,8 @@ public function enqueue($to, array $data, array $options = []): bool $defaults = [ 'subject' => '', + 'cc' => '', + 'bcc' => '', 'send_at' => new FrozenTime('now'), 'template' => 'default', 'layout' => 'default', @@ -199,6 +203,8 @@ protected function _initializeSchema(TableSchemaInterface $schema): TableSchemaI $schema->setColumnType('template_vars', $type); $schema->setColumnType('headers', $type); $schema->setColumnType('attachments', $type); + $schema->setColumnType('cc', $type); + $schema->setColumnType('bcc', $type); return $schema; } diff --git a/src/Shell/PreviewShell.php b/src/Shell/PreviewShell.php index 1ab9db5..e8e519e 100755 --- a/src/Shell/PreviewShell.php +++ b/src/Shell/PreviewShell.php @@ -61,6 +61,16 @@ public function preview($e) $email = new Mailer($configName); + // set cc + if (!empty($e->cc)) { + $email->setCC($e->cc); + } + + // set bcc + if (!empty($e->bcc)) { + $email->setBcc($e->bcc); + } + if (!empty($e['attachments'])) { $email->setAttachments($e['attachments']); } diff --git a/src/Shell/SenderShell.php b/src/Shell/SenderShell.php index b415938..5fc0af4 100755 --- a/src/Shell/SenderShell.php +++ b/src/Shell/SenderShell.php @@ -115,6 +115,16 @@ public function main(): void $transport->setConfig(['additionalParameters' => "-f $from"]); } + // set cc + if (!empty($e->cc)) { + $email->setCC($e->cc); + } + + // set bcc + if (!empty($e->bcc)) { + $email->setBcc($e->bcc); + } + if (!empty($e->attachments)) { $email->setAttachments($e->attachments); } diff --git a/tests/Fixture/EmailQueueFixture.php b/tests/Fixture/EmailQueueFixture.php index 62d901c..cd1c11a 100755 --- a/tests/Fixture/EmailQueueFixture.php +++ b/tests/Fixture/EmailQueueFixture.php @@ -19,6 +19,8 @@ class EmailQueueFixture extends TestFixture public $fields = [ 'id' => ['type' => 'uuid', 'null' => false], 'email' => ['type' => 'string', 'null' => false, 'default' => null, 'length' => 100, 'collate' => 'utf8_general_ci', 'charset' => 'utf8'], + 'cc' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 129, 'collate' => 'utf8_general_ci', 'charset' => 'utf8'], + 'bcc' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 129, 'collate' => 'utf8_general_ci', 'charset' => 'utf8'], 'from_name' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 100, 'collate' => 'utf8_general_ci', 'charset' => 'utf8'], 'from_email' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 100, 'collate' => 'utf8_general_ci', 'charset' => 'utf8'], 'subject' => ['type' => 'string', 'null' => false, 'default' => null, 'length' => 255, 'collate' => 'utf8_general_ci', 'charset' => 'utf8'], diff --git a/tests/TestCase/Model/Table/EmailQueueTest.php b/tests/TestCase/Model/Table/EmailQueueTest.php index d85dd98..95cd941 100755 --- a/tests/TestCase/Model/Table/EmailQueueTest.php +++ b/tests/TestCase/Model/Table/EmailQueueTest.php @@ -73,6 +73,8 @@ public function testEnqueue() 'error' => null, 'from_name' => null, 'from_email' => null, + 'cc' => '', + 'bcc' => '' ]; $sendAt = new Time($result['send_at']); unset($result['id'], $result['created'], $result['modified'], $result['send_at']); @@ -197,4 +199,28 @@ public function testProxy() $this->assertEquals('custom', $email['template']); $this->assertEquals('email', $email['layout']); } + + /** + * Test cc and bcc with string and array + */ + public function testCCAndBcc() + { + $cc = ['cc@example.com', 'cc2@example.com']; + $bcc = 'bcc@example.com'; + + $result = EmailQueue::enqueue( + 'd@example.com', + ['a' => 'c'], + ['subject' => 'Hey', 'cc' => $cc, 'bcc' => $bcc, 'template' => 'custom', 'layout' => 'email'] + ); + $this->assertTrue($result); + $email = $this->EmailQueue->find() + ->where(['email' => 'd@example.com']) + ->first() + ->toArray(); + + $this->assertEquals($cc[0], $email['cc'][0]); + $this->assertEquals($cc[1], $email['cc'][1]); + $this->assertEquals($bcc, $email['bcc']); + } } From 692c5dd70996c6d24a3a97cee64a5712c169b080 Mon Sep 17 00:00:00 2001 From: Philip Rogge Date: Fri, 5 Nov 2021 08:53:00 +0100 Subject: [PATCH 2/4] Added possibility for CC and BCC Fixed bug in PreviewShell --- src/Shell/PreviewShell.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Shell/PreviewShell.php b/src/Shell/PreviewShell.php index e8e519e..eb846c7 100755 --- a/src/Shell/PreviewShell.php +++ b/src/Shell/PreviewShell.php @@ -62,13 +62,13 @@ public function preview($e) $email = new Mailer($configName); // set cc - if (!empty($e->cc)) { - $email->setCC($e->cc); + if (!empty($e['cc'])) { + $email->setCC($e['cc']); } // set bcc - if (!empty($e->bcc)) { - $email->setBcc($e->bcc); + if (!empty($e['bcc'])) { + $email->setBcc($e['bcc']); } if (!empty($e['attachments'])) { From b8db22534fcf6492251c739c7e6fa81208065ff9 Mon Sep 17 00:00:00 2001 From: Steve Kirsch Date: Fri, 24 Mar 2023 00:25:27 +0100 Subject: [PATCH 3/4] implemented commands --- src/Command/PreviewCommand.php | 131 ++++++ src/Command/SenderCommand.php | 218 ++++++++++ src/Database/Type/SerializeType.php | 111 ++--- src/Model/Table/EmailQueueTable.php | 383 +++++++++--------- tests/TestCase/Command/PreviewCommandTest.php | 50 +++ tests/TestCase/Command/SenderCommandTest.php | 50 +++ 6 files changed, 698 insertions(+), 245 deletions(-) create mode 100644 src/Command/PreviewCommand.php create mode 100644 src/Command/SenderCommand.php create mode 100644 tests/TestCase/Command/PreviewCommandTest.php create mode 100644 tests/TestCase/Command/SenderCommandTest.php diff --git a/src/Command/PreviewCommand.php b/src/Command/PreviewCommand.php new file mode 100644 index 0000000..6bbac19 --- /dev/null +++ b/src/Command/PreviewCommand.php @@ -0,0 +1,131 @@ +io = &$io; + Configure::write('App.baseUrl', '/'); + + $conditions = []; + if ($args->getArgumentAt(0)) { + $conditions['id IN'] = $args->getArgumentAt(0); + } + + $emailQueue = TableRegistry::getTableLocator()->get('EmailQueue', ['className' => EmailQueueTable::class]); + $emails = $emailQueue->find()->where($conditions)->all()->toList(); + + if (!$emails) { + $this->io->out('No emails found'); + + return; + } + + // $this->io->clear(); + foreach ($emails as $i => $email) { + if ($i) { + $this->io->ask('Hit a key to continue'); + // $this->clear(); + } + $this->io->out('Email :' . $email['id']); + $this->preview($email); + } + } + + /** + * Preview email + * + * @param array $e email data + * @return void + */ + public function preview($e) + { + $configName = $e['config']; + $template = $e['template']; + $layout = $e['layout']; + $headers = empty($e['headers']) ? [] : (array)$e['headers']; + $theme = empty($e['theme']) ? '' : (string)$e['theme']; + + $email = new Mailer($configName); + + // set cc + if (!empty($e['cc'])) { + $email->setCC($e['cc']); + } + + // set bcc + if (!empty($e['bcc'])) { + $email->setBcc($e['bcc']); + } + + if (!empty($e['attachments'])) { + $email->setAttachments(unserialize($e['attachments'])); + } + + $email->setTransport('Debug') + ->setTo($e['email']) + ->setSubject($e['subject']) + ->setEmailFormat($e['format']) + ->addHeaders($headers) + ->setMessageId(false) + ->setReturnPath($email->getFrom()) + ->setViewVars(unserialize($e['template_vars'])); + + $email->viewBuilder() + ->setTheme($theme) + ->setTemplate($template) + ->setLayout($layout); + + $return = $email->deliver(); + + $this->io->out('Content:'); + $this->io->hr(); + $this->io->out($return['message']); + $this->io->hr(); + $this->io->out('Headers:'); + $this->io->hr(); + $this->io->out($return['headers']); + $this->io->hr(); + $this->io->out('Data:'); + $this->io->hr(); + debug($e['template_vars']); + $this->io->hr(); + $this->io->out(''); + } +} diff --git a/src/Command/SenderCommand.php b/src/Command/SenderCommand.php new file mode 100644 index 0000000..f6dd759 --- /dev/null +++ b/src/Command/SenderCommand.php @@ -0,0 +1,218 @@ +setDescription('Sends queued emails in a batch') + ->addOption( + 'limit', + [ + 'short' => 'l', + 'help' => 'How many emails should be sent in this batch?', + 'default' => 50, + ] + ) + ->addOption( + 'template', + [ + 'short' => 't', + 'help' => 'Name of the template to be used to render email', + 'default' => 'default', + ] + ) + ->addOption( + 'layout', + [ + 'short' => 'w', + 'help' => 'Name of the layout to be used to wrap template', + 'default' => 'default', + ] + ) + ->addOption( + 'stagger', + [ + 'short' => 's', + 'help' => 'Seconds to maximum wait randomly before proceeding (useful for parallel executions)', + 'default' => false, + ] + ) + ->addOption( + 'config', + [ + 'short' => 'c', + 'help' => 'Name of email settings to use as defined in email.php', + 'default' => 'default', + ] + ) + /* ->addSubCommand( + 'clearLocks', + [ + 'help' => 'Clears all locked emails in the queue, useful for recovering from crashes', + ] + ) */; + + + // $this->params = []; + + // $this->params['stagger'] = $parser-> + + return $parser; + } + + protected function _fillParams(Arguments $args) + { + $this->params = [ + 'stagger' => $args->getArgument('stagger'), + 'layout' => $args->getArgument('layout'), + 'config' => $args->getArgument('config'), + 'template' => $args->getArgument('template'), + 'limit' => $args->getArgument('limit'), + ]; + } + + /** + * Implement this method with your command's logic. + * + * @param \Cake\Console\Arguments $args The command arguments. + * @param \Cake\Console\ConsoleIo $io The console io + * @return null|void|int The exit code or null for success + */ + public function execute(Arguments $args, ConsoleIo $io) + { + $this->_fillParams($args); + $this->io = &$io; + if ($this->params['stagger']) { + sleep(random_int(0, $this->params['stagger'])); + } + + Configure::write('App.baseUrl', '/'); + // $emailQueue = TableRegistry::getTableLocator()->get('EmailQueue', ['className' => EmailQueueTable::class]); + $emailQueue = $this->fetchTable('EmailQueueTable', [ + 'className' => EmailQueueTable::class + ]); + $emails = $emailQueue->getBatch($this->params['limit']); + + $count = count($emails); + foreach ($emails as $e) { + $configName = $e->config === 'default' ? $this->params['config'] : $e->config; + $template = $e->template === 'default' ? $this->params['template'] : $e->template; + $layout = $e->layout === 'default' ? $this->params['layout'] : $e->layout; + $headers = empty($e->headers) ? [] : (array)$e->headers; + $theme = empty($e->theme) ? '' : (string)$e->theme; + $viewVars = empty($e->template_vars) ? [] : $e->template_vars; + $errorMessage = null; + + try { + $email = $this->_newEmail($configName); + + if (!empty($e->from_email) && !empty($e->from_name)) { + $email->setFrom($e->from_email, $e->from_name); + } + + $transport = $email->getTransport(); + + if ($transport && $transport->getConfig('additionalParameters')) { + $from = key($email->getFrom()); + $transport->setConfig(['additionalParameters' => "-f $from"]); + } + + // set cc + if (!empty($e->cc)) { + $email->setCC($e->cc); + } + + // set bcc + if (!empty($e->bcc)) { + $email->setBcc($e->bcc); + } + + if (!empty($e->attachments)) { + $email->setAttachments($e->attachments); + } + + $sent = $email + ->setTo($e->email) + ->setSubject($e->subject) + ->setEmailFormat($e->format) + ->addHeaders($headers) + ->setViewVars($viewVars) + ->setMessageId(false) + ->setReturnPath($email->getFrom()); + + $email->viewBuilder() + ->setLayout($layout) + ->setTheme($theme) + ->setTemplate($template); + + $email->deliver(); + } catch (SocketException $exception) { + $this->io->error($exception->getMessage()); + $errorMessage = $exception->getMessage(); + $sent = false; + } + + if ($sent) { + $emailQueue->success($e->id); + $this->io->out('Email ' . $e->id . ' was sent'); + } else { + $emailQueue->fail($e->id, $errorMessage); + $this->io->out('Email ' . $e->id . ' was not sent'); + } + } + if ($count > 0) { + $locks = collection($emails)->extract('id')->toList(); + $emailQueue->releaseLocks($locks); + } + } + + /** + * Clears all locked emails in the queue, useful for recovering from crashes. + * + * @return void + */ + public function clearLocks(): void + { + TableRegistry::getTableLocator() + ->get('EmailQueue', ['className' => EmailQueueTable::class]) + ->clearLocks(); + } + + /** + * Returns a new instance of CakeEmail. + * + * @param array|string $config array of configs, or string to load configs from app.php + * @return \Cake\Mailer\Mailer + */ + protected function _newEmail($config): Mailer + { + return new Mailer($config); + } +} diff --git a/src/Database/Type/SerializeType.php b/src/Database/Type/SerializeType.php index 8a7d5af..81b409f 100755 --- a/src/Database/Type/SerializeType.php +++ b/src/Database/Type/SerializeType.php @@ -1,4 +1,5 @@ __toString(); - } + if (is_object($value) && method_exists($value, '__toString')) { + return $value->__toString(); + } - return serialize($value); - } + return serialize($value); + } - /** - * Marshal - Return the value as is - * - * @param mixed $value php object - * @return mixed|null|string - */ - public function marshal($value) - { - return $value; - } + /** + * Marshal - Return the value as is + * + * @param mixed $value php object + * @return mixed|null|string + */ + public function marshal($value) + { + return $value; + } - /** - * Returns whether the cast to PHP is required to be invoked - * - * @return bool always true - */ - public function requiresToPhpCast(): bool - { - return true; - } + /** + * Returns whether the cast to PHP is required to be invoked + * + * @return bool always true + */ + public function requiresToPhpCast(): bool + { + return true; + } } diff --git a/src/Model/Table/EmailQueueTable.php b/src/Model/Table/EmailQueueTable.php index cb58b68..555c79f 100755 --- a/src/Model/Table/EmailQueueTable.php +++ b/src/Model/Table/EmailQueueTable.php @@ -1,4 +1,5 @@ addBehavior( - 'Timestamp', - [ - 'events' => [ - 'Model.beforeSave' => [ - 'created' => 'new', - 'modified' => 'always', - ], - ], - ] - ); - } - - /** - * Stores a new email message in the queue. - * - * @param mixed $to email or array of emails as recipients - * @param array $data associative array of variables to be passed to the email template - * @param array $options list of options for email sending. Possible keys: - * - * - subject : Email's subject - * - cc: array of carbon copy - * - bcc: array of blind carbon copy - * - send_at : date time sting representing the time this email should be sent at (in UTC) - * - template : the name of the element to use as template for the email message - * - layout : the name of the layout to be used to wrap email message - * - format: Type of template to use (html, text or both) - * - config : the name of the email config to be used for sending - * - * @throws \Exception any exception raised in transactional callback - * @throws \LengthException If `template` option length is greater than maximum allowed length - * @return bool - */ - public function enqueue($to, array $data, array $options = []): bool - { - if (array_key_exists('template', $options) && strlen($options['template']) > self::MAX_TEMPLATE_LENGTH) { - throw new LengthException('`template` length must be less or equal to ' . self::MAX_TEMPLATE_LENGTH); - } - - $defaults = [ - 'subject' => '', - 'cc' => '', - 'bcc' => '', - 'send_at' => new FrozenTime('now'), - 'template' => 'default', - 'layout' => 'default', - 'theme' => '', - 'format' => 'both', - 'headers' => [], - 'template_vars' => $data, - 'config' => 'default', - 'attachments' => [], - ]; - - $email = $options + $defaults; - if (!is_array($to)) { - $to = [$to]; - } - - $emails = []; - foreach ($to as $t) { - $emails[] = ['email' => $t] + $email; - } - - $emails = $this->newEntities($emails); - - return $this->getConnection()->transactional(function () use ($emails) { - $failure = collection($emails) - ->map(function ($email) { - return $this->save($email); - }) - ->contains(false); - - return !$failure; - }); - } - - /** - * Returns a list of queued emails that needs to be sent. - * - * @param int|string $size number of unset emails to return - * @throws \Exception any exception raised in transactional callback - * @return array list of unsent emails - */ - public function getBatch($size = 10): array - { - return $this->getConnection()->transactional(function () use ($size) { - $emails = $this->find() - ->where([ - $this->aliasField('sent') => false, - $this->aliasField('send_tries') . ' <=' => 3, - $this->aliasField('send_at') . ' <=' => new FrozenTime('now'), - $this->aliasField('locked') => false, - ]) - ->limit($size) - ->order([$this->aliasField('created') => 'ASC']); - - $emails - ->extract('id') - ->through(function (\Cake\Collection\CollectionInterface $ids) { - if (!$ids->isEmpty()) { - $this->updateAll(['locked' => true], ['id IN' => $ids->toList()]); - } - - return $ids; - }); - - return $emails->toList(); - }); - } - - /** - * Releases locks for all emails in $ids. - * - * @param array|\Traversable $ids The email ids to unlock - * - * @return void - */ - public function releaseLocks($ids): void - { - $this->updateAll(['locked' => false], ['id IN' => $ids]); - } - - /** - * Releases locks for all emails in queue, useful for recovering from crashes. - * - * @return void - */ - public function clearLocks(): void - { - $this->updateAll(['locked' => false], '1=1'); - } - - /** - * Marks an email from the queue as sent. - * - * @param string $id queued email id - * @return void - */ - public function success($id): void - { - $this->updateAll(['sent' => true], ['id' => $id]); - } - - /** - * Marks an email from the queue as failed, and increments the number of tries. - * - * @param string $id queued email id - * @param string $error message - * @return void - */ - public function fail($id, $error = null): void - { - $this->updateAll( - [ - 'send_tries' => new QueryExpression('send_tries + 1'), - 'error' => $error, - ], - [ - 'id' => $id, - ] - ); - } - - /** - * Sets the column type for template_vars and headers to json. - * - * @param \Cake\Database\Schema\TableSchemaInterface $schema The table description - * @return \Cake\Database\Schema\TableSchema - */ - protected function _initializeSchema(TableSchemaInterface $schema): TableSchemaInterface - { - $type = Configure::read('EmailQueue.serialization_type') ?: 'email_queue.serialize'; - $schema->setColumnType('template_vars', $type); - $schema->setColumnType('headers', $type); - $schema->setColumnType('attachments', $type); - $schema->setColumnType('cc', $type); - $schema->setColumnType('bcc', $type); - - return $schema; - } + public const MAX_TEMPLATE_LENGTH = 100; + + /** + * {@inheritdoc} + */ + public function initialize(array $config = []): void + { + TypeFactory::map('email_queue.json', JsonType::class); + TypeFactory::map('email_queue.serialize', SerializeType::class); + $this->addBehavior( + 'Timestamp', + [ + 'events' => [ + 'Model.beforeSave' => [ + 'created' => 'new', + 'modified' => 'always', + ], + ], + ] + ); + } + + /** + * Stores a new email message in the queue. + * + * @param mixed $to email or array of emails as recipients + * @param array $data associative array of variables to be passed to the email template + * @param array $options list of options for email sending. Possible keys: + * + * - subject : Email's subject + * - cc: array of carbon copy + * - bcc: array of blind carbon copy + * - send_at : date time sting representing the time this email should be sent at (in UTC) + * - template : the name of the element to use as template for the email message + * - layout : the name of the layout to be used to wrap email message + * - format: Type of template to use (html, text or both) + * - config : the name of the email config to be used for sending + * + * @throws \Exception any exception raised in transactional callback + * @throws \LengthException If `template` option length is greater than maximum allowed length + * @return bool + */ + public function enqueue($to, array $data, array $options = []): bool + { + if (array_key_exists('template', $options) && strlen($options['template']) > self::MAX_TEMPLATE_LENGTH) { + throw new LengthException('`template` length must be less or equal to ' . self::MAX_TEMPLATE_LENGTH); + } + + $defaults = [ + 'subject' => '', + 'cc' => '', + 'bcc' => '', + 'send_at' => new FrozenTime('now'), + 'template' => 'default', + 'layout' => 'default', + 'theme' => '', + 'format' => 'both', + 'headers' => [], + 'template_vars' => $data, + 'config' => 'default', + 'attachments' => [], + ]; + + $email = $options + $defaults; + if (!is_array($to)) { + $to = [$to]; + } + + $emails = []; + foreach ($to as $t) { + $emails[] = ['email' => $t] + $email; + } + + $emails = $this->newEntities($emails); + + return $this->getConnection()->transactional(function () use ($emails) { + $failure = collection($emails) + ->map(function ($email) { + return $this->save($email); + }) + ->contains(false); + + return !$failure; + }); + } + + /** + * Returns a list of queued emails that needs to be sent. + * + * @param int|string $size number of unset emails to return + * @throws \Exception any exception raised in transactional callback + * @return array list of unsent emails + */ + public function getBatch($size = 10): array + { + return $this->getConnection()->transactional(function () use ($size) { + $emails = $this->find() + ->where([ + $this->aliasField('sent') => false, + $this->aliasField('send_tries') . ' <=' => 3, + $this->aliasField('send_at') . ' <=' => new FrozenTime('now'), + $this->aliasField('locked') => false, + ]) + ->limit($size) + ->order([$this->aliasField('created') => 'ASC']) + ->all(); + + $emails + ->extract('id') + ->through(function (\Cake\Collection\CollectionInterface $ids) { + if (!$ids->isEmpty()) { + $this->updateAll(['locked' => true], ['id IN' => $ids->toList()]); + } + + return $ids; + }); + + return $emails->toList(); + }); + } + + /** + * Releases locks for all emails in $ids. + * + * @param array|\Traversable $ids The email ids to unlock + * + * @return void + */ + public function releaseLocks($ids): void + { + $this->updateAll(['locked' => false], ['id IN' => $ids]); + } + + /** + * Releases locks for all emails in queue, useful for recovering from crashes. + * + * @return void + */ + public function clearLocks(): void + { + $this->updateAll(['locked' => false], '1=1'); + } + + /** + * Marks an email from the queue as sent. + * + * @param string $id queued email id + * @return void + */ + public function success($id): void + { + $this->updateAll(['sent' => true], ['id' => $id]); + } + + /** + * Marks an email from the queue as failed, and increments the number of tries. + * + * @param string $id queued email id + * @param string $error message + * @return void + */ + public function fail($id, $error = null): void + { + $this->updateAll( + [ + 'send_tries' => new QueryExpression('send_tries + 1'), + 'error' => $error, + ], + [ + 'id' => $id, + ] + ); + } + + /** + * Sets the column type for template_vars and headers to json. + * + * @param \Cake\Database\Schema\TableSchemaInterface $schema The table description + * @return \Cake\Database\Schema\TableSchema + */ + protected function _initializeSchema(TableSchemaInterface $schema): TableSchemaInterface + { + $type = Configure::read('EmailQueue.serialization_type') ?: 'email_queue.serialize'; + $schema->setColumnType('template_vars', $type); + $schema->setColumnType('headers', $type); + $schema->setColumnType('attachments', $type); + $schema->setColumnType('cc', $type); + $schema->setColumnType('bcc', $type); + + return $schema; + } } diff --git a/tests/TestCase/Command/PreviewCommandTest.php b/tests/TestCase/Command/PreviewCommandTest.php new file mode 100644 index 0000000..aa6c0d0 --- /dev/null +++ b/tests/TestCase/Command/PreviewCommandTest.php @@ -0,0 +1,50 @@ +useCommandRunner(); + } + /** + * Test buildOptionParser method + * + * @return void + * @uses \EmailQueue\Command\PreviewCommand::buildOptionParser() + */ + public function testBuildOptionParser(): void + { + $this->markTestIncomplete('Not implemented yet.'); + } + + /** + * Test execute method + * + * @return void + * @uses \EmailQueue\Command\PreviewCommand::execute() + */ + public function testExecute(): void + { + $this->markTestIncomplete('Not implemented yet.'); + } +} diff --git a/tests/TestCase/Command/SenderCommandTest.php b/tests/TestCase/Command/SenderCommandTest.php new file mode 100644 index 0000000..242d891 --- /dev/null +++ b/tests/TestCase/Command/SenderCommandTest.php @@ -0,0 +1,50 @@ +useCommandRunner(); + } + /** + * Test buildOptionParser method + * + * @return void + * @uses \EmailQueue\Command\SenderCommand::buildOptionParser() + */ + public function testBuildOptionParser(): void + { + $this->markTestIncomplete('Not implemented yet.'); + } + + /** + * Test execute method + * + * @return void + * @uses \EmailQueue\Command\SenderCommand::execute() + */ + public function testExecute(): void + { + $this->markTestIncomplete('Not implemented yet.'); + } +} From 15d8c68179821a05f469955131b4a5f98c68ef33 Mon Sep 17 00:00:00 2001 From: Steve Kirsch Date: Fri, 24 Mar 2023 00:25:53 +0100 Subject: [PATCH 4/4] changed file perms --- LICENSE | 0 config/Migrations/20181120010607_add_error_message.php | 0 .../Migrations/20190610024410_AlterTemplateVarsToEmailQueue.php | 0 config/Migrations/20190814000000_AlterTemplateToEmailQueue.php | 0 config/Migrations/20211105000000_AddCCAndBccToEmailQueue.php | 0 src/Command/PreviewCommand.php | 0 src/Command/SenderCommand.php | 0 tests/TestCase/Command/PreviewCommandTest.php | 0 tests/TestCase/Command/SenderCommandTest.php | 0 tests/test_app/src/Mailer/TestMailer.php | 0 10 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 LICENSE mode change 100644 => 100755 config/Migrations/20181120010607_add_error_message.php mode change 100644 => 100755 config/Migrations/20190610024410_AlterTemplateVarsToEmailQueue.php mode change 100644 => 100755 config/Migrations/20190814000000_AlterTemplateToEmailQueue.php mode change 100644 => 100755 config/Migrations/20211105000000_AddCCAndBccToEmailQueue.php mode change 100644 => 100755 src/Command/PreviewCommand.php mode change 100644 => 100755 src/Command/SenderCommand.php mode change 100644 => 100755 tests/TestCase/Command/PreviewCommandTest.php mode change 100644 => 100755 tests/TestCase/Command/SenderCommandTest.php mode change 100644 => 100755 tests/test_app/src/Mailer/TestMailer.php diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/config/Migrations/20181120010607_add_error_message.php b/config/Migrations/20181120010607_add_error_message.php old mode 100644 new mode 100755 diff --git a/config/Migrations/20190610024410_AlterTemplateVarsToEmailQueue.php b/config/Migrations/20190610024410_AlterTemplateVarsToEmailQueue.php old mode 100644 new mode 100755 diff --git a/config/Migrations/20190814000000_AlterTemplateToEmailQueue.php b/config/Migrations/20190814000000_AlterTemplateToEmailQueue.php old mode 100644 new mode 100755 diff --git a/config/Migrations/20211105000000_AddCCAndBccToEmailQueue.php b/config/Migrations/20211105000000_AddCCAndBccToEmailQueue.php old mode 100644 new mode 100755 diff --git a/src/Command/PreviewCommand.php b/src/Command/PreviewCommand.php old mode 100644 new mode 100755 diff --git a/src/Command/SenderCommand.php b/src/Command/SenderCommand.php old mode 100644 new mode 100755 diff --git a/tests/TestCase/Command/PreviewCommandTest.php b/tests/TestCase/Command/PreviewCommandTest.php old mode 100644 new mode 100755 diff --git a/tests/TestCase/Command/SenderCommandTest.php b/tests/TestCase/Command/SenderCommandTest.php old mode 100644 new mode 100755 diff --git a/tests/test_app/src/Mailer/TestMailer.php b/tests/test_app/src/Mailer/TestMailer.php old mode 100644 new mode 100755