diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..99d0dc2 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,13 @@ +categories: + - title: "Breaking Changes" + labels: + - "BC-break" + - title: "Major Features" + labels: + - "MAJOR" + - title: "Documentation enhancements" + labels: + - "Documentation :books:" +template: | + ## What’s Changed + $CHANGES diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml new file mode 100644 index 0000000..ab60038 --- /dev/null +++ b/.github/workflows/build-docs.yml @@ -0,0 +1,15 @@ +name: Build Docs + +on: + push: + branches: + - develop + +jobs: + update_release_draft: + runs-on: ubuntu-latest + steps: + - name: Run Release Drafter + uses: release-drafter/release-drafter@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml new file mode 100644 index 0000000..c4af944 --- /dev/null +++ b/.github/workflows/build-release.yml @@ -0,0 +1,56 @@ +name: Build Release + +on: + push: + branches: + - '**\.build' + - 'release/*' + - '!**\.gen' + +jobs: + autocommit: + name: Build Release + runs-on: ubuntu-latest + container: + image: atk4/image:latest + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.ref }} + + - name: Install PHP dependencies + run: composer update --ansi --prefer-dist --no-interaction --no-progress --optimize-autoloader + + - name: Composer unset version + run: composer config version --unset + + - name: Update composer.json + run: >- + php -r ' + $f = __DIR__ . "/composer.json"; + $data = json_decode(file_get_contents($f), true); + foreach ($data as $k => $v) { + if (preg_match("~^(.+)-release$~", $k, $matches)) { + $data[$matches[1]] = $data[$k]; unset($data[$k]); + } + } + $str = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n"; + echo $str; + file_put_contents($f, $str); + ' + + - name: Composer validate config + run: composer validate --strict --no-check-lock && composer normalize --dry-run --no-check-lock + + - name: Commit + run: | + git config --global user.name "$(git show -s --format='%an')" + git config --global user.email "$(git show -s --format='%ae')" + git add -A && git diff --staged && git commit -m "Build Release" + + - name: Push + uses: ad-m/github-push-action@master + with: + branch: ${{ github.ref }}.gen + force: true + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml new file mode 100644 index 0000000..a917f27 --- /dev/null +++ b/.github/workflows/test-unit.yml @@ -0,0 +1,137 @@ +name: Unit + +on: + pull_request: + push: + schedule: + - cron: '0 0/2 * * *' + +jobs: + smoke-test: + name: Smoke + runs-on: ubuntu-latest + container: + image: atk4/image:${{ matrix.php }} + strategy: + fail-fast: false + matrix: + php: ['latest'] + type: ['Phpunit'] + include: + - php: 'latest' + type: 'CodingStyle' + - php: 'latest' + type: 'StaticAnalysis' + env: + LOG_COVERAGE: "" + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Configure PHP + run: | + if [ -n "$LOG_COVERAGE" ]; then echo "xdebug.mode=coverage" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini; else rm /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini; fi + php --version + + - name: Setup cache 1/2 + id: composer-cache + run: | + echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Setup cache 2/2 + if: ${{ !env.ACT }} + uses: actions/cache@v1 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-smoke-${{ matrix.php }}-${{ matrix.type }}-${{ hashFiles('composer.json') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Install PHP dependencies + run: | + if [ "${{ matrix.type }}" != "Phpunit" ] && [ "${{ matrix.type }}" != "StaticAnalysis" ]; then composer remove --no-interaction --no-update phpunit/phpunit johnkary/phpunit-speedtrap --dev ; fi + if [ "${{ matrix.type }}" != "CodingStyle" ]; then composer remove --no-interaction --no-update friendsofphp/php-cs-fixer --dev ; fi + if [ "${{ matrix.type }}" != "StaticAnalysis" ]; then composer remove --no-interaction --no-update phpstan/phpstan --dev ; fi + composer update --ansi --prefer-dist --no-interaction --no-progress --optimize-autoloader + + - name: Init + run: | + mkdir -p build/logs + + - name: "Run tests: Phpunit (only for Phpunit)" + if: matrix.type == 'Phpunit' + run: "vendor/bin/phpunit \"$(if [ -n \"$LOG_COVERAGE\" ]; then echo '--coverage-text'; else echo '--no-coverage'; fi)\" -v" + + - name: Check Coding Style (only for CodingStyle) + if: matrix.type == 'CodingStyle' + run: vendor/bin/php-cs-fixer fix --dry-run --using-cache=no --diff --verbose --show-progress=dots + + - name: Run Static Analysis (only for StaticAnalysis) + if: matrix.type == 'StaticAnalysis' + run: | + echo "memory_limit = 1G" > /usr/local/etc/php/conf.d/custom-memory-limit.ini + vendor/bin/phpstan analyse + + unit-test: + name: Unit + runs-on: ubuntu-latest + container: + image: atk4/image:${{ matrix.php }} + strategy: + fail-fast: false + matrix: + php: ['7.4', 'latest'] + type: ['Phpunit'] + include: + - php: 'latest' + type: 'Phpunit Lowest' + - php: 'latest' + type: 'Phpunit Burn' + env: + LOG_COVERAGE: "${{ fromJSON('{true: \"1\", false: \"\"}')[matrix.php == 'latest' && matrix.type == 'Phpunit' && (github.event_name == 'pull_request' || (github.event_name == 'push' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master')))] }}" + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Configure PHP + run: | + if [ -n "$LOG_COVERAGE" ]; then echo "xdebug.mode=coverage" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini; else rm /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini; fi + php --version + + - name: Setup cache 1/2 + id: composer-cache + run: | + echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Setup cache 2/2 + if: ${{ !env.ACT }} + uses: actions/cache@v1 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ matrix.type }}-${{ hashFiles('composer.json') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Install PHP dependencies + run: | + if [ "${{ matrix.type }}" != "Phpunit" ] && [ "${{ matrix.type }}" != "Phpunit Lowest" ] && [ "${{ matrix.type }}" != "Phpunit Burn" ]; then composer remove --no-interaction --no-update phpunit/phpunit --no-update phpunit/phpunit johnkary/phpunit-speedtrap --dev ; fi + if [ "${{ matrix.type }}" != "CodingStyle" ]; then composer remove --no-interaction --no-update friendsofphp/php-cs-fixer --dev ; fi + if [ "${{ matrix.type }}" != "StaticAnalysis" ]; then composer remove --no-interaction --no-update phpstan/phpstan --dev ; fi + composer update --ansi --prefer-dist --no-interaction --no-progress --optimize-autoloader + if [ "${{ matrix.type }}" == "Phpunit Lowest" ]; then composer update --ansi --prefer-dist --prefer-lowest --prefer-stable --no-interaction --no-progress --optimize-autoloader ; fi + if [ "${{ matrix.type }}" == "Phpunit Burn" ]; then sed -i 's/ *public function runBare(): void/public function runBare(): void { gc_collect_cycles(); $mem0 = memory_get_usage(); for ($i = 0; $i < '"$(if [ \"$GITHUB_EVENT_NAME\" == \"schedule\" ]; then echo 5; else echo 5; fi)"'; ++$i) { $this->_runBare(); if ($i === 0) { gc_collect_cycles(); $mem1 = memory_get_usage(); } } gc_collect_cycles(); $mem2 = memory_get_usage(); if ($mem2 - 4000 * 1024 > $mem0 || $mem2 - 1536 * 1024 > $mem1) { $this->onNotSuccessfulTest(new AssertionFailedError("Memory leak detected! (" . round($mem0 \/ (1024 * 1024), 3) . " + " . round(($mem1 - $mem0) \/ (1024 * 1024), 3) . " + " . round(($mem2 - $mem1) \/ (1024 * 1024), 3) . " MB, " . $i . " iterations)")); } } private function _runBare(): void/' vendor/phpunit/phpunit/src/Framework/TestCase.php && cat vendor/phpunit/phpunit/src/Framework/TestCase.php | grep '_runBare(' ; fi + + - name: Init + run: | + mkdir -p build/logs + + - name: "Run tests: Phpunit (only for Phpunit)" + if: startsWith(matrix.type, 'Phpunit') + run: "vendor/bin/phpunit \"$(if [ -n \"$LOG_COVERAGE\" ]; then echo '--coverage-text'; else echo '--no-coverage'; fi)\" -v" + + - name: Upload coverage logs (only for "latest" Phpunit) + if: env.LOG_COVERAGE + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: build/logs/clover.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c95aff4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +docs/build +/composer.lock +/build +/vendor +.DS_Store +.idea +/demos/db.sqlite diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..43cb550 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,69 @@ +in([__DIR__]) + ->exclude([ + 'vendor', + ]); + +$config = new \PhpCsFixer\Config(); +$config->setRiskyAllowed(true) + ->setRules([ + '@PhpCsFixer' => true, + '@PhpCsFixer:risky' =>true, + '@PHP71Migration:risky' => true, + '@PHP73Migration' => true, + + // required by PSR-12 + 'concat_space' => [ + 'spacing' => 'one', + ], + + // disable some too strict rules + 'phpdoc_types' => [ + // keep enabled, but without "alias" group to not fix + // "Callback" to "callback" in phpdoc + 'groups' => ['simple', 'meta'] + ], + 'phpdoc_types_order' => [ + 'null_adjustment' => 'always_last', + 'sort_algorithm' => 'none', + ], + 'single_line_throw' => false, + 'yoda_style' => [ + 'equal' => false, + 'identical' => false, + ], + 'native_function_invocation' => false, + 'non_printable_character' => [ + 'use_escape_sequences_in_strings' => true, + ], + 'void_return' => false, + 'blank_line_before_statement' => [ + 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'exit'], + ], + 'combine_consecutive_issets' => false, + 'combine_consecutive_unsets' => false, + 'multiline_whitespace_before_semicolons' => false, + 'no_superfluous_elseif' => false, + 'ordered_class_elements' => false, + 'php_unit_internal_class' => false, + 'php_unit_test_case_static_method_calls' => [ + 'call_type' => 'this', + ], + 'php_unit_test_class_requires_covers' => false, + 'phpdoc_add_missing_param_annotation' => false, + 'return_assignment' => false, + 'comment_to_phpdoc' => false, + 'list_syntax' => ['syntax' => 'short'], + 'general_phpdoc_annotation_remove' => [ + 'annotations' => ['author', 'copyright', 'throws'], + ], + 'nullable_type_declaration_for_default_null_value' => [ + 'use_nullable_type_declaration' => false, + ], + ]) + ->setFinder($finder) + ->setCacheFile(__DIR__ . '/.php_cs.cache'); + +return $config; \ No newline at end of file diff --git a/.php_cs.dist b/.php_cs.dist deleted file mode 100644 index 2b43804..0000000 --- a/.php_cs.dist +++ /dev/null @@ -1,109 +0,0 @@ -setRiskyAllowed(true) - ->setRules([ - '@PSR2' => true, - // Each line of multi-line DocComments must have an asterisk [PSR-5] and must be aligned with the first one. - 'align_multiline_comment' => ['comment_type' => 'phpdocs_like'], - // PHP arrays should be declared using the configured syntax. - 'array_syntax' => ['syntax' => 'short'], - // Binary operators should be surrounded by space as configured. - 'binary_operator_spaces' => [ - 'align_double_arrow' => true, - 'align_equals' => true - ], - // An empty line feed should precede a return statement. - 'blank_line_before_return' => true, - // Concatenation should be spaced according configuration. - 'concat_space' => true, - // Equal sign in declare statement should be surrounded by spaces or not following configuration. - 'declare_equal_normalize' => true, - // Force strict types declaration in all files. - // Requires PHP >= 7.0. - 'declare_strict_types' => true, - // Transforms imported FQCN parameters and return types in function arguments to short version. - 'fully_qualified_strict_types' => true, - // Ensure there is no code on the same line as the PHP open tag. - 'linebreak_after_opening_tag' => true, - // All instances created with new keyword must be followed by braces. - 'new_with_braces' => true, - // There should not be blank lines between docblock and the documented element. - 'no_blank_lines_after_phpdoc' => true, - // There should not be any empty comments. - 'no_empty_comment' => true, - // There should not be empty PHPDoc blocks. - 'no_empty_phpdoc' => true, - // Remove useless semicolon statements. - 'no_empty_statement' => true, - // Remove trailing commas in list function calls. - 'no_trailing_comma_in_list_call' => true, - // PHP single-line arrays should not have trailing comma. - 'no_trailing_comma_in_singleline_array' => true, - // Removes unneeded curly braces that are superfluous and aren't part of a control structure's body. - 'no_unneeded_curly_braces' => true, - // There should not be useless `else` cases. - 'no_useless_else' => true, - // There should not be an empty `return` statement at the end of a function. - 'no_useless_return' => true, - // Array index should always be written by using square braces. - 'normalize_index_brace' => true, - // PHPDoc should contain `@param` for all params. - 'phpdoc_add_missing_param_annotation' => true, - // All items of the given phpdoc tags must be either left-aligned or (by default) aligned vertically. - 'phpdoc_align' => true, - // PHPDoc annotation descriptions should not be a sentence. - 'phpdoc_annotation_without_dot' => true, - // Docblocks should have the same indentation as the documented subject. - 'phpdoc_indent' => true, - // Fix PHPDoc inline tags, make `@inheritdoc` always inline. - 'phpdoc_inline_tag' => true, - // `@access` annotations should be omitted from PHPDoc. - 'phpdoc_no_access' => true, - // `@return void` and `@return null` annotations should be omitted from PHPDoc. - 'phpdoc_no_empty_return' => true, - // `@package` and `@subpackage` annotations should be omitted from PHPDoc. - 'phpdoc_no_package' => true, - // Classy that does not inherit must not have `@inheritdoc` tags. - 'phpdoc_no_useless_inheritdoc' => true, - // Annotations in PHPDoc should be ordered so that `@param` annotations come first, then `@throws` annotations, then `@return` annotations. - 'phpdoc_order' => true, - // Scalar types should always be written in the same form. - // `int` not `integer`, `bool` not `boolean`, `float` not `real` or `double`. - 'phpdoc_scalar' => true, - // Single line `@var` PHPDoc should have proper spacing. - 'phpdoc_single_line_var_spacing' => true, - // PHPDoc summary should end in either a full stop, exclamation mark, or question mark. - 'phpdoc_summary' => true, - // Docblocks should only be used on structural elements. - 'phpdoc_to_comment' => true, - // PHPDoc should start and end with content, excluding the very first and last line of the docblocks. - 'phpdoc_trim' => true, - // The correct case must be used for standard PHP types in PHPDoc. - 'phpdoc_types' => true, - // There should be one or no space before colon, and one space after it in return type declarations, according to configuration. - 'return_type_declaration' => true, - // Replace all `<>` with `!=`. - 'standardize_not_equals' => true, - // Comparisons should be strict. - 'strict_comparison' => true, - // PHP multi-line arrays should have a trailing comma. - 'trailing_comma_in_multiline_array' => true, - // Add void return type to functions with missing or empty return statements, but priority is given to `@return` annotations. - // Requires PHP >= 7.1. - 'void_return' => true, - // Write conditions in Yoda style (`true`), non-Yoda style (`false`) or ignore those conditions (`null`) based on configuration. - 'yoda_style' => true, - ]) - ->setFinder(PhpCsFixer\Finder::create() - ->exclude('vendor') - ->in(__DIR__) - ); \ No newline at end of file diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/composer.json b/composer.json old mode 100644 new mode 100755 index 6a1ed62..5adf49e --- a/composer.json +++ b/composer.json @@ -1,33 +1,48 @@ { "name": "atk4/outbox", "description": "Agile toolkit Email", - "minimum-stability": "beta", "license": "MIT", "authors": [ + { + "name": "Romans Malinovskis", + "email": "romans@agiletoolkit.org", + "homepage": "https://nearly.guru/" + }, { "name": "Francesco Danti", "email": "fdanti@gmail.com" } ], "require": { - "php": "^7.3", - "atk4/ui": "2.0.4", - "phpmailer/phpmailer": "6.0.x-dev", - "friendsofphp/php-cs-fixer": "*" + "php": ">=7.4.0", + "atk4/ui": "^2.4.0", + "html2text/html2text": "^4.3.1", + "phpmailer/phpmailer": "^v6.4.1" }, "require-dev": { - "phpunit/phpunit": "*", - "satooshi/php-coveralls": "^1.0", - "codeclimate/php-test-reporter": "*" + "atk4/data": "^2.4.0", + "ergebnis/composer-normalize": "^2.13", + "friendsofphp/php-cs-fixer": "^3.0", + "johnkary/phpunit-speedtrap": "^3.2", + "phpstan/phpstan": "^0.12.82", + "phpunit/phpcov": "*", + "phpunit/phpunit": ">=9.3" }, "autoload": { "psr-4": { - "atk4\\outbox\\" : "src/" + "Atk4\\Outbox\\": "src/" } }, "autoload-dev": { "psr-4": { - "atk4\\outbox\\Test\\" : "tests/" + "Atk4\\Outbox\\Test\\": "tests/" } + }, + "minimum-stability": "dev", + "prefer-stable": true, + "require-release": { + "php": ">=7.4.0", + "atk4/ui": "^2.4.0", + "phpmailer/phpmailer": "^v6.4.1" } } diff --git a/demos/db.php b/demos/db.php new file mode 100644 index 0000000..d3619f2 --- /dev/null +++ b/demos/db.php @@ -0,0 +1,23 @@ +dropIfExists()->create(); + (new \Atk4\Schema\Migration(new MailTemplate($db)))->dropIfExists()->create(); + (new \Atk4\Schema\Migration(new MailResponse($db)))->dropIfExists()->create(); + (new \Atk4\Schema\Migration(new User($db)))->dropIfExists()->create(); +} + +return $db; diff --git a/demos/index.php b/demos/index.php new file mode 100644 index 0000000..2f5d73e --- /dev/null +++ b/demos/index.php @@ -0,0 +1,59 @@ + 'Agile Toolkit - Outbox']); +$app->db = include __DIR__ . '/db.php'; +$app->initLayout([Admin::class]); +$app->add([ + Outbox::class, + [ + 'mailer' => [ + FakeMailer::class, + ], + 'model' => [ + Mail::class, + ], + ], +]); + +$loader = Loader::addTo($app); +$loader->set(function (Loader $l) { + $route = $l->getApp()->stickyGet('route'); + $route = empty($route) ? 'mail' : $route; + + switch ($route) { + case 'mail': + MailAdmin::addTo($l); + + break; + case 'template': + MailTemplateAdmin::addTo($l); + + break; + } +}); + +/** @var Admin $layout */ +$layout = $app->layout; + +$layout->menuLeft + ->addItem(['Mail Tracking', 'icon' => 'envelope']) + ->on('click', $loader->jsLoad(['route' => 'mail'])); + +$layout->menuLeft + ->addItem(['Template Admin', 'icon' => 'cogs']) + ->on('click', $loader->jsLoad(['route' => 'template'])); + +$app->run(); diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..ae93292 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,26 @@ +includes: + - vendor/mahalux/atk4-hintable/phpstan-ext.neon + +parameters: + level: 6 + paths: + - ./ + excludes_analyse: + - cache/ + - build/ + - vendor/ + + # TODO review once we drop PHP 7.x support + treatPhpDocTypesAsCertain: false + + # some extra rules + checkAlwaysTrueCheckTypeFunctionCall: true + checkAlwaysTrueInstanceof: true + checkAlwaysTrueStrictComparison: true + checkExplicitMixedMissingReturn: true + checkFunctionNameCase: true + # TODO checkMissingClosureNativeReturnTypehintRule: true + reportMaybesInMethodSignatures: true + reportStaticMethodSignatures: true + checkTooWideReturnTypesInProtectedAndPublicMethods: true + checkMissingIterableValueType: false # TODO \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..0cb4962 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,21 @@ + + + + + ./src + + + + + + + + + + + tests/Bootstrap.php + tests + + + + diff --git a/src/MailAdmin.php b/src/MailAdmin.php new file mode 100644 index 0000000..2f096a1 --- /dev/null +++ b/src/MailAdmin.php @@ -0,0 +1,24 @@ +getApp(); + + $model = new Mail($app->db); + $model->getField('html')->system = true; + $model->addExpression('time', $model->refLink('response')->action('field', ['timestamp'])); + $model->setOrder('id', 'DESC'); + $this->setModel($model); + } +} diff --git a/src/MailTemplateAdmin.php b/src/MailTemplateAdmin.php new file mode 100644 index 0000000..96023af --- /dev/null +++ b/src/MailTemplateAdmin.php @@ -0,0 +1,37 @@ +setModel(new MailTemplate($this->getApp()->db)); + } +} diff --git a/src/Mailer/AbstractMailer.php b/src/Mailer/AbstractMailer.php old mode 100644 new mode 100755 index 36ebbc1..3338905 --- a/src/Mailer/AbstractMailer.php +++ b/src/Mailer/AbstractMailer.php @@ -2,22 +2,17 @@ declare(strict_types=1); -namespace atk4\outbox\Mailer; +namespace Atk4\Outbox\Mailer; -use atk4\core\DIContainerTrait; -use atk4\outbox\MailerInterface; -use atk4\outbox\Model\Mail; -use atk4\outbox\Model\MailResponse; -use Exception; +use Atk4\Core\DiContainerTrait; +use Atk4\Outbox\MailerInterface; +use Atk4\Outbox\Model\Mail; +use Atk4\Outbox\Model\MailResponse; use PHPMailer\PHPMailer\PHPMailer; use PHPMailer\PHPMailer\SMTP as PHPMailerSMTP; class AbstractMailer implements MailerInterface { - const SMTP_SECURE_NULL = ''; - const SMTP_SECURE_TLS = 'tls'; - const SMTP_SECURE_SSL = 'ssl'; - use DIContainerTrait; /** @var PHPMailer */ @@ -31,12 +26,14 @@ class AbstractMailer implements MailerInterface protected $host = 'localhost'; /** @var int */ protected $port = 587; - /** @var int */ - protected $secure = self::SMTP_SECURE_NULL; + /** @var string */ + protected $secure = ''; /** @var string */ protected $username; /** @var string */ protected $password; + /** @var string */ + protected $charset = PHPMailer::CHARSET_UTF8; public function __construct(array $defaults = []) { @@ -52,21 +49,38 @@ public function __construct(array $defaults = []) $this->phpmailer->SMTPAuth = $this->auth; $this->phpmailer->Username = $this->username; $this->phpmailer->Password = $this->password; + + $this->phpmailer->CharSet = $this->charset; } - public function send(Mail $mail): void + public function send(Mail $mail): MailResponse { - $mail_response = $mail->newInstance(MailResponse::class); + $mail_response = new MailResponse($mail->persistence); try { - $from = $mail->ref('from'); - $this->phpmailer->setFrom($from->get('email'), $from->get('name')); + $this->phpmailer->setFrom( + $mail->ref('from')->get('email'), + $mail->ref('from')->get('name') + ); + $this->addAddress( + $mail, + 'to', + function ($address): void { + $this->phpmailer->addAddress( + $address->get('email'), + $address->get('name') + ); + } + ); $this->addAddress( $mail, 'replyto', function ($address): void { - $this->phpmailer->addReplyTo($address->get('email'), $address->get('name')); + $this->phpmailer->addReplyTo( + $address->get('email'), + $address->get('name') + ); } ); @@ -74,7 +88,10 @@ function ($address): void { $mail, 'cc', function ($address): void { - $this->phpmailer->addCC($address->get('email'), $address->get('name')); + $this->phpmailer->addCC( + $address->get('email'), + $address->get('name') + ); } ); @@ -82,15 +99,25 @@ function ($address): void { $mail, 'bcc', function ($address): void { - $this->phpmailer->addBCC($address->get('email'), $address->get('name')); + $this->phpmailer->addBCC( + $address->get('email'), + $address->get('name') + ); } ); $this->phpmailer->Subject = $mail->get('subject'); - $this->phpmailer->msgHTML = $mail->get('html'); + $this->phpmailer->Body = $mail->get('html'); $this->phpmailer->AltBody = $mail->get('text'); - foreach ($mail->ref('attachments') as $model) { + foreach ($mail->ref('headers')->getIterator() as $model) { + $this->phpmailer->addCustomHeader( + $model->get('name'), + $model->get('value') + ); + } + + foreach ($mail->ref('attachments')->getIterator() as $model) { $this->phpmailer->addAttachment( $model->get('path'), $model->get('name'), @@ -107,25 +134,27 @@ function ($address): void { $mail->save(); // save successful MailResponse - $mail_response->save(["email_id"=> $mail->id]); - } catch (\PHPMailer\PHPMailer\Exception $exception) { + $mail_response->save(['email_id' => $mail->id]); + } catch (\Throwable $exception) { $mail->set('status', Mail::STATUS_ERROR); $mail->save(); - // save successful MailResponse + // save unsuccessful MailResponse $mail_response->save([ - "email_id"=> $mail->id, - "code" => $exception->getCode(), - "message" => $exception->getMessage() + 'email_id' => $mail->id, + 'code' => $exception->getCode(), + 'message' => $exception->getMessage(), ]); throw $exception; } + + return $mail_response; } protected function addAddress(Mail $mail, string $ref_name, callable $func): void { - foreach ($mail->ref($ref_name) as $id => $address) { + foreach ($mail->ref($ref_name)->getIterator() as $id => $address) { $func($address); } } diff --git a/src/Mailer/Gmail.php b/src/Mailer/Gmail.php old mode 100644 new mode 100755 index 2ec8787..1ae3a02 --- a/src/Mailer/Gmail.php +++ b/src/Mailer/Gmail.php @@ -2,22 +2,14 @@ declare(strict_types=1); -namespace atk4\outbox\Mailer; +namespace Atk4\Outbox\Mailer; -use atk4\core\Exception; +use PHPMailer\PHPMailer\PHPMailer; class Gmail extends SMTP { protected $host = 'smtp.gmail.com'; protected $port = 587; protected $auth = true; - - public function __construct(array $defaults = []) - { - if (empty($defaults['password']) || empty($defaults['username'])) { - throw new Exception('username and password must be defined in injection array'); - } - - parent::__construct($defaults); - } + protected $secure = PHPMailer::ENCRYPTION_SMTPS; } diff --git a/src/Mailer/SMTP.php b/src/Mailer/SMTP.php old mode 100644 new mode 100755 index 0d683b7..dd62c90 --- a/src/Mailer/SMTP.php +++ b/src/Mailer/SMTP.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace atk4\outbox\Mailer; +namespace Atk4\Outbox\Mailer; class SMTP extends AbstractMailer { diff --git a/src/MailerInterface.php b/src/MailerInterface.php old mode 100644 new mode 100755 index 034fb0f..4e41fc1 --- a/src/MailerInterface.php +++ b/src/MailerInterface.php @@ -2,11 +2,12 @@ declare(strict_types=1); -namespace atk4\outbox; +namespace Atk4\Outbox; -use atk4\outbox\Model\Mail; +use Atk4\Outbox\Model\Mail; +use Atk4\Outbox\Model\MailResponse; interface MailerInterface { - public function send(Mail $Message): void; + public function send(Mail $Message): MailResponse; } diff --git a/src/Model/Mail.php b/src/Model/Mail.php old mode 100644 new mode 100755 index e514f74..f000a03 --- a/src/Model/Mail.php +++ b/src/Model/Mail.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace atk4\outbox\Model; +namespace Atk4\Outbox\Model; -use atk4\data\Exception; -use atk4\data\Model; -use atk4\outbox\Outbox; +use Atk4\Data\Exception; +use Atk4\Data\Model; +use Atk4\Outbox\Outbox; /** * Class Mail. @@ -20,62 +20,61 @@ class Mail extends Model public const STATUS_ERROR = 'ERROR'; public const MAIL_STATUS = [ - 0 => self::STATUS_DRAFT, - 1 => self::STATUS_READY, - 2 => self::STATUS_SENDING, - 3 => self::STATUS_SENT, - 5 => self::STATUS_ERROR, + self::STATUS_DRAFT, + self::STATUS_READY, + self::STATUS_SENDING, + self::STATUS_SENT, + self::STATUS_ERROR, ]; public $table = 'mail'; - /** - * @throws \atk4\core\Exception - * @throws Exception - */ - public function init(): void + /** @var string */ + public $mail_template_default = MailTemplate::class; + + protected function init(): void { parent::init(); - $this->containsOne('from', MailAddress::class); + $this->containsOne('from', ['model' => [MailAddress::class]]); + $this->containsMany('replyto', ['model' => [MailAddress::class]]); - $this->containsMany('replyto', MailAddress::class); + $this->containsMany('headers', ['model' => [MailHeader::class]]); - $this->containsMany('to', MailAddress::class); - $this->containsMany('cc', MailAddress::class); - $this->containsMany('bcc', MailAddress::class); + $this->containsMany('to', ['model' => [MailAddress::class]]); + $this->containsMany('cc', ['model' => [MailAddress::class]]); + $this->containsMany('bcc', ['model' => [MailAddress::class]]); $this->addField('subject'); $this->addField('text', ['type' => 'text']); $this->addField('html', ['type' => 'text']); - $this->containsMany('attachments', MailAttachment::class); - - $this->addField('sent_at', ['type' => 'datetime']); - - $this->addField('postpone_to', ['type' => 'datetime']); + $this->containsMany('attachments', ['model' => [MailAttachment::class]]); $this->addField('status', [ - 'type' => 'enum', - 'values' => static::MAIL_STATUS, - 'default' => 0, + 'values' => array_combine( + static::MAIL_STATUS, + static::MAIL_STATUS + ), + 'default' => static::STATUS_DRAFT, ]); - $this->hasMany('response', MailResponse::class); + $this->hasMany('response', [ + 'model' => [MailResponse::class], + 'their_field' => 'email_id', + ]); } - /** - * @param string $identifier - * - * @return Mail - * @throws Exception - * - */ - public function withTemplateIdentifier(string $identifier): Mail + public function withTemplateIdentifier(string $identifier): self { - $template = new MailTemplate($this->persistence); - $template->load($identifier); + /** @var MailTemplate $template */ + $template = new $this->mail_template_default($this->persistence); + $template->tryLoadBy('identifier', $identifier); + + if (!$template->loaded()) { + throw new Exception('template "' . $identifier . '" not exists'); + } $this->withTemplate($template); @@ -84,20 +83,14 @@ public function withTemplateIdentifier(string $identifier): Mail /** * Set data from MailTemplate. - * - * @param MailTemplate $template - * - * @return Mail - * @throws Exception - * */ - public function withTemplate(MailTemplate $template): Mail + public function withTemplate(MailTemplate $template): self { $this->allowProcessing(); - foreach ($template->get() as $key => $value) { - if ($this->offsetExists($key)) { - $this->set($key, $value); + foreach ($template->get() as $fieldname => $value) { + if ($fieldname !== $this->id_field && $this->hasField($fieldname)) { + $this->set($fieldname, $value); } } @@ -106,23 +99,18 @@ public function withTemplate(MailTemplate $template): Mail /** * Check if can be processed. - * @throws Exception */ private function allowProcessing(): void { - if (0 !== (int)$this->get('status')) { + if ((int) $this->get('status') !== 0) { throw new Exception('You cannot modify a mail not in draft status'); } } /** * @param string|array|Model $tokens - * @param string|null $prefix - * - * @return Mail - * @throws Exception */ - public function replaceContent($tokens, ?string $prefix = null): Mail + public function replaceContent($tokens, string $prefix = null): self { if (is_string($tokens)) { $tokens = [$tokens => $prefix]; @@ -134,8 +122,11 @@ public function replaceContent($tokens, ?string $prefix = null): Mail } foreach ($tokens as $key => $value) { - $key = '{{' . (null === $prefix ? $key : $prefix . '.' . $key) . '}}'; - $this->replaceContentToken($key, $value); + if ($value === null) { + continue; + } + $key = '{{' . ($prefix === null ? $key : $prefix . '.' . $key) . '}}'; + $this->replaceContentToken($key, (string) $value); } return $this; @@ -143,15 +134,8 @@ public function replaceContent($tokens, ?string $prefix = null): Mail /** * Replace in subject, html and text using key with value. - * - * @param string $key - * @param string $value - * - * @return Mail - * @throws Exception - * */ - private function replaceContentToken(string $key, string $value): Mail + private function replaceContentToken(string $key, string $value): self { $this->allowProcessing(); @@ -163,26 +147,29 @@ private function replaceContentToken(string $key, string $value): Mail } /** - * Send Mail using $outbox or get from app - * - * @param Outbox|null $outbox - * - * @throws \atk4\core\Exception + * Send Mail using $outbox or get from app. */ - public function send(?Outbox $outbox = null): void + public function send(Outbox $outbox): MailResponse + { + return $outbox->send($this); + } + + public function saveAsTemplate(string $identifier): MailTemplate { - // if outbox is null check if App is present and has outbox added - if (null === $outbox && null !== $this->app && method_exists($this->app, 'getOutbox')) { - $outbox = $this->app->getOutbox(); + $mail_template = new MailTemplate($this->persistence); + $mail_template->addCondition('identifier', $identifier); + $mail_template->tryLoadAny(); + + if ($mail_template->loaded()) { + throw new \Atk4\Ui\Exception('Template Identifier already exists'); } - // if still null throw exception - if (null === $outbox) { - $exc = new \atk4\core\Exception('$outbox is null and App has no Outbox'); - $exc->addSolution('Add Outbox object to App'); - throw $exc->addSolution('Call method send with Outbox != null'); + foreach ($this->get() as $fieldname => $value) { + if ($fieldname !== $this->id_field && $mail_template->hasField($fieldname)) { + $mail_template->set($fieldname, $value); + } } - $outbox->send($this); + return $mail_template->save(); } } diff --git a/src/Model/MailAddress.php b/src/Model/MailAddress.php old mode 100644 new mode 100755 index 05ae8d0..a936863 --- a/src/Model/MailAddress.php +++ b/src/Model/MailAddress.php @@ -2,15 +2,16 @@ declare(strict_types=1); -namespace atk4\outbox\Model; +namespace Atk4\Outbox\Model; -use atk4\data\Model; +use Atk4\Data\Model; class MailAddress extends Model { - public function init(): void + protected function init(): void { parent::init(); + $this->addField('email'); $this->addField('name'); } diff --git a/src/Model/MailAttachment.php b/src/Model/MailAttachment.php old mode 100644 new mode 100755 index bd224e0..42c12ef --- a/src/Model/MailAttachment.php +++ b/src/Model/MailAttachment.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace atk4\outbox\Model; +namespace Atk4\Outbox\Model; -use atk4\data\Model; +use Atk4\Data\Model; class MailAttachment extends Model { - public function init(): void + protected function init(): void { parent::init(); diff --git a/src/Model/MailHeader.php b/src/Model/MailHeader.php new file mode 100644 index 0000000..169db41 --- /dev/null +++ b/src/Model/MailHeader.php @@ -0,0 +1,18 @@ +addField('name'); + $this->addField('value'); + } +} diff --git a/src/Model/MailResponse.php b/src/Model/MailResponse.php old mode 100644 new mode 100755 index fc3ad3d..5b55e08 --- a/src/Model/MailResponse.php +++ b/src/Model/MailResponse.php @@ -2,24 +2,30 @@ declare(strict_types=1); -namespace atk4\outbox\Model; +namespace Atk4\Outbox\Model; -use atk4\data\Model; +use Atk4\Data\Model; use DateTime; class MailResponse extends Model { public $table = 'mail_response'; - public function init(): void + protected function init(): void { parent::init(); - $this->hasOne("email_id", Mail::class); + $this->hasOne('email_id', ['model' => [Mail::class]]); - $this->addField("code", ['type' => 'int', 'default' => 0]); - $this->addField("message", ['type' => 'string']); + $this->addField('code', ['type' => 'integer', 'default' => 0]); + $this->addField( + 'message', + ['type' => 'string', 'default' => 'success'] + ); - $this->addField("timestamp", ['type' => 'datetime', 'default' => new DateTime()]); + $this->addField( + 'timestamp', + ['type' => 'datetime', 'default' => new DateTime()] + ); } } diff --git a/src/Model/MailTemplate.php b/src/Model/MailTemplate.php old mode 100644 new mode 100755 index 8041359..9673881 --- a/src/Model/MailTemplate.php +++ b/src/Model/MailTemplate.php @@ -2,34 +2,79 @@ declare(strict_types=1); -namespace atk4\outbox\Model; +namespace Atk4\Outbox\Model; -use atk4\data\Model; +use Atk4\Data\Model; class MailTemplate extends Model { - public $table = "mail_template"; + public $table = 'mail_template'; - public function init(): void + protected function init(): void { parent::init(); - $this->addFile('identifier'); + $this->addField('identifier'); - $this->containsOne('from', MailAddress::class); - $this->containsOne('replyto', MailAddress::class); + $this->containsOne('from', ['model' => [MailAddress::class]]); - $this->containsMany('to', MailAddress::class); - $this->containsMany('cc', MailAddress::class); - $this->containsMany('bcc', MailAddress::class); + $this->containsMany('replyto', ['model' => [MailAddress::class]]); + + $this->containsMany('header', ['model' => [MailHeader::class]]); + + $this->containsMany('to', ['model' => [MailAddress::class]]); + $this->containsMany('cc', ['model' => [MailAddress::class]]); + $this->containsMany('bcc', ['model' => [MailAddress::class]]); $this->addField('subject'); $this->addField('text', ['type' => 'text']); $this->addField('html', ['type' => 'text']); - $this->containsMany('attachment', MailAttachment::class); + $this->containsMany('attachment', ['model' => [MailAttachment::class]]); + + $this->containsMany('tokens', ['model' => [MailTemplateToken::class]]); + + $this->onHook('beforeSave', function (self $m) { + $m->refreshTokens(); + }, [], -200); + } + + public function refreshTokens(): void + { + $re = '/.*{{(.*)}}/m'; + + $matches = []; + + $tmp = []; + preg_match_all($re, $this->get('subject'), $tmp, PREG_SET_ORDER, 0); + $matches = array_merge($matches, $tmp); + + $tmp = []; + preg_match_all($re, $this->get('html'), $tmp, PREG_SET_ORDER, 0); + $matches = array_merge($matches, $tmp); + + $tmp = []; + preg_match_all($re, $this->get('text'), $tmp, PREG_SET_ORDER, 0); + $matches = array_merge($matches, $tmp); + + $tokens = $this->ref('tokens')->export(null, 'token'); + $new_tokens = []; + + //$this->set('tokens', []); + + // @todo can be done better? + foreach ($matches as [$match, $token]) { + if (in_array($token, $tokens, true)) { + continue; + } + + $new_tokens[$token] = [ + 'token' => $token, + 'description' => $tokens[$token]['description'] ?? '', + ]; + } - $this->containsMany('tokens', MailTemplateToken::class); + $this->set('tokens', array_values($new_tokens)); } } diff --git a/src/Model/MailTemplateToken.php b/src/Model/MailTemplateToken.php old mode 100644 new mode 100755 index aaf4a08..ab902a3 --- a/src/Model/MailTemplateToken.php +++ b/src/Model/MailTemplateToken.php @@ -2,17 +2,16 @@ declare(strict_types=1); -namespace atk4\outbox\Model; +namespace Atk4\Outbox\Model; -use atk4\data\Model; +use Atk4\Data\Model; class MailTemplateToken extends Model { - public function init(): void + protected function init(): void { parent::init(); - - $this->addField('identifier'); + $this->addField('token'); $this->addField('description'); } } diff --git a/src/Outbox.php b/src/Outbox.php old mode 100644 new mode 100755 index f75ea8e..8eeb0db --- a/src/Outbox.php +++ b/src/Outbox.php @@ -2,25 +2,17 @@ declare(strict_types=1); -namespace atk4\outbox; +namespace Atk4\Outbox; -use atk4\core\AppScopeTrait; -use atk4\core\DIContainerTrait; -use atk4\core\Exception; -use atk4\core\FactoryTrait; -use atk4\core\InitializerTrait; -use atk4\data\Persistence; -use atk4\outbox\Model\Mail; +use Atk4\Core\Exception; +use Atk4\Core\Factory; +use Atk4\Data\Persistence; +use Atk4\Outbox\Model\Mail; +use Atk4\Outbox\Model\MailResponse; +use Atk4\Ui\AbstractView; -class Outbox +class Outbox extends AbstractView { - use AppScopeTrait; - use InitializerTrait { - init as _init; - } - use DIContainerTrait; - use FactoryTrait; - /** * Mailer. * @@ -31,60 +23,61 @@ class Outbox /** * Default Mail model. * - * @var Mail + * @var Mail|array|string */ protected $model = Mail::class; - public function __construct($defaults = []) + /** @var string */ + protected $skin; + + public function __construct(array $defaults = []) { - if (empty($defaults['mailer'])) { - throw new Exception('No Mailer'); + if (is_array($defaults['mailer'])) { + $defaults['mailer'] = Factory::factory($defaults['mailer']); } - if (is_array($defaults['mailer'])) { - $class = array_pop($defaults['mailer']); - $defaults['mailer'] = new $class($defaults['mailer']); + if (!is_a($defaults['mailer'], MailerInterface::class, true)) { + throw new Exception('Mailer is not a MailerInterface'); } $this->setDefaults($defaults); + + if ($this->mailer === null) { + throw new Exception('No Mailer'); + } } - /** - * @throws Exception - */ - public function init(): void + protected function init(): void { - $this->_init(); + parent::init(); // Setup app, if present - if (null !== $this->app) { - $this->app->addMethod( - 'getOutbox', - function (): self { - return $this; - } - ); - - if (is_array($this->model)) { - if (!is_a($this->model[1] ?? null, Persistence::class)) { - $this->model[1] = $this->app->db; - } + $this->getApp()->addMethod( + 'getOutbox', + function (): self { + return $this; } + ); - if (is_string($this->model)) { - $this->model = [ - $this->model, - $this->app->db, - ]; + if (is_array($this->model)) { + if (!is_a($this->model[1] ?? null, Persistence::class)) { + $this->model[1] = $this->getApp()->db; } } - $this->mailer = $this->factory($this->mailer); - $this->model = $this->factory($this->model); + if (is_string($this->model)) { + $this->model = [ + $this->model, + $this->getApp()->db, + ]; + } + + $this->mailer = Factory::factory($this->mailer); + $this->model = Factory::factory($this->model); if (!is_a($this->mailer, MailerInterface::class)) { - $exc = new Exception('Mailer must be a subclass of MailerInterface'); - throw $exc->addSolution('You need to specify a Mailer which implements MailerInterface'); + throw (new Exception('Mailer must be a subclass of MailerInterface')) + ->addSolution('You need to specify a Mailer which implements MailerInterface'); } if (!is_a($this->model, Mail::class)) { @@ -96,42 +89,37 @@ function (): self { } } - public function callableSend(callable $send): void + public function callableSend(callable $send): MailResponse { $mail = $send($this->new()); - $this->send($mail); - } - public function new(): Mail - { - return clone $this->model; + return $this->send($mail); } - /** - * @param Mail $mail - * - * @return Mail - * @throws Exception - * - */ - public function send(Mail $mail): Mail + public function new(): Mail { $this->validateOutbox(); - $mail->hook('beforeSend'); - $this->mailer->send($mail); - $mail->hook('afterSend'); + return clone $this->model; } - /** - * @throws Exception - */ protected function validateOutbox(): void { if (!$this->_initialized) { - $exc = new Exception('Outbox must be initialized first'); - $exc->addSolution('if you use outbox with App, outbox must be add to app using method App::add'); - throw $exc->addSolution('if you use outbox without App, you need to call init() before use'); + throw (new Exception('Outbox must be initialized first')) + ->addSolution('if you use outbox with App, outbox must be add to app using method App::add') + ->addSolution('if you use outbox without App, you need to call init() before use'); } } + + public function send(Mail $mail): MailResponse + { + $this->validateOutbox(); + + $mail->hook('beforeSend'); + $response = $this->mailer->send($mail); + $mail->hook('afterSend', [$response]); + + return $response; + } } diff --git a/tests/Bootstrap.php b/tests/Bootstrap.php new file mode 100644 index 0000000..b96a181 --- /dev/null +++ b/tests/Bootstrap.php @@ -0,0 +1,128 @@ +elements)) { + return; + } + + $self = self::instance(); + + $persistence = Persistence::connect(getenv('MYSQL_DSN')); + $mail = new Mail($persistence); + $mail_template = new MailTemplate($persistence); + $mail_response = new MailResponse($persistence); + $user = new User($persistence); + + (new Migration($mail))->dropIfExists()->create(); + (new Migration($mail_template))->dropIfExists()->create(); + (new Migration($mail_response))->dropIfExists()->create(); + (new Migration($user))->dropIfExists()->create(); + + $self->el('persistence', $persistence); + $self->el('mail_model', $mail); + $self->el('mail_template', $mail_template); + $self->el('mail_response', $mail_response); + $self->el('user_model', $user); + + $this->prepareMailTemplate($mail_template); + $this->prepareMailTemplateUser($mail_template); + + $user->addCondition('email', 'user@email.it'); + $user->tryLoadAny(); + + $user->save([ + 'email' => 'user@email.it', + 'first_name' => 'John', + 'last_name' => 'Doe', + ]); + } + + public static function instance(): self + { + if (self::$instance !== null) { + return self::$instance; + } + + self::$instance = new self(); + + return self::$instance; + } + + public function el(string $name, object $obj = null): object + { + if ($obj === null) { + return $this->_getFromCollection($name, 'elements'); + } + + return $this->_addIntoCollection($name, $obj, 'elements'); + } + + private function prepareMailTemplate(MailTemplate $mail_template): void + { + $mail_template = $mail_template->newInstance()->tryLoadBy('identifier', 'template_test'); + + if ($mail_template->loaded()) { + return; + } + + $mail_template->set('identifier', 'template_test'); + + $mail_template->set('from', [ + 'email' => 'sender@email.it', + 'name' => 'sender', + ]); + + $mail_template->set('subject', 'subject mail for {{token}}'); + + $content = 'hi to all,|this is outbox library of {{token}}.||have a good day.'; + + $mail_template->set('html', str_replace('|', '
', $content)); + $mail_template->set('text', str_replace('|', PHP_EOL, $content)); + $mail_template->save(); + } + + private function prepareMailTemplateUser(MailTemplate $mail_template): void + { + $mail_template = $mail_template->newInstance()->tryLoadBy('identifier', 'template_test_user'); + + if ($mail_template->loaded()) { + return; + } + + $mail_template->set('identifier', 'template_test_user'); + + $mail_template->set('from', [ + 'email' => 'sender@email.it', + 'name' => 'sender', + ]); + + $content = 'hi to all,|this is outbox library of {{token}}.||have a good day.||{{user.first_name}} {{user.last_name}}'; + + $mail_template->set('subject', 'subject mail for {{user.name}}'); + $mail_template->set('html', str_replace('|', '
', $content)); + $mail_template->set('text', str_replace('|', PHP_EOL, $content)); + $mail_template->save(); + } +} diff --git a/tests/FakeMailer.php b/tests/FakeMailer.php new file mode 100755 index 0000000..5be6c57 --- /dev/null +++ b/tests/FakeMailer.php @@ -0,0 +1,27 @@ +set('status', Mail::STATUS_SENDING); + $message->save(); + + $response = new MailResponse($message->persistence); + + $message->set('status', Mail::STATUS_SENT); + $message->save(); + + return $response->save([ + 'email_id' => $message->id, + ]); + } +} diff --git a/tests/OutboxNoAppTest.php b/tests/OutboxNoAppTest.php new file mode 100644 index 0000000..6f06dde --- /dev/null +++ b/tests/OutboxNoAppTest.php @@ -0,0 +1,144 @@ +el('mail_model'); + + $outbox = new Outbox([ + 'mailer' => [ + FakeMailer::class, + ], + 'model' => $mail_model, + ]); + + $outbox->invokeInit(); + + $mail = $outbox->new() + ->withTemplateIdentifier('template_test') + ->replaceContent('token', 'Agile Toolkit'); + + $mail->ref('to')->save([ + 'email' => 'destination@email.it', + 'name' => 'destination', + ]); + + $response = $outbox->send($mail); + + $this->assertSame( + 'hi to all,
this is outbox library of Agile Toolkit.

