├── .php-cs-fixer.dist.php
├── LICENSE
├── README.md
├── SECURITY.md
├── composer-unused.php
├── composer.json
├── depfile.yaml
├── deptrac.yaml
├── infection.json.dist
├── psalm.xml
├── psalm_autoload.php
├── rector.php
├── roave-bc-check.yaml
└── src
├── Config
├── Events.php
├── Outbox.php
└── Routes.php
├── Controllers
└── Templates.php
├── Database
├── Migrations
│ ├── 2020-06-21-001839_create_outbox_tables.php
│ ├── 2020-10-17-195118_create_outbox_templates.php
│ ├── 2020-11-02-143410_add_templates_parent.php
│ └── 2020-11-10-102311_add_emails_template.php
└── Seeds
│ └── TemplateSeeder.php
├── Entities
├── Attachment.php
├── Email.php
├── Recipient.php
└── Template.php
├── Exceptions
└── TemplatesException.php
├── Language
└── en
│ └── Templates.php
├── Models
├── AttachmentModel.php
├── EmailModel.php
├── RecipientModel.php
└── TemplateModel.php
└── Views
├── Defaults
├── styles.php
└── template.php
├── Templates
├── form.php
├── index.php
└── send.php
├── layout.php
└── navbar.php
/.php-cs-fixer.dist.php:
--------------------------------------------------------------------------------
1 | files()
9 | ->in([
10 | __DIR__ . '/src/',
11 | __DIR__ . '/tests/',
12 | ])
13 | ->exclude('build')
14 | ->append([__FILE__]);
15 |
16 | $overrides = [];
17 |
18 | $options = [
19 | 'finder' => $finder,
20 | 'cacheFile' => 'build/.php-cs-fixer.cache',
21 | ];
22 |
23 | return Factory::create(new CodeIgniter4(), $overrides, $options)->forProjects();
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Tatter Software
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Tatter\Outbox
2 | Email toolkit for CodeIgniter 4
3 |
4 | [](https://github.com/tattersoftware/codeigniter4-outbox/actions/workflows/phpunit.yml)
5 | [](https://github.com/tattersoftware/codeigniter4-outbox/actions/workflows/phpstan.yml)
6 | [](https://github.com/tattersoftware/codeigniter4-outbox/actions/workflows/deptrac.yml)
7 | [](https://coveralls.io/github/tattersoftware/codeigniter4-outbox?branch=develop)
8 |
9 | ## Quick Start
10 |
11 | 1. Install with Composer: `> composer require tatter/outbox`
12 | 2. Prepare the database: `> php spark migrate -all && php spark db:seed "Tatter\Outbox\Database\Seeds\TemplateSeeder"`
13 | 3. Send beautiful, dynamic email:
14 | ```php
15 | model(TemplateModel::class)->findByName('Default')
16 | ->email([
17 | 'item' => 'Fancy Purse',
18 | 'cost' => '10 dollars',
19 | 'url' => site_url('items/show/' . $itemId),
20 | ])
21 | ->setTo($user->email)
22 | ->send();
23 | ```
24 |
25 | ## Features
26 |
27 | **Outbox** supplies useful tools to supplement the framework's native `Email` class:
28 | logging, style inlining, and templating.
29 |
30 | ## Installation
31 |
32 | Install easily via Composer to take advantage of CodeIgniter 4's autoloading capabilities
33 | and always be up-to-date:
34 | ```shell
35 | composer require tatter/outbox
36 | ```
37 |
38 | Or, install manually by downloading the source files and adding the directory to
39 | **app/Config/Autoload.php**.
40 |
41 | ## Configuration (optional)
42 |
43 | The library's default behavior can be altered by extending its config file. Copy
44 | **examples/Outbox.php** to **app/Config/** and follow the instructions
45 | in the comments. If no config file is found in **app/Config** then the library will use its own.
46 |
47 | If you plan to use the Template Routes (see below) you might also want to configure
48 | [Tatter\Layouts](https://github.com/tattersoftware/codeigniter4-layouts) to ensure the
49 | views are displayed properly for your app.
50 |
51 | ## Usage
52 |
53 | ### Logging
54 |
55 | By default **Outbox** will log any successfully sent emails in the database. This provides
56 | a handy paper-trail for applications that send a variety of status and communication
57 | messages to users. Use the `Tatter\Outbox\Models\EmailModel` and its corresponding entity
58 | to view email logs.
59 |
60 | ### Inlining
61 |
62 | Sending HTML email can be tricky, as support for HTML and CSS vary across displays and devices.
63 | **Outbox** includes `CssToInlineStyles`, a module by *tijsverkoyen* to take any CSS and
64 | inject it inline into an email template for maximum compatibility. This allows you to reuse
65 | site stylesheets or write your own from scratch and use them across any of your templates.
66 | Use the default styles from
67 | [Responsive HTML Email Template](https://github.com/leemunroe/responsive-html-email-template),
68 | supply your own as string parameters, or create a View file and add it to the configuration.
69 |
70 | ## Templating
71 |
72 | **Outbox** comes with a default template, a modified-for-CodeIgniter version of the
73 | [Responsive HTML Email Template](https://github.com/leemunroe/responsive-html-email-template).
74 | This provides a solid basis for your emails so you can be sure they will display nicely on
75 | any device. Run the Template Seeder to begin using this as the default:
76 | ```shell
77 | php spark db:seed "Tatter\Outbox\Database\Seeds\TemplateSeeder"
78 | ```
79 |
80 | You may also write your own templates and seed them or use the provided MVC bundle for
81 | managing email templates in your database. To enable the Controller you will need to
82 | toggle `$routeTemplates` in the configuration, or add the following routes to **app/Config/Routes.php**:
83 |
84 | ```php
85 | // Routes to Email Templates
86 | $routes->group('emails', ['namespace' => '\Tatter\Outbox\Controllers'], function ($routes)
87 | {
88 | $routes->get('templates/new/(:segment)', 'Templates::new/$1');
89 | $routes->get('templates/send/(:segment)', 'Templates::send/$1');
90 | $routes->post('templates/send/(:segment)', 'Templates::send_commit/$1');
91 | $routes->presenter('templates', ['controller' => 'Templates']);
92 | });
93 | ```
94 |
95 | Be sure to secure appropriate access to these routes (e.g. with a Filter).
96 |
97 | ### Tokens
98 |
99 | Templates use View Parser "tokens" that will be passed through to add your data.
100 | The `Template` Entity can do this for you by passing in your data parameters:
101 |
102 | ```php
103 | $template = model(TemplateModel::class)->findByName('Item Purchase');
104 |
105 | $subject = $template->renderSubject(['item' => 'Fancy Purse']);
106 | $body = $template->renderBody(['cost' => '10 dollars']);
107 | ```
108 |
109 | `renderBody()` will take care of inlining any CSS you have provided and including your
110 | template in its parent (if defined).
111 |
112 | If you do not need any other configuration you can get a fully prepared
113 | version of the `Email` class with rendered and inlined content from the library:
114 | ```php
115 | $email = $template->email($data);
116 | $email->setTo('jill@example.com')->send();
117 | ```
118 |
119 | ### Cascading Templates
120 |
121 | Each `Template` may also be created with a "Parent Template". Parent templates need to have
122 | a `{body}` token which will receive the parsed content from its child. Additional tokens
123 | in the parent template can be entered by defining them in the child.
124 |
125 | Cascading templates makes it easy to have a few "layouts" with many different variable
126 | messages for each layout. For example, your app may send both newsletters and receipts
127 | with their own layout (Parent Template) and then a myriad of different customizable
128 | messages for different occasions.
129 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | The development team and community take all security issues seriously. **Please do not make public any uncovered flaws.**
4 |
5 | ## Reporting a Vulnerability
6 |
7 | Thank you for improving the security of our code! Any assistance in removing security flaws will be acknowledged.
8 |
9 | **Please report security flaws by emailing the development team directly: support@tattersoftware.com**.
10 |
11 | The lead maintainer will acknowledge your email within 48 hours, and will send a more detailed response within 48 hours indicating
12 | the next steps in handling your report. After the initial reply to your report, the security team will endeavor to keep you informed of the
13 | progress towards a fix and full announcement, and may ask for additional information or guidance.
14 |
15 | ## Disclosure Policy
16 |
17 | When the security team receives a security bug report, they will assign it to a primary handler.
18 | This person will coordinate the fix and release process, involving the following steps:
19 |
20 | - Confirm the problem and determine the affected versions.
21 | - Audit code to find any potential similar problems.
22 | - Prepare fixes for all releases still under maintenance. These fixes will be released as fast as possible.
23 |
24 | ## Comments on this Policy
25 |
26 | If you have suggestions on how this process could be improved please submit a Pull Request.
27 |
--------------------------------------------------------------------------------
/composer-unused.php:
--------------------------------------------------------------------------------
1 | $config
11 | ->addNamedFilter(NamedFilter::fromString('tatter/layouts'))
12 | ->setAdditionalFilesFor('codeigniter4/framework', [
13 | ...Glob::glob(__DIR__ . '/vendor/codeigniter4/framework/system/Helpers/*.php'),
14 | ]);
15 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tatter/outbox",
3 | "description": "Email toolkit for CodeIgniter 4",
4 | "license": "MIT",
5 | "type": "library",
6 | "keywords": [
7 | "codeigniter",
8 | "codeigniter4",
9 | "outbox",
10 | "email",
11 | "inline",
12 | "logging"
13 | ],
14 | "authors": [
15 | {
16 | "name": "Matthew Gatner",
17 | "email": "mgatner@tattersoftware.com",
18 | "homepage": "https://tattersoftware.com",
19 | "role": "Developer"
20 | }
21 | ],
22 | "homepage": "https://github.com/tattersoftware/codeigniter4-outbox",
23 | "require": {
24 | "php": "^7.4 || ^8.0",
25 | "tatter/layouts": "^1.0",
26 | "tijsverkoyen/css-to-inline-styles": "^2.2"
27 | },
28 | "require-dev": {
29 | "codeigniter4/framework": "^4.1",
30 | "tatter/tools": "^2.0"
31 | },
32 | "minimum-stability": "dev",
33 | "prefer-stable": true,
34 | "autoload": {
35 | "psr-4": {
36 | "Tatter\\Outbox\\": "src"
37 | },
38 | "exclude-from-classmap": [
39 | "**/Database/Migrations/**"
40 | ]
41 | },
42 | "autoload-dev": {
43 | "psr-4": {
44 | "Tests\\Support\\": "tests/_support"
45 | }
46 | },
47 | "config": {
48 | "allow-plugins": {
49 | "phpstan/extension-installer": true
50 | }
51 | },
52 | "scripts": {
53 | "analyze": [
54 | "phpstan analyze",
55 | "psalm",
56 | "rector process --dry-run"
57 | ],
58 | "ci": [
59 | "Composer\\Config::disableProcessTimeout",
60 | "@deduplicate",
61 | "@analyze",
62 | "@composer normalize --dry-run",
63 | "@test",
64 | "@inspect",
65 | "@style"
66 | ],
67 | "deduplicate": "phpcpd app/ src/",
68 | "inspect": "deptrac analyze --cache-file=build/deptrac.cache",
69 | "mutate": "infection --threads=2 --skip-initial-tests --coverage=build/phpunit",
70 | "retool": "retool",
71 | "style": "php-cs-fixer fix --verbose --ansi --using-cache=no",
72 | "test": "phpunit"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/depfile.yaml:
--------------------------------------------------------------------------------
1 | paths:
2 | - ./src/
3 | - ./vendor/codeigniter4/framework/system/
4 | exclude_files:
5 | - '#.*test.*#i'
6 | layers:
7 | - name: Model
8 | collectors:
9 | - type: bool
10 | must:
11 | - type: className
12 | regex: .*[A-Za-z]+Model$
13 | must_not:
14 | - type: directory
15 | regex: vendor/.*
16 | - name: Vendor Model
17 | collectors:
18 | - type: bool
19 | must:
20 | - type: className
21 | regex: .*[A-Za-z]+Model$
22 | - type: directory
23 | regex: vendor/.*
24 | - name: Controller
25 | collectors:
26 | - type: bool
27 | must:
28 | - type: className
29 | regex: .*\/Controllers\/.*
30 | must_not:
31 | - type: directory
32 | regex: vendor/.*
33 | - name: Vendor Controller
34 | collectors:
35 | - type: bool
36 | must:
37 | - type: className
38 | regex: .*\/Controllers\/.*
39 | - type: directory
40 | regex: vendor/.*
41 | - name: Config
42 | collectors:
43 | - type: bool
44 | must:
45 | - type: directory
46 | regex: src/Config/.*
47 | must_not:
48 | - type: className
49 | regex: .*Services
50 | - type: directory
51 | regex: vendor/.*
52 | - name: Vendor Config
53 | collectors:
54 | - type: bool
55 | must:
56 | - type: directory
57 | regex: vendor/.*/Config/.*
58 | must_not:
59 | - type: className
60 | regex: .*Services
61 | - name: Entity
62 | collectors:
63 | - type: bool
64 | must:
65 | - type: directory
66 | regex: src/Entities/.*
67 | must_not:
68 | - type: directory
69 | regex: vendor/.*
70 | - name: Vendor Entity
71 | collectors:
72 | - type: bool
73 | must:
74 | - type: directory
75 | regex: vendor/.*/Entities/.*
76 | - name: View
77 | collectors:
78 | - type: bool
79 | must:
80 | - type: directory
81 | regex: src/Views/.*
82 | must_not:
83 | - type: directory
84 | regex: vendor/.*
85 | - name: Vendor View
86 | collectors:
87 | - type: bool
88 | must:
89 | - type: directory
90 | regex: vendor/.*/Views/.*
91 | - name: Service
92 | collectors:
93 | - type: className
94 | regex: .*Services.*
95 | ruleset:
96 | Entity:
97 | - Config
98 | - Model
99 | - Service
100 | - Vendor Config
101 | - Vendor Entity
102 | - Vendor Model
103 | Config:
104 | - Service
105 | - Vendor Config
106 | Model:
107 | - Config
108 | - Entity
109 | - Service
110 | - Vendor Config
111 | - Vendor Entity
112 | - Vendor Model
113 | Service:
114 | - Config
115 | - Vendor Config
116 |
117 | # Ignore anything in the Vendor layers
118 | Vendor Model:
119 | - Config
120 | - Service
121 | - Vendor Config
122 | - Vendor Controller
123 | - Vendor Entity
124 | - Vendor Model
125 | - Vendor View
126 | Vendor Controller:
127 | - Service
128 | - Vendor Config
129 | - Vendor Controller
130 | - Vendor Entity
131 | - Vendor Model
132 | - Vendor View
133 | Vendor Config:
134 | - Config
135 | - Service
136 | - Vendor Config
137 | - Vendor Controller
138 | - Vendor Entity
139 | - Vendor Model
140 | - Vendor View
141 | Vendor Entity:
142 | - Service
143 | - Vendor Config
144 | - Vendor Controller
145 | - Vendor Entity
146 | - Vendor Model
147 | - Vendor View
148 | Vendor View:
149 | - Service
150 | - Vendor Config
151 | - Vendor Controller
152 | - Vendor Entity
153 | - Vendor Model
154 | - Vendor View
155 | skip_violations:
156 |
--------------------------------------------------------------------------------
/deptrac.yaml:
--------------------------------------------------------------------------------
1 | parameters:
2 | paths:
3 | - ./src/
4 | - ./vendor/codeigniter4/framework/system/
5 | exclude_files:
6 | - '#.*test.*#i'
7 | layers:
8 | - name: Model
9 | collectors:
10 | - type: bool
11 | must:
12 | - type: className
13 | regex: .*[A-Za-z]+Model$
14 | must_not:
15 | - type: directory
16 | regex: vendor/.*
17 | - name: Vendor Model
18 | collectors:
19 | - type: bool
20 | must:
21 | - type: className
22 | regex: .*[A-Za-z]+Model$
23 | - type: directory
24 | regex: vendor/.*
25 | - name: Controller
26 | collectors:
27 | - type: bool
28 | must:
29 | - type: className
30 | regex: .*\/Controllers\/.*
31 | must_not:
32 | - type: directory
33 | regex: vendor/.*
34 | - name: Vendor Controller
35 | collectors:
36 | - type: bool
37 | must:
38 | - type: className
39 | regex: .*\/Controllers\/.*
40 | - type: directory
41 | regex: vendor/.*
42 | - name: Config
43 | collectors:
44 | - type: bool
45 | must:
46 | - type: directory
47 | regex: src/Config/.*
48 | must_not:
49 | - type: className
50 | regex: .*Services
51 | - type: directory
52 | regex: vendor/.*
53 | - name: Vendor Config
54 | collectors:
55 | - type: bool
56 | must:
57 | - type: directory
58 | regex: vendor/.*/Config/.*
59 | must_not:
60 | - type: className
61 | regex: .*Services
62 | - name: Entity
63 | collectors:
64 | - type: bool
65 | must:
66 | - type: directory
67 | regex: src/Entities/.*
68 | must_not:
69 | - type: directory
70 | regex: vendor/.*
71 | - name: Vendor Entity
72 | collectors:
73 | - type: bool
74 | must:
75 | - type: directory
76 | regex: vendor/.*/Entities/.*
77 | - name: View
78 | collectors:
79 | - type: bool
80 | must:
81 | - type: directory
82 | regex: src/Views/.*
83 | must_not:
84 | - type: directory
85 | regex: vendor/.*
86 | - name: Vendor View
87 | collectors:
88 | - type: bool
89 | must:
90 | - type: directory
91 | regex: vendor/.*/Views/.*
92 | - name: Service
93 | collectors:
94 | - type: className
95 | regex: .*Services.*
96 | ruleset:
97 | Entity:
98 | - Config
99 | - Model
100 | - Service
101 | - Vendor Config
102 | - Vendor Entity
103 | - Vendor Model
104 | Config:
105 | - Service
106 | - Vendor Config
107 | Model:
108 | - Config
109 | - Entity
110 | - Service
111 | - Vendor Config
112 | - Vendor Entity
113 | - Vendor Model
114 | Service:
115 | - Config
116 | - Vendor Config
117 |
118 | # Ignore anything in the Vendor layers
119 | Vendor Model:
120 | - Config
121 | - Service
122 | - Vendor Config
123 | - Vendor Controller
124 | - Vendor Entity
125 | - Vendor Model
126 | - Vendor View
127 | Vendor Controller:
128 | - Service
129 | - Vendor Config
130 | - Vendor Controller
131 | - Vendor Entity
132 | - Vendor Model
133 | - Vendor View
134 | Vendor Config:
135 | - Config
136 | - Service
137 | - Vendor Config
138 | - Vendor Controller
139 | - Vendor Entity
140 | - Vendor Model
141 | - Vendor View
142 | Vendor Entity:
143 | - Service
144 | - Vendor Config
145 | - Vendor Controller
146 | - Vendor Entity
147 | - Vendor Model
148 | - Vendor View
149 | Vendor View:
150 | - Service
151 | - Vendor Config
152 | - Vendor Controller
153 | - Vendor Entity
154 | - Vendor Model
155 | - Vendor View
156 | skip_violations:
157 |
--------------------------------------------------------------------------------
/infection.json.dist:
--------------------------------------------------------------------------------
1 | {
2 | "source": {
3 | "directories": [
4 | "src/"
5 | ],
6 | "excludes": [
7 | "Config",
8 | "Database/Migrations",
9 | "Views"
10 | ]
11 | },
12 | "logs": {
13 | "text": "build/infection.log"
14 | },
15 | "mutators": {
16 | "@default": true
17 | },
18 | "bootstrap": "vendor/codeigniter4/framework/system/Test/bootstrap.php"
19 | }
20 |
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/psalm_autoload.php:
--------------------------------------------------------------------------------
1 | sets([SetList::DEAD_CODE, LevelSetList::UP_TO_PHP_74, PHPUnitSetList::PHPUNIT_SPECIFIC_METHOD, PHPUnitSetList::PHPUNIT_80]);
40 | $rectorConfig->parallel();
41 | // The paths to refactor (can also be supplied with CLI arguments)
42 | $rectorConfig->paths([
43 | __DIR__ . '/src/',
44 | __DIR__ . '/tests/',
45 | ]);
46 |
47 | // Include Composer's autoload - required for global execution, remove if running locally
48 | $rectorConfig->autoloadPaths([
49 | __DIR__ . '/vendor/autoload.php',
50 | ]);
51 |
52 | // Do you need to include constants, class aliases, or a custom autoloader?
53 | $rectorConfig->bootstrapFiles([
54 | realpath(getcwd()) . '/vendor/codeigniter4/framework/system/Test/bootstrap.php',
55 | ]);
56 |
57 | if (is_file(__DIR__ . '/phpstan.neon.dist')) {
58 | $rectorConfig->phpstanConfig(__DIR__ . '/phpstan.neon.dist');
59 | }
60 |
61 | // Set the target version for refactoring
62 | $rectorConfig->phpVersion(PhpVersion::PHP_74);
63 |
64 | // Auto-import fully qualified class names
65 | $rectorConfig->importNames();
66 |
67 | // Are there files or rules you need to skip?
68 | $rectorConfig->skip([
69 | __DIR__ . '/src/Views',
70 |
71 | JsonThrowOnErrorRector::class,
72 | StringifyStrNeedlesRector::class,
73 |
74 | // Note: requires php 8
75 | RemoveUnusedPromotedPropertyRector::class,
76 |
77 | // Ignore tests that might make calls without a result
78 | RemoveEmptyMethodCallRector::class => [
79 | __DIR__ . '/tests',
80 | ],
81 |
82 | // Ignore files that should not be namespaced
83 | NormalizeNamespaceByPSR4ComposerAutoloadRector::class => [
84 | __DIR__ . '/src/Helpers',
85 | ],
86 |
87 | // May load view files directly when detecting classes
88 | StringClassNameToClassConstantRector::class,
89 |
90 | // May be uninitialized on purpose
91 | AddDefaultValueForUndefinedVariableRector::class,
92 | ]);
93 | $rectorConfig->rule(SimplifyUselessVariableRector::class);
94 | $rectorConfig->rule(RemoveAlwaysElseRector::class);
95 | $rectorConfig->rule(CountArrayToEmptyArrayComparisonRector::class);
96 | $rectorConfig->rule(ForToForeachRector::class);
97 | $rectorConfig->rule(ChangeNestedForeachIfsToEarlyContinueRector::class);
98 | $rectorConfig->rule(ChangeIfElseValueAssignToEarlyReturnRector::class);
99 | $rectorConfig->rule(SimplifyStrposLowerRector::class);
100 | $rectorConfig->rule(CombineIfRector::class);
101 | $rectorConfig->rule(SimplifyIfReturnBoolRector::class);
102 | $rectorConfig->rule(InlineIfToExplicitIfRector::class);
103 | $rectorConfig->rule(PreparedValueToEarlyReturnRector::class);
104 | $rectorConfig->rule(ShortenElseIfRector::class);
105 | $rectorConfig->rule(SimplifyIfElseToTernaryRector::class);
106 | $rectorConfig->rule(UnusedForeachValueToArrayKeysRector::class);
107 | $rectorConfig->rule(ChangeArrayPushToArrayAssignRector::class);
108 | $rectorConfig->rule(UnnecessaryTernaryExpressionRector::class);
109 | $rectorConfig->rule(AddPregQuoteDelimiterRector::class);
110 | $rectorConfig->rule(SimplifyRegexPatternRector::class);
111 | $rectorConfig->rule(FuncGetArgsToVariadicParamRector::class);
112 | $rectorConfig->rule(MakeInheritedMethodVisibilitySameAsParentRector::class);
113 | $rectorConfig->rule(SimplifyEmptyArrayCheckRector::class);
114 | $rectorConfig->rule(NormalizeNamespaceByPSR4ComposerAutoloadRector::class);
115 | $rectorConfig
116 | ->ruleWithConfiguration(TypedPropertyRector::class, [
117 | // Set to false if you use in libraries, or it does create breaking changes.
118 | TypedPropertyRector::INLINE_PUBLIC => false,
119 | ]);
120 | };
121 |
--------------------------------------------------------------------------------
/roave-bc-check.yaml:
--------------------------------------------------------------------------------
1 | parameters:
2 | ignoreErrors:
3 | - '#\[BC\] SKIPPED: .+ could not be found in the located source#'
4 |
--------------------------------------------------------------------------------
/src/Config/Events.php:
--------------------------------------------------------------------------------
1 | logging) {
13 | return;
14 | }
15 |
16 | // Create the email record (ignores insert collisions)
17 | $id = model(EmailModel::class)->ignore()->insert($params); // @phpstan-ignore-line
18 | if ($id) {
19 | // Add each recipient
20 | foreach ($params['recipients'] as $email) {
21 | model(RecipientModel::class)->insert([
22 | 'outbox_email_id' => $id,
23 | 'email' => $email,
24 | ]);
25 | }
26 |
27 | // Add each attachment
28 | foreach ($params['attachments'] as $attachment) {
29 | $test = model(AttachmentModel::class)->insert([
30 | 'outbox_email_id' => $id,
31 | 'name' => $attachment['name'][0],
32 | 'newName' => $attachment['name'][1],
33 | 'disposition' => $attachment['disposition'],
34 | 'type' => $attachment['type'],
35 | 'multipart' => $attachment['multipart'],
36 | 'bytes' => filesize($attachment['name'][0]),
37 | ]);
38 | }
39 | }
40 | });
41 |
--------------------------------------------------------------------------------
/src/Config/Outbox.php:
--------------------------------------------------------------------------------
1 | routeTemplates)) {
6 | return;
7 | }
8 |
9 | $routes ??= service('routes');
10 |
11 | // Routes to Email Templates
12 | $routes->group('emails', ['namespace' => '\Tatter\Outbox\Controllers'], static function ($routes) {
13 | $routes->get('templates/new/(:segment)', 'Templates::new/$1');
14 | $routes->get('templates/send/(:segment)', 'Templates::send/$1');
15 | $routes->post('templates/send/(:segment)', 'Templates::send_commit/$1');
16 | $routes->presenter('templates', ['controller' => 'Templates']);
17 | });
18 |
--------------------------------------------------------------------------------
/src/Controllers/Templates.php:
--------------------------------------------------------------------------------
1 | model->find($id)) {
33 | return $template;
34 | }
35 |
36 | throw PageNotFoundException::forPageNotFound();
37 | }
38 |
39 | //--------------------------------------------------------------------
40 |
41 | /**
42 | * Displays the form to add a Template, with an optional one to copy.
43 | *
44 | * @param int|string|null $id Another template to use as a starting point
45 | */
46 | public function new($id = null): string
47 | {
48 | $template = null === $id ? new Template() : $this->model->find($id);
49 |
50 | return view('Tatter\Outbox\Views\Templates\form', [
51 | 'method' => $template->id ? 'Clone' : 'New',
52 | 'template' => $template,
53 | 'templates' => $this->model->orderBy('name')->findAll(),
54 | ]);
55 | }
56 |
57 | /**
58 | * Creates a new Template.
59 | */
60 | public function create(): RedirectResponse
61 | {
62 | if ($this->model->insert($this->getData())) {
63 | return redirect()->to(site_url('emails/templates'))->with('success', 'Email template created.');
64 | }
65 |
66 | return redirect()->back()->withInput()->with('error', $this->model->errors());
67 | }
68 |
69 | /**
70 | * Lists all available Templates.
71 | */
72 | public function index(): string
73 | {
74 | return view('Tatter\Outbox\Views\Templates\index', [
75 | 'templates' => $this->model->orderBy('name')->findAll(),
76 | ]);
77 | }
78 |
79 | /**
80 | * Renders a Template for previewing.
81 | *
82 | * @param int|string|null $id
83 | *
84 | * @throws PageNotFoundException
85 | */
86 | public function show($id = null): string
87 | {
88 | return $this->getTemplate($id)->renderBody();
89 | }
90 |
91 | /**
92 | * Displays the form to edit a Template.
93 | *
94 | * @param int|string $id
95 | *
96 | * @throws PageNotFoundException
97 | */
98 | public function edit($id = null): string
99 | {
100 | return view('Tatter\Outbox\Views\Templates\form', [
101 | 'method' => 'Edit',
102 | 'template' => $this->getTemplate($id),
103 | 'templates' => $this->model->where('id !=', $id)->orderBy('name')->findAll(),
104 | ]);
105 | }
106 |
107 | /**
108 | * Updates a Template from posted form data.
109 | *
110 | * @param int|string $id
111 | *
112 | * @throws PageNotFoundException
113 | */
114 | public function update($id = null): RedirectResponse
115 | {
116 | $template = $this->getTemplate($id);
117 |
118 | if ($this->model->update($template->id, $this->getData())) {
119 | return redirect()->back()->with('success', 'Email template updated.');
120 | }
121 |
122 | return redirect()->back()->withInput()->with('error', $this->model->errors());
123 | }
124 |
125 | /**
126 | * Deletes a Template.
127 | *
128 | * @param int|string $id
129 | *
130 | * @throws PageNotFoundException
131 | */
132 | public function remove($id = null): RedirectResponse
133 | {
134 | $template = $this->getTemplate($id);
135 |
136 | if ($this->model->delete($template->id)) {
137 | return redirect()->back()->with('success', 'Email template removed.');
138 | }
139 |
140 | return redirect()->back()->withInput()->with('error', $this->model->errors());
141 | }
142 |
143 | /**
144 | * Displays the form to send an email.
145 | *
146 | * @param int|string $id
147 | *
148 | * @throws PageNotFoundException
149 | */
150 | public function send($id = null): string
151 | {
152 | return view('Tatter\Outbox\Views\Templates\send', [
153 | 'template' => $this->getTemplate($id),
154 | ]);
155 | }
156 |
157 | /**
158 | * Sends an email using the Template.
159 | *
160 | * @param int|string $id
161 | *
162 | * @throws PageNotFoundException
163 | */
164 | public function send_commit($id = null): RedirectResponse
165 | {
166 | if (! $this->validate([
167 | 'fromEmail' => 'valid_email',
168 | 'recipients' => 'valid_emails',
169 | ])) {
170 | return redirect()->back()->withInput()->with('error', implode('. ', $this->validator->getErrors()));
171 | }
172 |
173 | $email = $this->getTemplate($id)->email($this->request->getPost());
174 |
175 | $email->setFrom($this->request->getPost('fromEmail'), $this->request->getPost('fromName'));
176 | $email->setTo($this->request->getPost('recipients'));
177 |
178 | if ($email->send(false)) {
179 | return redirect()->back()->with('success', 'Email sent');
180 | }
181 |
182 | return redirect()->back()->withInput()->with('error', $email->printDebugger([]));
183 | }
184 |
185 | /**
186 | * Retrieves POST data safe for the database.
187 | */
188 | protected function getData(): array
189 | {
190 | $data = $this->request->getPost();
191 |
192 | if (array_key_exists('parent_id', $data)) {
193 | $data['parent_id'] = empty($data['parent_id']) ? null : $data['parent_id'];
194 | }
195 |
196 | return $data;
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/src/Database/Migrations/2020-06-21-001839_create_outbox_tables.php:
--------------------------------------------------------------------------------
1 | ['type' => 'varchar', 'constraint' => 255, 'null' => true],
14 | 'body' => ['type' => 'text', 'null' => true],
15 | 'fromEmail' => ['type' => 'varchar', 'constraint' => 63, 'null' => true],
16 | 'fromName' => ['type' => 'varchar', 'constraint' => 63, 'null' => true],
17 | 'userAgent' => ['type' => 'varchar', 'constraint' => 63, 'null' => true],
18 | 'protocol' => ['type' => 'varchar', 'constraint' => 63, 'null' => true],
19 | 'mailType' => ['type' => 'varchar', 'constraint' => 63, 'null' => true],
20 | 'charset' => ['type' => 'varchar', 'constraint' => 63, 'null' => true],
21 | 'priority' => ['type' => 'int', 'null' => true],
22 | 'sendMultipart' => ['type' => 'boolean', 'default' => 1],
23 | 'BCCBatchMode' => ['type' => 'boolean', 'default' => 1],
24 | 'BCCBatchSize' => ['type' => 'int', 'unsigned' => true, 'null' => true],
25 | 'created_at' => ['type' => 'datetime', 'null' => true],
26 | 'updated_at' => ['type' => 'datetime', 'null' => true],
27 | ];
28 |
29 | $this->forge->addField('id');
30 | $this->forge->addField($fields);
31 |
32 | $this->forge->addKey('subject');
33 | $this->forge->addKey('created_at');
34 |
35 | $this->forge->createTable('outbox_emails');
36 |
37 | // Recipients
38 | $fields = [
39 | 'outbox_email_id' => ['type' => 'int'],
40 | 'email' => ['type' => 'varchar', 'constraint' => 255],
41 | ];
42 |
43 | $this->forge->addField('id');
44 | $this->forge->addField($fields);
45 |
46 | $this->forge->addKey('outbox_email_id');
47 | $this->forge->addKey('email');
48 |
49 | $this->forge->createTable('outbox_recipients');
50 |
51 | // Attachments
52 | $fields = [
53 | 'outbox_email_id' => ['type' => 'int'],
54 | 'name' => ['type' => 'varchar', 'constraint' => 255],
55 | 'newName' => ['type' => 'varchar', 'constraint' => 255, 'null' => true],
56 | 'disposition' => ['type' => 'varchar', 'constraint' => 63, 'null' => true],
57 | 'type' => ['type' => 'varchar', 'constraint' => 63, 'null' => true],
58 | 'multipart' => ['type' => 'varchar', 'constraint' => 63, 'null' => true],
59 | 'bytes' => ['type' => 'int', 'unsigned' => true],
60 | ];
61 |
62 | $this->forge->addField('id');
63 | $this->forge->addField($fields);
64 |
65 | $this->forge->addKey('outbox_email_id');
66 | $this->forge->addKey('name');
67 | $this->forge->addKey('bytes');
68 |
69 | $this->forge->createTable('outbox_attachments');
70 | }
71 |
72 | //--------------------------------------------------------------------
73 |
74 | public function down()
75 | {
76 | $this->forge->dropTable('outbox_emails');
77 | $this->forge->dropTable('outbox_recipients');
78 | $this->forge->dropTable('outbox_attachments');
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Database/Migrations/2020-10-17-195118_create_outbox_templates.php:
--------------------------------------------------------------------------------
1 | ['type' => 'varchar', 'constraint' => 255, 'null' => true],
13 | 'subject' => ['type' => 'varchar', 'constraint' => 255, 'null' => true],
14 | 'body' => ['type' => 'text', 'null' => true],
15 | 'created_at' => ['type' => 'datetime', 'null' => true],
16 | 'updated_at' => ['type' => 'datetime', 'null' => true],
17 | 'deleted_at' => ['type' => 'datetime', 'null' => true],
18 | ];
19 |
20 | $this->forge->addField('id');
21 | $this->forge->addField($fields);
22 |
23 | $this->forge->addKey('name');
24 | $this->forge->addKey('created_at');
25 |
26 | $this->forge->createTable('outbox_templates');
27 | }
28 |
29 | //--------------------------------------------------------------------
30 |
31 | public function down()
32 | {
33 | $this->forge->dropTable('outbox_templates');
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Database/Migrations/2020-11-02-143410_add_templates_parent.php:
--------------------------------------------------------------------------------
1 | forge->addColumn('outbox_templates', [
12 | 'parent_id' => ['type' => 'int', 'null' => true, 'after' => 'body'],
13 | ]);
14 | }
15 |
16 | public function down()
17 | {
18 | $this->forge->dropColumn('outbox_templates', 'parent_id');
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Database/Migrations/2020-11-10-102311_add_emails_template.php:
--------------------------------------------------------------------------------
1 | forge->addColumn('outbox_emails', [
12 | 'template_id' => ['type' => 'int', 'null' => true, 'after' => 'BCCBatchSize'],
13 | 'template' => ['type' => 'varchar', 'constraint' => 255, 'null' => true, 'after' => 'BCCBatchSize'],
14 | ]);
15 | }
16 |
17 | public function down()
18 | {
19 | $this->forge->dropColumn('outbox_emails', 'template');
20 | $this->forge->dropColumn('outbox_emails', 'template_id');
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Database/Seeds/TemplateSeeder.php:
--------------------------------------------------------------------------------
1 | first()) {
14 | return;
15 | }
16 |
17 | // Add the Default Template to the database
18 | model(TemplateModel::class)->insert([
19 | 'name' => 'Default',
20 | 'subject' => '{subject}',
21 | 'body' => view('Tatter\Outbox\Views\Defaults\template'),
22 | ]);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Entities/Attachment.php:
--------------------------------------------------------------------------------
1 | 'int',
12 | 'bytes' => 'int',
13 | ];
14 | }
15 |
--------------------------------------------------------------------------------
/src/Entities/Email.php:
--------------------------------------------------------------------------------
1 | 'int',
22 | 'sendMultipart' => 'boolean',
23 | 'BCCBatchMode' => 'boolean',
24 | 'BCCBatchSize' => 'int',
25 | 'template_id' => '?int',
26 | ];
27 |
28 | /**
29 | * Attachments cache
30 | *
31 | * @var array|null
32 | */
33 | protected $attachments;
34 |
35 | /**
36 | * Recipients cache
37 | *
38 | * @var array|null
39 | */
40 | protected $recipients;
41 |
42 | /**
43 | * Returns this email's attachments.
44 | */
45 | public function getAttachments(): array
46 | {
47 | return $this->getRelatedItems('attachment');
48 | }
49 |
50 | /**
51 | * Returns this email's recipients.
52 | */
53 | public function getRecipients(): array
54 | {
55 | return $this->getRelatedItems('recipient');
56 | }
57 |
58 | /**
59 | * Helper function to return attachments or recipients.
60 | *
61 | * @param string $target Object/table/model to request
62 | *
63 | * @throws RuntimeException
64 | */
65 | protected function getRelatedItems(string $target): array
66 | {
67 | if (empty($this->id)) {
68 | throw new RuntimeException('Object must be created before getting relations.');
69 | }
70 |
71 | $property = $target . 's';
72 | $model = 'Tatter\Outbox\Models\\' . ucfirst($target) . 'Model';
73 |
74 | if (null === $this->{$property}) {
75 | $this->{$property} = model($model)->where('outbox_email_id', $this->id)->findAll();
76 | }
77 |
78 | return $this->{$property};
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Entities/Recipient.php:
--------------------------------------------------------------------------------
1 | 'int'];
11 | }
12 |
--------------------------------------------------------------------------------
/src/Entities/Template.php:
--------------------------------------------------------------------------------
1 | '?int',
20 | 'parent_id' => '?int',
21 | ];
22 |
23 | /**
24 | * Stored parent Template.
25 | *
26 | * @var self|null
27 | */
28 | protected $parent;
29 |
30 | //--------------------------------------------------------------------
31 |
32 | /**
33 | * Returns the parent Template, if set.
34 | */
35 | public function getParent(): ?self
36 | {
37 | if (! isset($this->attributes['parent_id'])) {
38 | return null;
39 | }
40 |
41 | if (null === $this->parent) {
42 | $this->parent = model(TemplateModel::class)->find($this->attributes['parent_id']);
43 | }
44 |
45 | return $this->parent;
46 | }
47 |
48 | /**
49 | * Returns the subject from the parent Template if this one is empty.
50 | */
51 | public function getSubject(): string
52 | {
53 | if (! empty($this->attributes['subject'])) {
54 | return $this->attributes['subject'];
55 | }
56 |
57 | return $this->getParent() ? $this->parent->subject : '';
58 | }
59 |
60 | /**
61 | * Returns any subject or body tokens from this and parent.
62 | */
63 | public function getTokens(): array
64 | {
65 | preg_match_all('#\{(\w+)\}#', $this->attributes['subject'] . $this->attributes['body'], $matches);
66 |
67 | if ($parent = $this->getParent()) {
68 | // Prepend parent tokens (minus {body})
69 | $parentTokens = array_diff($parent->getTokens(), ['body']);
70 | $matches[1] = array_unique(array_merge($parentTokens, $matches[1]));
71 | }
72 |
73 | return $matches[1];
74 | }
75 |
76 | //--------------------------------------------------------------------
77 |
78 | /**
79 | * Renders the body with inlined CSS.
80 | *
81 | * @param array $data Variables to exchange for Template tokens
82 | * @param string|null $css CSS to use for inlining, null to use view from config
83 | */
84 | public function renderBody($data = [], ?string $css = null): string
85 | {
86 | if (empty($this->attributes['body'])) {
87 | return '';
88 | }
89 |
90 | // Replace tokens with $data values
91 | $body = service('parser')
92 | ->setData($data, 'raw')
93 | ->renderString($this->attributes['body'], ['debug' => false]);
94 |
95 | // If this has a parent Template then render it with this body
96 | if ($parent = $this->getParent()) {
97 | $data['body'] = $body;
98 |
99 | return $parent->renderBody($data, $css);
100 | }
101 |
102 | // Determine styling
103 | if (null === $css && config('Outbox')->styles) {
104 | $css = view(config('Outbox')->styles, [], ['debug' => false]);
105 | }
106 |
107 | return $css === '' ? $body : (new CssToInlineStyles())->convert($body, $css);
108 | }
109 |
110 | /**
111 | * Renders the subject.
112 | *
113 | * @param array $data Variables to exchange for Template tokens
114 | */
115 | public function renderSubject($data = []): string
116 | {
117 | if (! $subject = $this->getSubject()) {
118 | return '';
119 | }
120 |
121 | // Replace tokens with $data values
122 | return service('parser')
123 | ->setData($data, 'raw')
124 | ->renderString($subject, ['debug' => false]);
125 | }
126 |
127 | //--------------------------------------------------------------------
128 |
129 | /**
130 | * Returns an Email instance primed to this Template's rendered values.
131 | *
132 | * @param array $data Variables to use when rendering the body
133 | * @param string|null $styles CSS to use for inlining, null to use configured view
134 | */
135 | public function email($data = [], ?string $styles = null): Email
136 | {
137 | // Start with the default config and add necessary settings
138 | $email = service('email');
139 | $email->mailType = 'html';
140 | $email->wordWrap = false;
141 | $email->template = $this->attributes['name'];
142 | $email->template_id = $this->attributes['id'] ?? null;
143 |
144 | // Render the subject and body and return the Email instance
145 | return $email
146 | ->setSubject($this->renderSubject($data))
147 | ->setMessage($this->renderBody($data, $styles));
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/src/Exceptions/TemplatesException.php:
--------------------------------------------------------------------------------
1 | 'Unable to find the email template "{0}".',
7 | ];
8 |
--------------------------------------------------------------------------------
/src/Models/AttachmentModel.php:
--------------------------------------------------------------------------------
1 | where('name', $name)->first()) {
29 | return $template;
30 | }
31 |
32 | throw TemplatesException::forMissingTemplate($name);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Views/Defaults/styles.php:
--------------------------------------------------------------------------------
1 | /* -------------------------------------
2 | GLOBAL RESETS
3 | ------------------------------------- */
4 |
5 | img {
6 | border: none;
7 | -ms-interpolation-mode: bicubic;
8 | max-width: 100%;
9 | }
10 |
11 | body {
12 | background-color: #f6f6f6;
13 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
14 | -webkit-font-smoothing: antialiased;
15 | font-size: 14px;
16 | line-height: 1.4;
17 | margin: 0;
18 | padding: 0;
19 | -ms-text-size-adjust: 100%;
20 | -webkit-text-size-adjust: 100%;
21 | }
22 |
23 | table {
24 | border-collapse: separate;
25 | mso-table-lspace: 0pt;
26 | mso-table-rspace: 0pt;
27 | width: 100%; }
28 | table td {
29 | font-family: sans-serif;
30 | font-size: 14px;
31 | vertical-align: top;
32 | }
33 |
34 | /* -------------------------------------
35 | BODY & CONTAINER
36 | ------------------------------------- */
37 |
38 | .body {
39 | background-color: #f6f6f6;
40 | width: 100%;
41 | }
42 |
43 | /* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
44 | .container {
45 | display: block;
46 | margin: 0 auto !important;
47 | /* makes it centered */
48 | max-width: 580px;
49 | padding: 10px;
50 | width: 580px;
51 | }
52 |
53 | /* This should also be a block element, so that it will fill 100% of the .container */
54 | .content {
55 | box-sizing: border-box;
56 | display: block;
57 | margin: 0 auto;
58 | max-width: 580px;
59 | padding: 10px;
60 | }
61 |
62 | /* -------------------------------------
63 | HEADER, FOOTER, MAIN
64 | ------------------------------------- */
65 | .main {
66 | background: #ffffff;
67 | border-radius: 3px;
68 | width: 100%;
69 | }
70 |
71 | .wrapper {
72 | box-sizing: border-box;
73 | padding: 20px;
74 | }
75 |
76 | .content-block {
77 | padding-bottom: 10px;
78 | padding-top: 10px;
79 | }
80 |
81 | .footer {
82 | clear: both;
83 | margin-top: 10px;
84 | text-align: center;
85 | width: 100%;
86 | }
87 | .footer td,
88 | .footer p,
89 | .footer span,
90 | .footer a {
91 | color: #999999;
92 | font-size: 12px;
93 | text-align: center;
94 | }
95 |
96 | /* -------------------------------------
97 | TYPOGRAPHY
98 | ------------------------------------- */
99 | h1,
100 | h2,
101 | h3,
102 | h4 {
103 | color: #000000;
104 | font-family: sans-serif;
105 | font-weight: 400;
106 | line-height: 1.4;
107 | margin: 0;
108 | margin-bottom: 30px;
109 | }
110 |
111 | h1 {
112 | font-size: 35px;
113 | font-weight: 300;
114 | text-align: center;
115 | text-transform: capitalize;
116 | }
117 |
118 | p,
119 | ul,
120 | ol {
121 | font-family: sans-serif;
122 | font-size: 14px;
123 | font-weight: normal;
124 | margin: 0;
125 | margin-bottom: 15px;
126 | }
127 | p li,
128 | ul li,
129 | ol li {
130 | list-style-position: inside;
131 | margin-left: 5px;
132 | }
133 |
134 | a {
135 | color: #3498db;
136 | text-decoration: underline;
137 | }
138 |
139 | /* -------------------------------------
140 | BUTTONS
141 | ------------------------------------- */
142 | .btn {
143 | box-sizing: border-box;
144 | width: 100%; }
145 | .btn > tbody > tr > td {
146 | padding-bottom: 15px; }
147 | .btn table {
148 | width: auto;
149 | }
150 | .btn table td {
151 | background-color: #ffffff;
152 | border-radius: 5px;
153 | text-align: center;
154 | }
155 | .btn a {
156 | background-color: #ffffff;
157 | border: solid 1px #3498db;
158 | border-radius: 5px;
159 | box-sizing: border-box;
160 | color: #3498db;
161 | cursor: pointer;
162 | display: inline-block;
163 | font-size: 14px;
164 | font-weight: bold;
165 | margin: 0;
166 | padding: 12px 25px;
167 | text-decoration: none;
168 | text-transform: capitalize;
169 | }
170 |
171 | .btn-primary table td {
172 | background-color: #3498db;
173 | }
174 |
175 | .btn-primary a {
176 | background-color: #3498db;
177 | border-color: #3498db;
178 | color: #ffffff;
179 | }
180 |
181 | /* -------------------------------------
182 | OTHER STYLES THAT MIGHT BE USEFUL
183 | ------------------------------------- */
184 | .last {
185 | margin-bottom: 0;
186 | }
187 |
188 | .first {
189 | margin-top: 0;
190 | }
191 |
192 | .align-center {
193 | text-align: center;
194 | }
195 |
196 | .align-right {
197 | text-align: right;
198 | }
199 |
200 | .align-left {
201 | text-align: left;
202 | }
203 |
204 | .clear {
205 | clear: both;
206 | }
207 |
208 | .mt0 {
209 | margin-top: 0;
210 | }
211 |
212 | .mb0 {
213 | margin-bottom: 0;
214 | }
215 |
216 | .preheader {
217 | color: transparent;
218 | display: none;
219 | height: 0;
220 | max-height: 0;
221 | max-width: 0;
222 | opacity: 0;
223 | overflow: hidden;
224 | mso-hide: all;
225 | visibility: hidden;
226 | width: 0;
227 | }
228 |
229 | .powered-by a {
230 | text-decoration: none;
231 | }
232 |
233 | hr {
234 | border: 0;
235 | border-bottom: 1px solid #f6f6f6;
236 | margin: 20px 0;
237 | }
238 |
239 | /* -------------------------------------
240 | BRANDING
241 | ------------------------------------- */
242 | .text-orange {
243 | color: #e07a42;
244 | }
245 |
246 | .bg-orange {
247 | background-color: #fa751b;
248 | }
249 |
250 | .btn-orange table td {
251 | color: white;
252 | background-color: #e07a42;
253 | border-color: #e07a42;
254 | }
255 | .btn-orange a {
256 | color: white;
257 | background-color: #e07a42;
258 | border-color: #e07a42;
259 | }
260 |
--------------------------------------------------------------------------------
/src/Views/Defaults/template.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {title}
7 |
101 |
102 |
103 |
104 |
105 |
106 | |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | {body}
120 | |
121 |
122 |
123 | |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
141 |
142 |
143 |
144 | |
145 | |
146 |
147 |
148 |
149 |
150 |
--------------------------------------------------------------------------------
/src/Views/Templates/form.php:
--------------------------------------------------------------------------------
1 | extend(config('Layouts')->outbox) ?>
2 | section('navbar') ?>
3 |
4 | = view('Tatter\Outbox\Views\navbar') ?>
5 |
6 | endSection() ?>
7 | section('main') ?>
8 |
9 |
10 |
11 |
= $method ?> Template
12 |
= $template->name ?>
13 |
14 |
43 |
44 |
45 |
46 |
47 | endSection() ?>
48 |
--------------------------------------------------------------------------------
/src/Views/Templates/index.php:
--------------------------------------------------------------------------------
1 | extend(config('Layouts')->outbox) ?>
2 | section('navbar') ?>
3 |
4 | = view('Tatter\Outbox\Views\navbar') ?>
5 |
6 | endSection() ?>
7 | section('main') ?>
8 |
9 |
10 |
11 |
Email Templates
12 |
13 |
14 |
No templates.
15 |
16 |
17 |
18 |
19 | Name |
20 | Subject |
21 | Parent |
22 | Added |
23 | |
24 |
25 |
26 |
27 |
28 |
29 |
30 | = $template->name ?> |
31 | = $template->subject ?> |
32 | = ($parent = $template->getParent()) ? $parent->name : 'NONE' ?> |
33 | = $template->created_at->format('n/j/Y') ?> |
34 |
35 | = anchor('emails/templates/show/' . $template->id, 'View') ?>
36 | |
37 | = anchor('emails/templates/edit/' . $template->id, 'Edit') ?>
38 | |
39 | = anchor('emails/templates/new/' . $template->id, 'Clone') ?>
40 | |
41 | = anchor('emails/templates/send/' . $template->id, 'Send') ?>
42 | |
43 | = anchor('emails/templates/remove/' . $template->id, 'Remove') ?>
44 | |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | endSection() ?>
56 |
--------------------------------------------------------------------------------
/src/Views/Templates/send.php:
--------------------------------------------------------------------------------
1 | extend(config('Layouts')->outbox) ?>
2 | section('navbar') ?>
3 |
4 | = view('Tatter\Outbox\Views\navbar') ?>
5 |
6 | endSection() ?>
7 | section('main') ?>
8 |
9 |
10 |
11 |
Send Email
12 |
Template: = $template->name ?>
13 |
14 |
76 |
77 |
78 |
79 | endSection() ?>
80 |
--------------------------------------------------------------------------------
/src/Views/layout.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Email Templates
9 |
10 |
11 |
12 |
13 |
14 | = $this->renderSection('headerAssets') ?>
15 |
16 |
17 |
18 |
30 |
31 | getFlashdata('error')): ?>
32 | = $error ?>
33 |
34 | getFlashdata('success')): ?>
35 | = $success ?>
36 |
37 |
38 |
39 |
40 | = $this->renderSection('main') ?>
41 |
42 |
43 |
44 |
45 |
46 |
47 | = $this->renderSection('footerAssets') ?>
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/Views/navbar.php:
--------------------------------------------------------------------------------
1 |
2 |
10 |
--------------------------------------------------------------------------------