├── .gitignore
├── README.md
├── composer.json
├── phpunit.xml.dist
├── src
├── Mail.php
├── config.php
├── facade
│ └── Mail.php
└── mail
│ ├── Mailable.php
│ ├── Mailer.php
│ ├── Markdown.php
│ ├── Message.php
│ ├── SendQueuedMailable.php
│ ├── resource
│ ├── css
│ │ └── default.css
│ └── view
│ │ ├── button.twig
│ │ ├── code.twig
│ │ ├── footer.twig
│ │ ├── header.twig
│ │ ├── layout.twig
│ │ ├── message.twig
│ │ ├── panel.twig
│ │ ├── promotion.twig
│ │ ├── subcopy.twig
│ │ └── table.twig
│ └── twig
│ ├── Node
│ └── Component.php
│ └── TokenParser
│ └── Component.php
└── tests
├── TwigComponentTest.php
├── bootstrap.php
└── fixtures
└── normal.twig
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea
2 | /vendor
3 | composer.lock
4 | /runtime
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ThinkPHP6 邮件发送扩展
2 |
3 | 支持 `smtp` `sendmail` `mail` `sendcloud` `log` 等驱动,其中`log`驱动会把邮件内容写入日志,供调试用
4 |
5 | ## 安装
6 | ~~~
7 | composer require yunwuxin/think-mail
8 | ~~~
9 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "yunwuxin/think-mail",
3 | "description": "The Mailer Library For ThinkPHP6",
4 | "require": {
5 | "php": ">=8.0",
6 | "topthink/think-queue": "^3.0",
7 | "cebe/markdown": "^1.1",
8 | "yunwuxin/think-twig": "^3.0",
9 | "tijsverkoyen/css-to-inline-styles": "^2.2",
10 | "topthink/framework": "^6.0|^8.0",
11 | "nette/mail": "^4.0"
12 | },
13 | "require-dev": {
14 | "mikey179/vfsstream": "^1.6",
15 | "mockery/mockery": "^1.2",
16 | "phpunit/phpunit": "^7.0|^8.0"
17 | },
18 | "license": "Apache-2.0",
19 | "authors": [
20 | {
21 | "name": "yunwuxin",
22 | "email": "448901948@qq.com"
23 | }
24 | ],
25 | "autoload": {
26 | "psr-4": {
27 | "yunwuxin\\": "src"
28 | }
29 | },
30 | "autoload-dev": {
31 | "psr-4": {
32 | "think\\tests\\": "tests/"
33 | }
34 | },
35 | "extra": {
36 | "think": {
37 | "config": {
38 | "mail": "src/config.php"
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
17 | ./tests
18 |
19 |
20 |
21 |
22 | ./src/think
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/Mail.php:
--------------------------------------------------------------------------------
1 |
10 | // +----------------------------------------------------------------------
11 | namespace yunwuxin;
12 |
13 | use InvalidArgumentException;
14 | use Nette\Mail\SendmailMailer;
15 | use Nette\Mail\SmtpMailer;
16 | use think\helper\Arr;
17 | use think\Manager;
18 | use yunwuxin\mail\Mailable;
19 | use yunwuxin\mail\Mailer;
20 |
21 | /**
22 | * Class Mail
23 | *
24 | * @package yunwuxin
25 | * @method Mailer from($users)
26 | * @method Mailer to($users)
27 | * @method Mailer cc($users)
28 | * @method Mailer bcc($users)
29 | * @method send(Mailable $mailable)
30 | * @method sendNow(Mailable $mailable)
31 | * @method queue(Mailable $mailable)
32 | * @method array failures()
33 | */
34 | class Mail extends Manager
35 | {
36 | public function getConfig(string $name = null, $default = null)
37 | {
38 | if (!is_null($name)) {
39 | return $this->app->config->get('mail.' . $name, $default);
40 | }
41 |
42 | return $this->app->config->get('mail');
43 | }
44 |
45 | public function getTransportConfig($transport, $name = null, $default = null)
46 | {
47 | if ($config = $this->getConfig("transports.{$transport}")) {
48 | return Arr::get($config, $name, $default);
49 | }
50 |
51 | throw new InvalidArgumentException("Transport [$transport] not found.");
52 | }
53 |
54 | protected function createSmtpDriver($config)
55 | {
56 | return new SmtpMailer(
57 | host: $config['host'],
58 | port: $config['port'],
59 | username: $config['username'],
60 | password: $config['password'],
61 | encryption: $config['encryption'] ?? null,
62 | );
63 | }
64 |
65 | protected function createSendmailDriver()
66 | {
67 | return new SendmailMailer();
68 | }
69 |
70 | protected function resolveConfig(string $name)
71 | {
72 | return $this->getTransportConfig($name);
73 | }
74 |
75 | protected function createDriver(string $name)
76 | {
77 | $transport = parent::createDriver($name);
78 |
79 | /** @var Mailer $mailer */
80 | $mailer = $this->app->invokeClass(Mailer::class, [$transport]);
81 |
82 | $mailer->from($this->app->config->get('mail.from'));
83 |
84 | return $mailer;
85 | }
86 |
87 | /**
88 | * 默认驱动
89 | * @return string|null
90 | */
91 | public function getDefaultDriver()
92 | {
93 | return $this->getConfig('default');
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/config.php:
--------------------------------------------------------------------------------
1 |
10 | // +----------------------------------------------------------------------
11 |
12 | return [
13 | 'default' => 'smtp',
14 | 'transports' => [
15 | 'smtp' => [
16 | 'type' => 'smtp',
17 | 'host' => 'mail.example.com',
18 | 'port' => 25,
19 | 'encryption' => 'tls',
20 | 'username' => 'username',
21 | 'password' => 'password',
22 | ],
23 | 'sendmail' => [
24 | 'type' => 'sendmail',
25 | ],
26 | ],
27 | 'from' => [
28 | 'address' => 'example@example',
29 | 'name' => 'App Name',
30 | ],
31 | ];
32 |
--------------------------------------------------------------------------------
/src/facade/Mail.php:
--------------------------------------------------------------------------------
1 |
10 | // +----------------------------------------------------------------------
11 |
12 | namespace yunwuxin\mail;
13 |
14 | use think\Collection;
15 |
16 | /**
17 | * Class Mailable
18 | * @package yunwuxin\mail
19 | *
20 | * @property string $queue
21 | * @property integer $delay
22 | * @property string $connection
23 | */
24 | class Mailable
25 | {
26 | /** @var array 发信人 */
27 | public $from = [];
28 |
29 | /** @var array 收信人 */
30 | public $to = [];
31 |
32 | /** @var array 抄送 */
33 | public $cc = [];
34 |
35 | /** @var array 密送 */
36 | public $bcc = [];
37 |
38 | /** @var array 回复人 */
39 | public $replyTo = [];
40 |
41 | /** @var string 标题 */
42 | public $subject;
43 |
44 | /** @var string 邮件内容(富文本) */
45 | public $view;
46 |
47 | /** @var string 邮件内容(纯文本) */
48 | public $textView;
49 |
50 | /** @var string 邮件内容(MarkDown) */
51 | public $markdown;
52 |
53 | /** @var array 动态数据 */
54 | public $viewData = [];
55 |
56 | /** @var array 附件(文件名) */
57 | public $attachments = [];
58 |
59 | /** @var array 附件(数据) */
60 | public $rawAttachments = [];
61 |
62 | public $callbacks = [];
63 |
64 | public $markdownCallback = null;
65 |
66 | protected function build()
67 | {
68 | //...
69 | }
70 |
71 | public function withMessage($callback)
72 | {
73 | $this->callbacks[] = $callback;
74 |
75 | return $this;
76 | }
77 |
78 | /**
79 | * 设置发信人
80 | * @param $address
81 | * @param null $name
82 | * @return Mailable
83 | */
84 | public function from($address, $name = null)
85 | {
86 | return $this->setAddress($address, $name, 'from');
87 | }
88 |
89 | /**
90 | * 设置收信人
91 | * @param $address
92 | * @param null $name
93 | * @return Mailable
94 | */
95 | public function to($address, $name = null)
96 | {
97 | return $this->setAddress($address, $name, 'to');
98 | }
99 |
100 | /**
101 | * 设置抄送
102 | * @param $address
103 | * @param null $name
104 | * @return Mailable
105 | */
106 | public function cc($address, $name = null)
107 | {
108 | return $this->setAddress($address, $name, 'cc');
109 | }
110 |
111 | /**
112 | * 设置密送
113 | * @param $address
114 | * @param null $name
115 | * @return Mailable
116 | */
117 | public function bcc($address, $name = null)
118 | {
119 | return $this->setAddress($address, $name, 'bcc');
120 | }
121 |
122 | /**
123 | * 设置回复人
124 | * @param $address
125 | * @param null $name
126 | * @return Mailable
127 | */
128 | public function replyTo($address, $name = null)
129 | {
130 | return $this->setAddress($address, $name, 'replyTo');
131 | }
132 |
133 | /**
134 | * 设置地址
135 | *
136 | * @param object|array|string $address
137 | * @param string|null $name
138 | * @param string $property
139 | * @return $this
140 | */
141 | protected function setAddress($address, $name = null, $property = 'to')
142 | {
143 | if (is_object($address) && !$address instanceof Collection) {
144 | $address = [$address];
145 | }
146 |
147 | if ($address instanceof Collection || is_array($address)) {
148 | foreach ($address as $user) {
149 | $user = $this->parseUser($user);
150 |
151 | $this->{$property}($user->email, isset($user->name) ? $user->name : null);
152 | }
153 | } else {
154 | $this->{$property}[] = compact('address', 'name');
155 | }
156 |
157 | return $this;
158 | }
159 |
160 | /**
161 | * 格式化用户
162 | * @param $user
163 | * @return object
164 | */
165 | protected function parseUser($user)
166 | {
167 | if (is_array($user)) {
168 | return (object) $user;
169 | } elseif (is_string($user)) {
170 | return (object) ['email' => $user];
171 | }
172 |
173 | return $user;
174 | }
175 |
176 | /**
177 | * 设置标题
178 | * @param $subject
179 | * @return $this
180 | */
181 | public function subject($subject)
182 | {
183 | $this->subject = $subject;
184 |
185 | return $this;
186 | }
187 |
188 | /**
189 | * 设置模板
190 | * @param $view
191 | * @param array $data
192 | * @return $this
193 | */
194 | public function view($view, array $data = [])
195 | {
196 | $this->view = $view;
197 | $this->viewData = $data;
198 |
199 | return $this;
200 | }
201 |
202 | /**
203 | * 设置文本
204 | * @param $textView
205 | * @param array $data
206 | * @return $this
207 | */
208 | public function text($textView, array $data = [])
209 | {
210 | $this->textView = $textView;
211 | $this->viewData = $data;
212 |
213 | return $this;
214 | }
215 |
216 | public function markdown($markdown, array $data = [], $callback = null)
217 | {
218 | $this->markdown = $markdown;
219 | $this->viewData = $data;
220 | $this->markdownCallback = $callback;
221 |
222 | return $this;
223 | }
224 |
225 | /**
226 | * 设置数据
227 | * @param $key
228 | * @param null $value
229 | * @return $this
230 | */
231 | public function with($key, $value = null)
232 | {
233 | if (is_array($key)) {
234 | $this->viewData = array_merge($this->viewData, $key);
235 | } else {
236 | $this->viewData[$key] = $value;
237 | }
238 |
239 | return $this;
240 | }
241 |
242 | /**
243 | * 设置附件
244 | * @param $file
245 | * @return $this
246 | */
247 | public function attach($file)
248 | {
249 | $this->attachments[] = $file;
250 |
251 | return $this;
252 | }
253 |
254 | /**
255 | * 设置附件
256 | * @param $data
257 | * @param $name
258 | * @return $this
259 | */
260 | public function attachData($data, $name)
261 | {
262 | $this->rawAttachments[] = compact('data', 'name');
263 |
264 | return $this;
265 | }
266 | }
267 |
--------------------------------------------------------------------------------
/src/mail/Mailer.php:
--------------------------------------------------------------------------------
1 |
10 | // +----------------------------------------------------------------------
11 | namespace yunwuxin\mail;
12 |
13 | use Nette\Mail\Mailer as Transport;
14 | use think\App;
15 | use think\Queue;
16 | use think\queue\Queueable;
17 | use think\queue\ShouldQueue;
18 |
19 | class Mailer
20 | {
21 |
22 | /** @var Transport */
23 | protected $transport;
24 |
25 | /** @var array 发信人 */
26 | protected $from;
27 |
28 | /** @var array 收信人 */
29 | protected $to = [];
30 |
31 | /** @var array 抄送 */
32 | protected $cc = [];
33 |
34 | /** @var array 密送 */
35 | protected $bcc = [];
36 |
37 | /** @var Queue */
38 | protected $queue;
39 |
40 | /** @var App */
41 | protected $app;
42 |
43 | public function __construct(Transport $transport, Queue $queue, App $app)
44 | {
45 | $this->transport = $transport;
46 | $this->queue = $queue;
47 | $this->app = $app;
48 | }
49 |
50 | public function from($users)
51 | {
52 | $this->from = $users;
53 | }
54 |
55 | public function to($users)
56 | {
57 | $this->to = $users;
58 |
59 | return $this;
60 | }
61 |
62 | public function cc($users)
63 | {
64 | $this->cc = $users;
65 |
66 | return $this;
67 | }
68 |
69 | public function bcc($users)
70 | {
71 | $this->bcc = $users;
72 |
73 | return $this;
74 | }
75 |
76 | /**
77 | * 发送邮件
78 | * @param Mailable $mailable
79 | */
80 | public function send(Mailable $mailable)
81 | {
82 | if ($mailable instanceof ShouldQueue) {
83 | $this->queue($mailable);
84 | } else {
85 | $this->sendNow($mailable);
86 | }
87 | }
88 |
89 | /**
90 | * 发送邮件(立即发送)
91 | * @param Mailable $mailable
92 | */
93 | public function sendNow(Mailable $mailable)
94 | {
95 | $message = $this->createMessage($mailable);
96 |
97 | if (isset($this->to['address'])) {
98 | $message->to($this->to['address'], $this->to['name']);
99 | }
100 |
101 | if (!empty($this->cc)) {
102 | $message->cc($this->cc);
103 | }
104 | if (!empty($this->bcc)) {
105 | $message->bcc($this->bcc);
106 | }
107 |
108 | $this->sendMessage($message);
109 | }
110 |
111 | /**
112 | * 推送至队列发送
113 | * @param Mailable|ShouldQueue $mailable
114 | */
115 | public function queue($mailable)
116 | {
117 | $job = new SendQueuedMailable($mailable);
118 |
119 | if (in_array(Queueable::class, class_uses_recursive($mailable))) {
120 | $queue = $this->queue->connection($mailable->connection);
121 | if ($mailable->delay > 0) {
122 | $queue->later($mailable->delay, $job, '', $mailable->queue);
123 | } else {
124 | $queue->push($job, '', $mailable->queue);
125 | }
126 | } else {
127 | $this->queue->push($job);
128 | }
129 | }
130 |
131 | /**
132 | * 创建Message
133 | * @param Mailable $mailable
134 | * @return Message
135 | */
136 | protected function createMessage(Mailable $mailable)
137 | {
138 | if (!empty($this->from['address'])) {
139 | $mailable->from($this->from['address'], $this->from['name']);
140 | }
141 |
142 | return $this->app->invokeClass(Message::class, [$mailable]);
143 | }
144 |
145 | /**
146 | * 发送Message
147 | * @param Message $message
148 | */
149 | protected function sendMessage($message)
150 | {
151 | $this->transport->send($message->getMail());
152 | }
153 |
154 | }
155 |
--------------------------------------------------------------------------------
/src/mail/Markdown.php:
--------------------------------------------------------------------------------
1 |
10 | // +----------------------------------------------------------------------
11 |
12 | namespace yunwuxin\mail;
13 |
14 | use Closure;
15 | use ReflectionClass;
16 | use ReflectionProperty;
17 | use think\App;
18 | use think\helper\Str;
19 | use think\View;
20 | use think\view\driver\Twig;
21 | use TijsVerkoyen\CssToInlineStyles\CssToInlineStyles;
22 | use Twig\TwigFilter;
23 | use yunwuxin\mail\twig\TokenParser\Component;
24 |
25 | /**
26 | * Class Message
27 | * @package yunwuxin\mail
28 | *
29 | * @mixin \Nette\Mail\Message
30 | */
31 | class Message
32 | {
33 | /** @var \Nette\Mail\Message */
34 | protected $mail;
35 |
36 | /** @var View */
37 | protected $view;
38 |
39 | /** @var App */
40 | protected $app;
41 |
42 | public function __construct(Mailable $mailable, View $view, App $app)
43 | {
44 | $this->mail = new \Nette\Mail\Message();
45 | $this->view = $view;
46 | $this->app = $app;
47 |
48 | $this->build($mailable);
49 | }
50 |
51 | protected function build(Mailable $mailable)
52 | {
53 | $this->app->invoke([$mailable, 'build'], [], true);
54 |
55 | $this->buildContent($mailable)
56 | ->buildFrom($mailable)
57 | ->buildRecipients($mailable)
58 | ->buildSubject($mailable)
59 | ->runCallbacks($mailable)
60 | ->buildAttachments($mailable);
61 | }
62 |
63 | /**
64 | * 构造数据
65 | * @param Mailable $mailable
66 | * @return array
67 | */
68 | protected function buildViewData(Mailable $mailable)
69 | {
70 | $data = $mailable->viewData;
71 |
72 | foreach ((new ReflectionClass($mailable))->getProperties(ReflectionProperty::IS_PUBLIC) as $property) {
73 | if ($property->getDeclaringClass()->getName() !== Mailable::class) {
74 | $data[$property->getName()] = $property->getValue($mailable);
75 | }
76 | }
77 |
78 | $data['message'] = $this;
79 |
80 | return $data;
81 | }
82 |
83 | /**
84 | * 添加内容
85 | * @param Mailable $mailable
86 | * @return $this
87 | */
88 | protected function buildContent(Mailable $mailable)
89 | {
90 | $data = $this->buildViewData($mailable);
91 |
92 | if (isset($mailable->markdown)) {
93 |
94 | $html = $this->parseDown($mailable->markdown, $data, $mailable->markdownCallback);
95 |
96 | $css = $this->app->config->get('mail.css', __DIR__ . '/resource/css/default.css');
97 |
98 | $html = (new CssToInlineStyles())->convert($html, file_get_contents($css));
99 |
100 | $this->setHtmlBody($html);
101 | } else {
102 | if (isset($mailable->view)) {
103 | $this->setHtmlBody($this->fetchView($mailable->view, $data));
104 | } elseif (isset($mailable->textView)) {
105 | $method = isset($mailable->view) ? 'addPart' : 'setBody';
106 |
107 | $this->$method($this->fetchView($mailable->textView, $data));
108 | }
109 | }
110 | return $this;
111 | }
112 |
113 | /**
114 | * 解析markdown
115 | * @param $view
116 | * @param $data
117 | * @param Closure|null $callback
118 | * @return string
119 | */
120 | protected function parseDown($view, $data, Closure $callback = null)
121 | {
122 | /** @var Twig $twig */
123 | $twig = $this->view->engine('twig');
124 |
125 | $parser = new Markdown();
126 | $parser->html5 = true;
127 |
128 | $twig->getTwig()->addFilter(new TwigFilter('maildown', function ($content) use ($parser) {
129 | $content = preg_replace('/^[^\S\n]+/m', '', $content);
130 | return $parser->parse($content);
131 | }));
132 |
133 | $twig->getTwig()->addTokenParser(new Component());
134 |
135 | $twig->getLoader()->addPath(__DIR__ . '/resource/view', 'mail');
136 |
137 | if ($callback) {
138 | $callback($twig);
139 | }
140 |
141 | $content = $twig->getTwig()->render($view . '.twig', $data);
142 |
143 | //清理
144 | $this->view->forgetDriver('twig');
145 |
146 | return $content;
147 | }
148 |
149 | /**
150 | * 调用模板引擎渲染模板
151 | * @param $view
152 | * @param $data
153 | * @return string
154 | */
155 | protected function fetchView($view, $data)
156 | {
157 | return $this->view->fetch($view, $data);
158 | }
159 |
160 | /**
161 | * 构造发信人
162 | * @param Mailable $mailable
163 | * @return $this
164 | */
165 | protected function buildFrom(Mailable $mailable)
166 | {
167 | if (!empty($mailable->from)) {
168 | $this->from($mailable->from[0]['address'], $mailable->from[0]['name']);
169 | }
170 | return $this;
171 | }
172 |
173 | /**
174 | * 构造收信人
175 | * @param Mailable $mailable
176 | * @return $this
177 | */
178 | protected function buildRecipients(Mailable $mailable)
179 | {
180 | foreach (['to', 'cc', 'bcc', 'replyTo'] as $type) {
181 | foreach ($mailable->{$type} as $recipient) {
182 | $this->{$type}($recipient['address'], $recipient['name']);
183 | }
184 | }
185 |
186 | return $this;
187 | }
188 |
189 | /**
190 | * 构造标题
191 | * @param Mailable $mailable
192 | * @return $this
193 | */
194 | protected function buildSubject(Mailable $mailable)
195 | {
196 | if ($mailable->subject) {
197 | $this->subject($mailable->subject);
198 | } else {
199 | $this->subject(Str::title(Str::snake(class_basename($mailable), ' ')));
200 | }
201 |
202 | return $this;
203 | }
204 |
205 | /**
206 | * 构造附件
207 | * @param Mailable $mailable
208 | * @return $this
209 | */
210 | protected function buildAttachments(Mailable $mailable)
211 | {
212 | foreach ($mailable->attachments as $attachment) {
213 | $this->attach($attachment);
214 | }
215 |
216 | foreach ($mailable->rawAttachments as $attachment) {
217 | $this->attach($attachment['name'], $attachment['data']);
218 | }
219 |
220 | return $this;
221 | }
222 |
223 | /**
224 | * 执行回调
225 | *
226 | * @param Mailable $mailable
227 | * @return $this
228 | */
229 | protected function runCallbacks(Mailable $mailable)
230 | {
231 | foreach ($mailable->callbacks as $callback) {
232 | $callback($this->mail);
233 | }
234 |
235 | return $this;
236 | }
237 |
238 | /**
239 | * Add a "from" address to the message.
240 | *
241 | * @param string|array $address
242 | * @param string|null $name
243 | * @return $this
244 | */
245 | public function from($address, $name = null)
246 | {
247 | $this->mail->setFrom($address, $name);
248 |
249 | return $this;
250 | }
251 |
252 | /**
253 | * Set the "return path" of the message.
254 | *
255 | * @param string $address
256 | * @return $this
257 | */
258 | public function returnPath($address)
259 | {
260 | $this->mail->setReturnPath($address);
261 |
262 | return $this;
263 | }
264 |
265 | /**
266 | * Add a recipient to the message.
267 | *
268 | * @param string|array $address
269 | * @param string|null $name
270 | * @return $this
271 | */
272 | public function to($address, $name = null)
273 | {
274 | $this->mail->addTo($address, $name);
275 |
276 | return $this;
277 | }
278 |
279 | /**
280 | * Add a carbon copy to the message.
281 | *
282 | * @param string|array $address
283 | * @param string|null $name
284 | * @return $this
285 | */
286 | public function cc($address, $name = null)
287 | {
288 | $this->mail->addCc($address, $name);
289 | return $this;
290 | }
291 |
292 | /**
293 | * Add a blind carbon copy to the message.
294 | *
295 | * @param string|array $address
296 | * @param string|null $name
297 | * @return $this
298 | */
299 | public function bcc($address, $name = null)
300 | {
301 | $this->mail->addBcc($address, $name);
302 | return $this;
303 | }
304 |
305 | /**
306 | * Add a reply to address to the message.
307 | *
308 | * @param string|array $address
309 | * @param string|null $name
310 | * @return $this
311 | */
312 | public function replyTo($address, $name = null)
313 | {
314 | $this->mail->addReplyTo($address, $name);
315 | return $this;
316 | }
317 |
318 | /**
319 | * Set the subject of the message.
320 | *
321 | * @param string $subject
322 | * @return $this
323 | */
324 | public function subject($subject)
325 | {
326 | $this->mail->setSubject($subject);
327 | return $this;
328 | }
329 |
330 | /**
331 | * Set the message priority level.
332 | *
333 | * @param int $level
334 | * @return $this
335 | */
336 | public function priority($level)
337 | {
338 | $this->mail->setPriority($level);
339 | return $this;
340 | }
341 |
342 | /**
343 | * Attach a file to the message.
344 | *
345 | * @param string $file
346 | * @return $this
347 | */
348 | public function attach($file, $content = null, $contentType = null)
349 | {
350 | $this->mail->addAttachment($file, $content, $contentType);
351 | return $this;
352 | }
353 |
354 | /**
355 | * Embed a file in the message and get the CID.
356 | *
357 | * @param string $file
358 | */
359 | public function embed($file, $content = null, $contentType = null)
360 | {
361 | return $this->mail->addEmbeddedFile($file, $content, $contentType);
362 | }
363 |
364 | /**
365 | * Get the underlying Swift Message instance.
366 | *
367 | * @return \Nette\Mail\Message
368 | */
369 | public function getMail()
370 | {
371 | return $this->mail;
372 | }
373 |
374 | /**
375 | * Dynamically pass missing methods to the Swift instance.
376 | *
377 | * @param string $method
378 | * @param array $parameters
379 | * @return mixed
380 | */
381 | public function __call($method, $parameters)
382 | {
383 | $callable = [$this->mail, $method];
384 |
385 | return call_user_func_array($callable, $parameters);
386 | }
387 | }
388 |
--------------------------------------------------------------------------------
/src/mail/SendQueuedMailable.php:
--------------------------------------------------------------------------------
1 |
10 | // +----------------------------------------------------------------------
11 |
12 | namespace yunwuxin\mail;
13 |
14 | use yunwuxin\Mail;
15 |
16 | class SendQueuedMailable
17 | {
18 | /** @var Mailable */
19 | protected $mailable;
20 |
21 | public function __construct(Mailable $mailable)
22 | {
23 | $this->mailable = $mailable;
24 | }
25 |
26 | public function handle(Mail $mail)
27 | {
28 | $mail->sendNow($this->mailable);
29 | }
30 |
31 | public function __clone()
32 | {
33 | $this->mailable = clone $this->mailable;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/mail/resource/css/default.css:
--------------------------------------------------------------------------------
1 | /* Base */
2 |
3 | body, body *:not(html):not(style):not(br):not(tr):not(code) {
4 | font-family: "微软雅黑", "Microsoft Yahei", Avenir, Helvetica, sans-serif;
5 | box-sizing: border-box;
6 | }
7 |
8 | body {
9 | background-color: #f5f8fa;
10 | color: #74787E;
11 | height: 100%;
12 | hyphens: auto;
13 | line-height: 1.4;
14 | margin: 0;
15 | -moz-hyphens: auto;
16 | -ms-word-break: break-all;
17 | width: 100% !important;
18 | -webkit-hyphens: auto;
19 | -webkit-text-size-adjust: none;
20 | word-break: break-all;
21 | word-break: break-word;
22 | font-size: 14px;
23 | }
24 |
25 | p,
26 | ul,
27 | ol,
28 | blockquote {
29 | line-height: 1.4;
30 | text-align: left;
31 | }
32 |
33 | a {
34 | color: #3869D4;
35 | }
36 |
37 | a img {
38 | border: none;
39 | }
40 |
41 | /* Typography */
42 |
43 | h1 {
44 | color: #2F3133;
45 | font-size: 19px;
46 | font-weight: bold;
47 | margin-top: 0;
48 | text-align: left;
49 | }
50 |
51 | h2 {
52 | color: #2F3133;
53 | font-size: 16px;
54 | font-weight: bold;
55 | margin-top: 0;
56 | text-align: left;
57 | }
58 |
59 | h3 {
60 | color: #2F3133;
61 | font-size: 14px;
62 | font-weight: bold;
63 | margin-top: 0;
64 | text-align: left;
65 | }
66 |
67 | p {
68 | color: #74787E;
69 | font-size: 14px;
70 | line-height: 1.4;
71 | margin-top: 0;
72 | text-align: left;
73 | }
74 |
75 | p.sub {
76 | font-size: 12px;
77 | }
78 |
79 | img {
80 | max-width: 100%;
81 | }
82 |
83 | /* Layout */
84 |
85 | .wrapper {
86 | background-color: #f5f8fa;
87 | margin: 0;
88 | padding: 0;
89 | width: 100%;
90 | -premailer-cellpadding: 0;
91 | -premailer-cellspacing: 0;
92 | -premailer-width: 100%;
93 | }
94 |
95 | .content {
96 | margin: 0;
97 | padding: 0;
98 | width: 100%;
99 | -premailer-cellpadding: 0;
100 | -premailer-cellspacing: 0;
101 | -premailer-width: 100%;
102 | }
103 |
104 | /* Header */
105 |
106 | .header {
107 | padding: 25px 0;
108 | text-align: center;
109 | }
110 |
111 | .header a {
112 | color: #bbbfc3;
113 | font-size: 19px;
114 | font-weight: bold;
115 | text-decoration: none;
116 | text-shadow: 0 1px 0 white;
117 | }
118 |
119 | /* Body */
120 |
121 | .body {
122 | background-color: #FFFFFF;
123 | border-bottom: 1px solid #EDEFF2;
124 | border-top: 1px solid #EDEFF2;
125 | margin: 0;
126 | padding: 0;
127 | width: 100%;
128 | -premailer-cellpadding: 0;
129 | -premailer-cellspacing: 0;
130 | -premailer-width: 100%;
131 | }
132 |
133 | .inner-body {
134 | background-color: #FFFFFF;
135 | margin: 0 auto;
136 | padding: 0;
137 | width: 570px;
138 | -premailer-cellpadding: 0;
139 | -premailer-cellspacing: 0;
140 | -premailer-width: 570px;
141 | }
142 |
143 | /* Subcopy */
144 |
145 | .subcopy {
146 | border-top: 1px solid #EDEFF2;
147 | margin-top: 25px;
148 | padding-top: 25px;
149 | }
150 |
151 | .subcopy p {
152 | font-size: 12px;
153 | }
154 |
155 | /* Footer */
156 |
157 | .footer {
158 | margin: 0 auto;
159 | padding: 0;
160 | text-align: center;
161 | width: 570px;
162 | -premailer-cellpadding: 0;
163 | -premailer-cellspacing: 0;
164 | -premailer-width: 570px;
165 | }
166 |
167 | .footer p {
168 | color: #AEAEAE;
169 | font-size: 12px;
170 | text-align: center;
171 | }
172 |
173 | /* Tables */
174 |
175 | .table table {
176 | margin: 30px auto;
177 | width: 100%;
178 | -premailer-cellpadding: 0;
179 | -premailer-cellspacing: 0;
180 | -premailer-width: 100%;
181 | }
182 |
183 | .table th {
184 | border-bottom: 1px solid #EDEFF2;
185 | padding-bottom: 8px;
186 | }
187 |
188 | .table td {
189 | color: #74787E;
190 | font-size: 15px;
191 | line-height: 18px;
192 | padding: 10px 0;
193 | }
194 |
195 | .content-cell {
196 | padding: 35px;
197 | }
198 |
199 | /* Buttons */
200 |
201 | .action {
202 | margin: 30px auto;
203 | padding: 0;
204 | text-align: center;
205 | width: 100%;
206 | -premailer-cellpadding: 0;
207 | -premailer-cellspacing: 0;
208 | -premailer-width: 100%;
209 | }
210 |
211 | .button {
212 | border-radius: 3px;
213 | box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
214 | color: #FFF;
215 | display: inline-block;
216 | text-decoration: none;
217 | -webkit-text-size-adjust: none;
218 | }
219 |
220 | .button-blue {
221 | background-color: #3097D1;
222 | border-top: 10px solid #3097D1;
223 | border-right: 18px solid #3097D1;
224 | border-bottom: 10px solid #3097D1;
225 | border-left: 18px solid #3097D1;
226 | }
227 |
228 | .button-green {
229 | background-color: #2ab27b;
230 | border-top: 10px solid #2ab27b;
231 | border-right: 18px solid #2ab27b;
232 | border-bottom: 10px solid #2ab27b;
233 | border-left: 18px solid #2ab27b;
234 | }
235 |
236 | .button-red {
237 | background-color: #bf5329;
238 | border-top: 10px solid #bf5329;
239 | border-right: 18px solid #bf5329;
240 | border-bottom: 10px solid #bf5329;
241 | border-left: 18px solid #bf5329;
242 | }
243 |
244 | .code {
245 | padding: 10px 18px 10px 18px;
246 | border-radius: 3px;
247 | text-align: center;
248 | text-decoration: none;
249 | background-color: #ecf4fb;
250 | color: #4581E9;
251 | font-size: 20px;
252 | font-weight: 700;
253 | letter-spacing: 2px;
254 | margin: 0;
255 | white-space: nowrap;
256 | line-height: 1.7;
257 | }
258 |
259 | /* Panels */
260 |
261 | .panel {
262 | margin: 0 0 21px;
263 | }
264 |
265 | .panel-content {
266 | background-color: #EDEFF2;
267 | padding: 16px;
268 | }
269 |
270 | .panel-item {
271 | padding: 0;
272 | }
273 |
274 | .panel-item p:last-of-type {
275 | margin-bottom: 0;
276 | padding-bottom: 0;
277 | }
278 |
279 | /* Promotions */
280 |
281 | .promotion {
282 | background-color: #FFFFFF;
283 | border: 2px dashed #9BA2AB;
284 | margin: 0;
285 | margin-bottom: 25px;
286 | margin-top: 25px;
287 | padding: 24px;
288 | width: 100%;
289 | -premailer-cellpadding: 0;
290 | -premailer-cellspacing: 0;
291 | -premailer-width: 100%;
292 | }
293 |
294 | .promotion h1 {
295 | text-align: center;
296 | }
297 |
298 | .promotion p {
299 | font-size: 15px;
300 | text-align: center;
301 | }
302 |
--------------------------------------------------------------------------------
/src/mail/resource/view/button.twig:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
15 | |
16 |
17 |
18 | |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/mail/resource/view/code.twig:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | {{ slot }}
11 | |
12 |
13 |
14 | |
15 |
16 |
17 | |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/mail/resource/view/footer.twig:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 | |
11 |
12 |
--------------------------------------------------------------------------------
/src/mail/resource/view/header.twig:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/src/mail/resource/view/layout.twig:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
26 |
27 |
28 |
29 |
30 |
31 | {% block header %}
32 | {% endblock %}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | {% block body %}
42 | {% endblock %}
43 |
44 | {% block subcopy %}
45 | {% endblock %}
46 | |
47 |
48 |
49 | |
50 |
51 |
52 | {% block footer %}
53 | {% endblock %}
54 |
55 | |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/src/mail/resource/view/message.twig:
--------------------------------------------------------------------------------
1 | {% extends "@mail/layout.twig" %}
2 |
3 | {% block header %}
4 | {% component "@mail/header" with { url: config('app.url') } %}
5 | {{ config('app.name') }}
6 | {% endcomponent %}
7 | {% endblock %}
8 |
9 | {% block body %}
10 | {{ slot|maildown|raw }}
11 | {% endblock %}
12 |
13 | {% block footer %}
14 | {% component "@mail/footer" %}
15 | © {{ 'now'|date('Y') }} {{ config('app.name') }}. All rights reserved.
16 | {% endcomponent %}
17 | {% endblock %}
18 |
--------------------------------------------------------------------------------
/src/mail/resource/view/panel.twig:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ slot|maildown|raw }}
8 | |
9 |
10 |
11 | |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/mail/resource/view/promotion.twig:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/mail/resource/view/subcopy.twig:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ slot|maildown|raw }}
5 | |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/mail/resource/view/table.twig:
--------------------------------------------------------------------------------
1 |
2 | {{ slot|maildown|raw }}
3 |
4 |
--------------------------------------------------------------------------------
/src/mail/twig/Node/Component.php:
--------------------------------------------------------------------------------
1 | $expr, 'body' => $body];
14 | if (null !== $variables) {
15 | $nodes['variables'] = $variables;
16 | }
17 |
18 | parent::__construct($nodes, ['only' => (bool) $only, 'ignore_missing' => (bool) $ignoreMissing], $lineno, $tag);
19 | }
20 |
21 | public function compile(Compiler $compiler)
22 | {
23 | $compiler->addDebugInfo($this);
24 |
25 | $this->addTemplateArguments($compiler);
26 |
27 | if ($this->getAttribute('ignore_missing')) {
28 |
29 | $template = $compiler->getVarName();
30 |
31 | $compiler
32 | ->write(sprintf("$%s = null;\n", $template))
33 | ->write("try {\n")
34 | ->indent()
35 | ->write(sprintf('$%s = ', $template));
36 |
37 | $this->addGetTemplate($compiler);
38 |
39 | $compiler
40 | ->raw(";\n")
41 | ->outdent()
42 | ->write("} catch (LoaderError \$e) {\n")
43 | ->indent()
44 | ->write("// ignore missing template\n")
45 | ->outdent()
46 | ->write("}\n")
47 | ->write(sprintf("if ($%s) {\n", $template))
48 | ->indent()
49 | ->write(sprintf('$%s->display($context', $template));
50 |
51 | $compiler
52 | ->raw(");\n")
53 | ->outdent()
54 | ->write("}\n");
55 | } else {
56 | $this->addGetTemplate($compiler);
57 | $compiler->raw('->display($context);');
58 | $compiler->raw("\n");
59 | }
60 | }
61 |
62 | protected function addGetTemplate(Compiler $compiler)
63 | {
64 | $compiler
65 | ->write('$this->loadTemplate(')
66 | ->subcompile($this->getNode('expr'))
67 | ->raw('.".twig", ')
68 | ->repr($this->getTemplateName())
69 | ->raw(', ')
70 | ->repr($this->getTemplateLine())
71 | ->raw(')');
72 | }
73 |
74 | protected function addTemplateArguments(Compiler $compiler)
75 | {
76 | if (!$this->hasNode('variables')) {
77 | if (false !== $this->getAttribute('only')) {
78 | $compiler->write('$context = [];');
79 | }
80 | } elseif (false === $this->getAttribute('only')) {
81 | $compiler
82 | ->write('$context = twig_array_merge($context, ')
83 | ->subcompile($this->getNode('variables'))
84 | ->raw(');');
85 | } else {
86 | $compiler->write('$context = twig_to_array(');
87 | $compiler->subcompile($this->getNode('variables'));
88 | $compiler->raw(');');
89 | }
90 | $compiler->raw("\n");
91 |
92 | $compiler
93 | ->write("ob_start();\n")
94 | ->subcompile($this->getNode('body'))
95 | ->write('$context["slot"] = ob_get_clean();')
96 | ->raw("\n");
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/mail/twig/TokenParser/Component.php:
--------------------------------------------------------------------------------
1 | parser->getExpressionParser()->parseExpression();
20 |
21 | $stream = $this->parser->getStream();
22 |
23 | list($variables, $only, $ignoreMissing) = $this->parseArguments($stream);
24 |
25 | $stream->expect(Token::BLOCK_END_TYPE);
26 | $body = $this->parser->subparse([$this, 'decideBlockEnd'], true);
27 | $stream->expect(Token::BLOCK_END_TYPE);
28 |
29 | return new ComponentNode($body, $expr, $variables, $only, $ignoreMissing, $token->getLine(), $this->getTag());
30 | }
31 |
32 | public function decideBlockEnd(Token $token)
33 | {
34 | return $token->test('endcomponent');
35 | }
36 |
37 | protected function parseArguments(TokenStream $stream)
38 | {
39 | $ignoreMissing = false;
40 | if ($stream->nextIf(Token::NAME_TYPE, 'ignore')) {
41 | $stream->expect(Token::NAME_TYPE, 'missing');
42 |
43 | $ignoreMissing = true;
44 | }
45 |
46 | $variables = null;
47 | if ($stream->nextIf(Token::NAME_TYPE, 'with')) {
48 | $variables = $this->parser->getExpressionParser()->parseExpression();
49 | }
50 |
51 | $only = false;
52 | if ($stream->nextIf(Token::NAME_TYPE, 'only')) {
53 | $only = true;
54 | }
55 |
56 | return [$variables, $only, $ignoreMissing];
57 | }
58 |
59 | public function getTag()
60 | {
61 | return 'component';
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/tests/TwigComponentTest.php:
--------------------------------------------------------------------------------
1 | app = m::mock(App::class)->makePartial();
29 |
30 | Container::setInstance($this->app);
31 | $this->app->shouldReceive('make')->with(App::class)->andReturn($this->app);
32 | $this->app->shouldReceive('isDebug')->andReturnTrue();
33 |
34 | $this->view = new View($this->app);
35 |
36 | $this->twig = $this->view->engine('twig');
37 | $parser = new Markdown();
38 | $parser->html5 = true;
39 | $this->twig->getTwig()->addFilter(new TwigFilter('markdown', function ($content) use ($parser) {
40 | $content = preg_replace('/^[^\S\n]+/m', '', $content);
41 | return $parser->parse($content);
42 | }));
43 |
44 | $this->twig->getTwig()->addTokenParser(new Component());
45 |
46 | $this->twig->getLoader()->addPath(__DIR__ . '/../src/mail/resource/view', 'mail');
47 |
48 | $this->twig->getLoader()->addPath(__DIR__ . '/fixtures', 'fixtures');
49 | }
50 |
51 | protected function tearDown(): void
52 | {
53 | m::close();
54 | }
55 |
56 | public function testNormalComponent()
57 | {
58 | echo $this->twig->getTwig()->render('@fixtures/normal.twig',
59 | ['level' => 'aaa',
60 | 'subject' => 'bbbb',
61 | 'greeting' => 'ccc',
62 | 'introLines' => ['aaa'],
63 | 'outroLines' => ['aaa'],
64 | 'actionText' => null,
65 | 'actionUrl' => null,
66 | ]);
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 |