have a good day.', + $mail->get('html') + ); + $this->assertSame($response->get('email_id'), $mail->id); + } + + public function testWithAddressAdvanced(): void + { + /** @var Mail $mail_model */ + $mail_model = Bootstrap::instance()->el('mail_model'); + + /** @var User $user_model */ + $user_model = Bootstrap::instance()->el('user_model'); + $user_model = $user_model->loadAny(); + + $outbox = new Outbox([ + 'mailer' => [ + FakeMailer::class, + ], + 'model' => $mail_model, + ]); + + $outbox->invokeInit(); + + $mail = $outbox->new() + ->withTemplateIdentifier('template_test_user') + ->replaceContent('token', 'Agile Toolkit') + ->replaceContent($user_model, 'user'); + + $mail->ref('to')->insert([ + 'email' => 'test@email.it', + 'name' => 'test email', + ]); + $mail->ref('to')->insert([ + 'email' => $user_model->getMailAddress()->get('email'), + 'name' => $user_model->getMailAddress()->get('name'), + ]); + + $mail->ref('cc')->insert([ + 'email' => 'test@email.it', + 'name' => 'test email', + ]); + $mail->ref('cc')->insert([ + 'email' => $user_model->getMailAddress()->get('email'), + 'name' => $user_model->getMailAddress()->get('name'), + ]); + $mail->ref('bcc')->insert([ + 'email' => 'test@email.it', + 'name' => 'test email', + ]); + $mail->ref('bcc')->insert([ + 'email' => $user_model->getMailAddress()->get('email'), + 'name' => $user_model->getMailAddress()->get('name'), + ]); + + $mail->ref('replyto')->insert([ + 'email' => 'test@email.it', + 'name' => 'test email', + ]); + $mail->ref('replyto')->insert([ + 'email' => $user_model->getMailAddress()->get('email'), + 'name' => $user_model->getMailAddress()->get('name'), + ]); + + $mail->ref('headers')->insert([ + 'name' => 'x-custom-header', + 'value' => 'Agile Toolkit', + ]); + + $response = $outbox->send($mail); + + $this->assertSame( + 'hi to all,
this is outbox library of Agile Toolkit.

