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;
+ }
+}