1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\mail;
use Yii;
use yii\base\Component;
use yii\base\InvalidConfigException;
use yii\base\ViewContextInterface;
use yii\web\View;
/**
* BaseMailer serves as a base class that implements the basic functions required by [[MailerInterface]].
*
* Concrete child classes should may focus on implementing the [[sendMessage()]] method.
*
* @see BaseMessage
*
* @property View $view View instance. Note that the type of this property differs in getter and setter. See
* [[getView()]] and [[setView()]] for details.
* @property string $viewPath The directory that contains the view files for composing mail messages Defaults
* to '@app/mail'.
*
* @author Paul Klimov <klimov.paul@gmail.com>
* @since 2.0
*/
abstract class BaseMailer extends Component implements MailerInterface, ViewContextInterface
{
/**
* @event MailEvent an event raised right before send.
* You may set [[MailEvent::isValid]] to be false to cancel the send.
*/
const EVENT_BEFORE_SEND = 'beforeSend';
/**
* @event MailEvent an event raised right after send.
*/
const EVENT_AFTER_SEND = 'afterSend';
/**
* @var string|boolean HTML layout view name. This is the layout used to render HTML mail body.
* The property can take the following values:
*
* - a relative view name: a view file relative to [[viewPath]], e.g., 'layouts/html'.
* - a path alias: an absolute view file path specified as a path alias, e.g., '@app/mail/html'.
* - a boolean false: the layout is disabled.
*/
public $htmlLayout = 'layouts/html';
/**
* @var string|boolean text layout view name. This is the layout used to render TEXT mail body.
* Please refer to [[htmlLayout]] for possible values that this property can take.
*/
public $textLayout = 'layouts/text';
/**
* @var array the configuration that should be applied to any newly created
* email message instance by [[createMessage()]] or [[compose()]]. Any valid property defined
* by [[MessageInterface]] can be configured, such as `from`, `to`, `subject`, `textBody`, `htmlBody`, etc.
*
* For example:
*
* ~~~
* [
* 'charset' => 'UTF-8',
* 'from' => 'noreply@mydomain.com',
* 'bcc' => 'developer@mydomain.com',
* ]
* ~~~
*/
public $messageConfig = [];
/**
* @var string the default class name of the new message instances created by [[createMessage()]]
*/
public $messageClass = 'yii\mail\BaseMessage';
/**
* @var boolean whether to save email messages as files under [[fileTransportPath]] instead of sending them
* to the actual recipients. This is usually used during development for debugging purpose.
* @see fileTransportPath
*/
public $useFileTransport = false;
/**
* @var string the directory where the email messages are saved when [[useFileTransport]] is true.
*/
public $fileTransportPath = '@runtime/mail';
/**
* @var callable a PHP callback that will be called by [[send()]] when [[useFileTransport]] is true.
* The callback should return a file name which will be used to save the email message.
* If not set, the file name will be generated based on the current timestamp.
*
* The signature of the callback is:
*
* ~~~
* function ($mailer, $message)
* ~~~
*/
public $fileTransportCallback;
/**
* @var \yii\base\View|array view instance or its array configuration.
*/
private $_view = [];
/**
* @var string the directory containing view files for composing mail messages.
*/
private $_viewPath;
/**
* @param array|View $view view instance or its array configuration that will be used to
* render message bodies.
* @throws InvalidConfigException on invalid argument.
*/
public function setView($view)
{
if (!is_array($view) && !is_object($view)) {
throw new InvalidConfigException('"' . get_class($this) . '::view" should be either object or configuration array, "' . gettype($view) . '" given.');
}
$this->_view = $view;
}
/**
* @return View view instance.
*/
public function getView()
{
if (!is_object($this->_view)) {
$this->_view = $this->createView($this->_view);
}
return $this->_view;
}
/**
* Creates view instance from given configuration.
* @param array $config view configuration.
* @return View view instance.
*/
protected function createView(array $config)
{
if (!array_key_exists('class', $config)) {
$config['class'] = View::className();
}
return Yii::createObject($config);
}
private $_message;
/**
* Creates a new message instance and optionally composes its body content via view rendering.
*
* @param string|array $view the view to be used for rendering the message body. This can be:
*
* - a string, which represents the view name or path alias for rendering the HTML body of the email.
* In this case, the text body will be generated by applying `strip_tags()` to the HTML body.
* - an array with 'html' and/or 'text' elements. The 'html' element refers to the view name or path alias
* for rendering the HTML body, while 'text' element is for rendering the text body. For example,
* `['html' => 'contact-html', 'text' => 'contact-text']`.
* - null, meaning the message instance will be returned without body content.
*
* The view to be rendered can be specified in one of the following formats:
*
* - path alias (e.g. "@app/mail/contact");
* - a relative view name (e.g. "contact") located under [[viewPath]].
*
* @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file.
* @return MessageInterface message instance.
*/
public function compose($view = null, array $params = [])
{
$message = $this->createMessage();
if ($view === null) {
return $message;
}
if (!array_key_exists('message', $params)) {
$params['message'] = $message;
}
$this->_message = $message;
if (is_array($view)) {
if (isset($view['html'])) {
$html = $this->render($view['html'], $params, $this->htmlLayout);
}
if (isset($view['text'])) {
$text = $this->render($view['text'], $params, $this->textLayout);
}
} else {
$html = $this->render($view, $params, $this->htmlLayout);
}
$this->_message = null;
if (isset($html)) {
$message->setHtmlBody($html);
}
if (isset($text)) {
$message->setTextBody($text);
} elseif (isset($html)) {
if (preg_match('|<body[^>]*>(.*?)</body>|is', $html, $match)) {
$html = $match[1];
}
$html = preg_replace('|<style[^>]*>(.*?)</style>|is', '', $html);
$message->setTextBody(strip_tags($html));
}
return $message;
}
/**
* Creates a new message instance.
* The newly created instance will be initialized with the configuration specified by [[messageConfig]].
* If the configuration does not specify a 'class', the [[messageClass]] will be used as the class
* of the new message instance.
* @return MessageInterface message instance.
*/
protected function createMessage()
{
$config = $this->messageConfig;
if (!array_key_exists('class', $config)) {
$config['class'] = $this->messageClass;
}
$config['mailer'] = $this;
return Yii::createObject($config);
}
/**
* Sends the given email message.
* This method will log a message about the email being sent.
* If [[useFileTransport]] is true, it will save the email as a file under [[fileTransportPath]].
* Otherwise, it will call [[sendMessage()]] to send the email to its recipient(s).
* Child classes should implement [[sendMessage()]] with the actual email sending logic.
* @param MessageInterface $message email message instance to be sent
* @return boolean whether the message has been sent successfully
*/
public function send($message)
{
if (!$this->beforeSend($message)) {
return false;
}
$address = $message->getTo();
if (is_array($address)) {
$address = implode(', ', array_keys($address));
}
Yii::info('Sending email "' . $message->getSubject() . '" to "' . $address . '"', __METHOD__);
if ($this->useFileTransport) {
$isSuccessful = $this->saveMessage($message);
} else {
$isSuccessful = $this->sendMessage($message);
}
$this->afterSend($message, $isSuccessful);
return $isSuccessful;
}
/**
* Sends multiple messages at once.
*
* The default implementation simply calls [[send()]] multiple times.
* Child classes may override this method to implement more efficient way of
* sending multiple messages.
*
* @param array $messages list of email messages, which should be sent.
* @return integer number of messages that are successfully sent.
*/
public function sendMultiple(array $messages)
{
$successCount = 0;
foreach ($messages as $message) {
if ($this->send($message)) {
$successCount++;
}
}
return $successCount;
}
/**
* Renders the specified view with optional parameters and layout.
* The view will be rendered using the [[view]] component.
* @param string $view the view name or the path alias of the view file.
* @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file.
* @param string|boolean $layout layout view name or path alias. If false, no layout will be applied.
* @return string the rendering result.
*/
public function render($view, $params = [], $layout = false)
{
$output = $this->getView()->render($view, $params, $this);
if ($layout !== false) {
return $this->getView()->render($layout, ['content' => $output, 'message' => $this->_message], $this);
} else {
return $output;
}
}
/**
* Sends the specified message.
* This method should be implemented by child classes with the actual email sending logic.
* @param MessageInterface $message the message to be sent
* @return boolean whether the message is sent successfully
*/
abstract protected function sendMessage($message);
/**
* Saves the message as a file under [[fileTransportPath]].
* @param MessageInterface $message
* @return boolean whether the message is saved successfully
*/
protected function saveMessage($message)
{
$path = Yii::getAlias($this->fileTransportPath);
if (!is_dir(($path))) {
mkdir($path, 0777, true);
}
if ($this->fileTransportCallback !== null) {
$file = $path . '/' . call_user_func($this->fileTransportCallback, $this, $message);
} else {
$file = $path . '/' . $this->generateMessageFileName();
}
file_put_contents($file, $message->toString());
return true;
}
/**
* @return string the file name for saving the message when [[useFileTransport]] is true.
*/
public function generateMessageFileName()
{
$time = microtime(true);
return date('Ymd-His-', $time) . sprintf('%04d', (int) (($time - (int) $time) * 10000)) . '-' . sprintf('%04d', mt_rand(0, 10000)) . '.eml';
}
/**
* @return string the directory that contains the view files for composing mail messages
* Defaults to '@app/mail'.
*/
public function getViewPath()
{
if ($this->_viewPath === null) {
$this->setViewPath('@app/mail');
}
return $this->_viewPath;
}
/**
* @param string $path the directory that contains the view files for composing mail messages
* This can be specified as an absolute path or a path alias.
*/
public function setViewPath($path)
{
$this->_viewPath = Yii::getAlias($path);
}
/**
* This method is invoked right before mail send.
* You may override this method to do last-minute preparation for the message.
* If you override this method, please make sure you call the parent implementation first.
* @param MessageInterface $message
* @return boolean whether to continue sending an email.
*/
public function beforeSend($message)
{
$event = new MailEvent(['message' => $message]);
$this->trigger(self::EVENT_BEFORE_SEND, $event);
return $event->isValid;
}
/**
* This method is invoked right after mail was send.
* You may override this method to do some postprocessing or logging based on mail send status.
* If you override this method, please make sure you call the parent implementation first.
* @param MessageInterface $message
* @param boolean $isSuccessful
*/
public function afterSend($message, $isSuccessful)
{
$event = new MailEvent(['message' => $message, 'isSuccessful' => $isSuccessful]);
$this->trigger(self::EVENT_AFTER_SEND, $event);
}
}