have a good day.

John Doe', + $mail->get('html') + ); + $this->assertSame($response->get('email_id'), $mail->id); + } + + public function testExceptionNoInit(): void + { + $this->expectException(Exception::class); + + /** @var Mail $mail_model */ + $mail_model = Bootstrap::instance()->el('mail_model'); + + $outbox = new Outbox([ + 'mailer' => [ + FakeMailer::class, + ], + 'model' => $mail_model, + ]); + + //$outbox->init(); <-- this cause exception on send + + $mail = $outbox->new() + ->withTemplateIdentifier('template_test_user') + ->replaceContent('token', 'Agile Toolkit'); + + $response = $outbox->send($mail); + } + + protected function setUp(): void + { + Bootstrap::instance()->setup(); + } +} diff --git a/tests/OutboxTest.php b/tests/OutboxTest.php old mode 100644 new mode 100755 index baae812..dc367a1 --- a/tests/OutboxTest.php +++ b/tests/OutboxTest.php @@ -1,70 +1,115 @@ getOutboxFromApp(); + + $mail = $outbox->new() + ->withTemplateIdentifier('template_test') + ->replaceContent('token', 'Agile Toolkit'); + + $mail->ref('to')->save([ + 'email' => 'destination@email.it', + 'name' => 'destination', + ]); + + $response = $outbox->send($mail); + + $this->assertSame( + 'hi to all,
this is outbox library of Agile Toolkit.

have a good day.', + $mail->get('html') + ); + $this->assertSame($response->get('email_id'), $mail->id); + } + + private function getApp(): App { $app = new App(); + $app->db = Bootstrap::instance()->el('persistence'); + $app->initLayout([Layout::class]); $app->add([ Outbox::class, - 'mailer' => [ - Gmail::class, - 'username' => 'test', - 'password' => 'password' + [ + 'mailer' => [ + FakeMailer::class, + ], + 'model' => Mail::class, ], - 'model' => [ - Mail::class - ] ]); + + return $app; } - public function testSend() + private function getOutboxFromApp(): Outbox { $app = $this->getApp(); - /** @var Outbox $outbox */ - $outbox = $this->getApp()->getOutbox(); + if (!is_callable([$app, 'getOutbox'])) { + throw new Exception('App without getOutbox method'); + } - /** @var Mail $mail */ - $mail = $outbox->new() - ->withTemplateIdentifier('test') - ->replaceContent('test', 'testing') - ; - - $outbox->send($mail); + return $app->getOutbox(); } - public function testSendCallable() + public function testSendCallable(): void { - $app = $this->getApp(); - $user_model = new User($app->db); + $this->getOutboxFromApp()->callableSend(function (Mail $mail) { + $mail->withTemplateIdentifier('template_test') + ->replaceContent('token', 'Agile Toolkit'); - /** @var Outbox $outbox */ - $outbox = $app->getOutbox(); + $mail->ref('to')->save([ + 'email' => 'destination@email.it', + 'name' => 'destination', + ]); - /** @var Mail $mail */ - $outbox->callableSend(static function (Mail $mail) use ($user_model) { - $mail->withTemplateIdentifier('test') - ->replaceContent('test', 'testing') - ->replaceContent([ - 'array_token_1' => 'token_content_1', - 'array_token_2' => 'token_content_2', - ], 'testing') - ->replaceContent($user_model, 'user'); + $mail->onHook('afterSend', function ($m, $response) { + $this->assertSame( + 'hi to all,
this is outbox library of Agile Toolkit.

have a good day.', + $m->get('html') + ); + }); return $mail; }); } + + public function testMailSaveAsTemplate(): void + { + /** @var Mail $mail_model */ + $mail_model = Bootstrap::instance()->_getFromCollection( + 'mail_model', + 'elements' + ); + + $template_model = $mail_model->loadAny()->saveAsTemplate('new_mail_template'); + $data = $template_model->get(); + $template_model->delete(); + + foreach ($data as $fieldname => $value) { + if ($fieldname !== $template_model->id_field && $mail_model->hasField($fieldname)) { + $this->assertSame($value, $mail_model->get($fieldname)); + } + } + } + + protected function setUp(): void + { + Bootstrap::instance()->setup(); + } } diff --git a/tests/User.php b/tests/User.php new file mode 100644 index 0000000..a960ea5 --- /dev/null +++ b/tests/User.php @@ -0,0 +1,37 @@ +addField('first_name'); + $this->addField('last_name'); + + $this->addField('email'); + + //$this->addExpression('name', '([first_name] || [last_name])'); + } + + public function getMailAddress(): MailAddress + { + $address = new MailAddress(new Array_()); + $address->set('email', $this->get('email')); + $address->set('name', $this->get('first_name') . ' ' . $this->get('last_name')); + + return $address; + } +}