BaseMailer::beforeSend()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 6
ccs 4
cts 4
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @link https://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license https://www.yiiframework.com/license/
6
 */
7
8
namespace yii\mail;
9
10
use Yii;
11
use yii\base\Component;
12
use yii\base\InvalidConfigException;
13
use yii\base\ViewContextInterface;
14
use yii\web\View;
15
16
/**
17
 * BaseMailer serves as a base class that implements the basic functions required by [[MailerInterface]].
18
 *
19
 * Concrete child classes should may focus on implementing the [[sendMessage()]] method.
20
 *
21
 * @see BaseMessage
22
 *
23
 * For more details and usage information on BaseMailer, see the [guide article on mailing](guide:tutorial-mailing).
24
 *
25
 * @property View $view View instance. Note that the type of this property differs in getter and setter. See
26
 * [[getView()]] and [[setView()]] for details.
27
 * @property string $viewPath The directory that contains the view files for composing mail messages Defaults
28
 * to '@app/mail'.
29
 *
30
 * @author Paul Klimov <[email protected]>
31
 * @since 2.0
32
 */
33
abstract class BaseMailer extends Component implements MailerInterface, ViewContextInterface
34
{
35
    /**
36
     * @event MailEvent an event raised right before send.
37
     * You may set [[MailEvent::isValid]] to be false to cancel the send.
38
     */
39
    const EVENT_BEFORE_SEND = 'beforeSend';
40
    /**
41
     * @event MailEvent an event raised right after send.
42
     */
43
    const EVENT_AFTER_SEND = 'afterSend';
44
45
    /**
46
     * @var string|bool HTML layout view name. This is the layout used to render HTML mail body.
47
     * The property can take the following values:
48
     *
49
     * - a relative view name: a view file relative to [[viewPath]], e.g., 'layouts/html'.
50
     * - a [path alias](guide:concept-aliases): an absolute view file path specified as a path alias, e.g., '@app/mail/html'.
51
     * - a boolean false: the layout is disabled.
52
     */
53
    public $htmlLayout = 'layouts/html';
54
    /**
55
     * @var string|bool text layout view name. This is the layout used to render TEXT mail body.
56
     * Please refer to [[htmlLayout]] for possible values that this property can take.
57
     */
58
    public $textLayout = 'layouts/text';
59
    /**
60
     * @var array the configuration that should be applied to any newly created
61
     * email message instance by [[createMessage()]] or [[compose()]]. Any valid property defined
62
     * by [[MessageInterface]] can be configured, such as `from`, `to`, `subject`, `textBody`, `htmlBody`, etc.
63
     *
64
     * For example:
65
     *
66
     * ```php
67
     * [
68
     *     'charset' => 'UTF-8',
69
     *     'from' => '[email protected]',
70
     *     'bcc' => '[email protected]',
71
     * ]
72
     * ```
73
     */
74
    public $messageConfig = [];
75
    /**
76
     * @var string the default class name of the new message instances created by [[createMessage()]]
77
     */
78
    public $messageClass = 'yii\mail\BaseMessage';
79
    /**
80
     * @var bool whether to save email messages as files under [[fileTransportPath]] instead of sending them
81
     * to the actual recipients. This is usually used during development for debugging purpose.
82
     * @see fileTransportPath
83
     */
84
    public $useFileTransport = false;
85
    /**
86
     * @var string the directory where the email messages are saved when [[useFileTransport]] is true.
87
     */
88
    public $fileTransportPath = '@runtime/mail';
89
    /**
90
     * @var callable|null a PHP callback that will be called by [[send()]] when [[useFileTransport]] is true.
91
     * The callback should return a file name which will be used to save the email message.
92
     * If not set, the file name will be generated based on the current timestamp.
93
     *
94
     * The signature of the callback is:
95
     *
96
     * ```php
97
     * function ($mailer, $message)
98
     * ```
99
     */
100
    public $fileTransportCallback;
101
102
    /**
103
     * @var \yii\base\View|array view instance or its array configuration.
104
     */
105
    private $_view = [];
106
    /**
107
     * @var string the directory containing view files for composing mail messages.
108
     */
109
    private $_viewPath;
110
111
112
    /**
113
     * @param array|View $view view instance or its array configuration that will be used to
114
     * render message bodies.
115
     * @throws InvalidConfigException on invalid argument.
116
     */
117 1
    public function setView($view)
118
    {
119 1
        if (!is_array($view) && !is_object($view)) {
120
            throw new InvalidConfigException('"' . get_class($this) . '::view" should be either object or configuration array, "' . gettype($view) . '" given.');
121
        }
122 1
        $this->_view = $view;
123
    }
124
125
    /**
126
     * @return View view instance.
127
     */
128 7
    public function getView()
129
    {
130 7
        if (!is_object($this->_view)) {
131 7
            $this->_view = $this->createView($this->_view);
132
        }
133
134 7
        return $this->_view;
135
    }
136
137
    /**
138
     * Creates view instance from given configuration.
139
     * @param array $config view configuration.
140
     * @return View view instance.
141
     */
142 7
    protected function createView(array $config)
143
    {
144 7
        if (!array_key_exists('class', $config)) {
145 7
            $config['class'] = View::className();
0 ignored issues
show
Deprecated Code introduced by
The function yii\base\BaseObject::className() has been deprecated: since 2.0.14. On PHP >=5.5, use `::class` instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

145
            $config['class'] = /** @scrutinizer ignore-deprecated */ View::className();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
146
        }
147
148 7
        return Yii::createObject($config);
149
    }
150
151
    private $_message;
152
153
    /**
154
     * Creates a new message instance and optionally composes its body content via view rendering.
155
     *
156
     * @param string|array|null $view the view to be used for rendering the message body. This can be:
157
     *
158
     * - a string, which represents the view name or [path alias](guide:concept-aliases) for rendering the HTML body of the email.
159
     *   In this case, the text body will be generated by applying `strip_tags()` to the HTML body.
160
     * - an array with 'html' and/or 'text' elements. The 'html' element refers to the view name or path alias
161
     *   for rendering the HTML body, while 'text' element is for rendering the text body. For example,
162
     *   `['html' => 'contact-html', 'text' => 'contact-text']`.
163
     * - null, meaning the message instance will be returned without body content.
164
     *
165
     * The view to be rendered can be specified in one of the following formats:
166
     *
167
     * - path alias (e.g. "@app/mail/contact");
168
     * - a relative view name (e.g. "contact") located under [[viewPath]].
169
     *
170
     * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file.
171
     * @return MessageInterface message instance.
172
     */
173 8
    public function compose($view = null, array $params = [])
174
    {
175 8
        $message = $this->createMessage();
176 8
        if ($view === null) {
177 5
            return $message;
178
        }
179
180 3
        if (!array_key_exists('message', $params)) {
181 3
            $params['message'] = $message;
182
        }
183
184 3
        $this->_message = $message;
185
186 3
        if (is_array($view)) {
187 3
            if (isset($view['html'])) {
188 3
                $html = $this->render($view['html'], $params, $this->htmlLayout);
189
            }
190 3
            if (isset($view['text'])) {
191 3
                $text = $this->render($view['text'], $params, $this->textLayout);
192
            }
193
        } else {
194 1
            $html = $this->render($view, $params, $this->htmlLayout);
195
        }
196
197
198 3
        $this->_message = null;
199
200 3
        if (isset($html)) {
201 3
            $message->setHtmlBody($html);
202
        }
203 3
        if (isset($text)) {
204 1
            $message->setTextBody($text);
205 3
        } elseif (isset($html)) {
206 3
            if (preg_match('~<body[^>]*>(.*?)</body>~is', $html, $match)) {
207 1
                $html = $match[1];
208
            }
209
            // remove style and script
210 3
            $html = preg_replace('~<((style|script))[^>]*>(.*?)</\1>~is', '', $html);
211
            // strip all HTML tags and decoded HTML entities
212 3
            $text = html_entity_decode(strip_tags($html), ENT_QUOTES | ENT_HTML5, Yii::$app ? Yii::$app->charset : 'UTF-8');
213
            // improve whitespace
214 3
            $text = preg_replace("~^[ \t]+~m", '', trim($text));
215 3
            $text = preg_replace('~\R\R+~mu', "\n\n", $text);
216 3
            $message->setTextBody($text);
217
        }
218
219 3
        return $message;
220
    }
221
222
    /**
223
     * Creates a new message instance.
224
     * The newly created instance will be initialized with the configuration specified by [[messageConfig]].
225
     * If the configuration does not specify a 'class', the [[messageClass]] will be used as the class
226
     * of the new message instance.
227
     * @return MessageInterface message instance.
228
     */
229 8
    protected function createMessage()
230
    {
231 8
        $config = $this->messageConfig;
232 8
        if (!array_key_exists('class', $config)) {
233 8
            $config['class'] = $this->messageClass;
234
        }
235 8
        $config['mailer'] = $this;
236 8
        return Yii::createObject($config);
237
    }
238
239
    /**
240
     * Sends the given email message.
241
     * This method will log a message about the email being sent.
242
     * If [[useFileTransport]] is true, it will save the email as a file under [[fileTransportPath]].
243
     * Otherwise, it will call [[sendMessage()]] to send the email to its recipient(s).
244
     * Child classes should implement [[sendMessage()]] with the actual email sending logic.
245
     * @param MessageInterface $message email message instance to be sent
246
     * @return bool whether the message has been sent successfully
247
     */
248 3
    public function send($message)
249
    {
250 3
        if (!$this->beforeSend($message)) {
251
            return false;
252
        }
253
254 3
        $address = $message->getTo();
255 3
        if (is_array($address)) {
256
            $address = implode(', ', array_keys($address));
257
        }
258 3
        Yii::info('Sending email "' . $message->getSubject() . '" to "' . $address . '"', __METHOD__);
259
260 3
        if ($this->useFileTransport) {
261 1
            $isSuccessful = $this->saveMessage($message);
262
        } else {
263 2
            $isSuccessful = $this->sendMessage($message);
264
        }
265 3
        $this->afterSend($message, $isSuccessful);
266
267 3
        return $isSuccessful;
268
    }
269
270
    /**
271
     * Sends multiple messages at once.
272
     *
273
     * The default implementation simply calls [[send()]] multiple times.
274
     * Child classes may override this method to implement more efficient way of
275
     * sending multiple messages.
276
     *
277
     * @param array $messages list of email messages, which should be sent.
278
     * @return int number of messages that are successfully sent.
279
     */
280
    public function sendMultiple(array $messages)
281
    {
282
        $successCount = 0;
283
        foreach ($messages as $message) {
284
            if ($this->send($message)) {
285
                $successCount++;
286
            }
287
        }
288
289
        return $successCount;
290
    }
291
292
    /**
293
     * Renders the specified view with optional parameters and layout.
294
     * The view will be rendered using the [[view]] component.
295
     * @param string $view the view name or the [path alias](guide:concept-aliases) of the view file.
296
     * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file.
297
     * @param string|bool $layout layout view name or [path alias](guide:concept-aliases). If false, no layout will be applied.
298
     * @return string the rendering result.
299
     */
300 5
    public function render($view, $params = [], $layout = false)
301
    {
302 5
        $output = $this->getView()->render($view, $params, $this);
303 5
        if ($layout !== false) {
304 1
            return $this->getView()->render($layout, ['content' => $output, 'message' => $this->_message], $this);
0 ignored issues
show
Bug introduced by
It seems like $layout can also be of type true; however, parameter $view of yii\base\View::render() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

304
            return $this->getView()->render(/** @scrutinizer ignore-type */ $layout, ['content' => $output, 'message' => $this->_message], $this);
Loading history...
305
        }
306
307 4
        return $output;
308
    }
309
310
    /**
311
     * Sends the specified message.
312
     * This method should be implemented by child classes with the actual email sending logic.
313
     * @param MessageInterface $message the message to be sent
314
     * @return bool whether the message is sent successfully
315
     */
316
    abstract protected function sendMessage($message);
317
318
    /**
319
     * Saves the message as a file under [[fileTransportPath]].
320
     * @param MessageInterface $message
321
     * @return bool whether the message is saved successfully
322
     */
323 1
    protected function saveMessage($message)
324
    {
325 1
        $path = Yii::getAlias($this->fileTransportPath);
326 1
        if (!is_dir($path)) {
0 ignored issues
show
Bug introduced by
It seems like $path can also be of type false; however, parameter $filename of is_dir() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

326
        if (!is_dir(/** @scrutinizer ignore-type */ $path)) {
Loading history...
327 1
            mkdir($path, 0777, true);
0 ignored issues
show
Bug introduced by
It seems like $path can also be of type false; however, parameter $directory of mkdir() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

327
            mkdir(/** @scrutinizer ignore-type */ $path, 0777, true);
Loading history...
328
        }
329 1
        if ($this->fileTransportCallback !== null) {
330 1
            $file = $path . '/' . call_user_func($this->fileTransportCallback, $this, $message);
0 ignored issues
show
Bug introduced by
Are you sure $path of type false|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

330
            $file = /** @scrutinizer ignore-type */ $path . '/' . call_user_func($this->fileTransportCallback, $this, $message);
Loading history...
331
        } else {
332
            $file = $path . '/' . $this->generateMessageFileName();
333
        }
334 1
        file_put_contents($file, $message->toString());
335
336 1
        return true;
337
    }
338
339
    /**
340
     * @return string the file name for saving the message when [[useFileTransport]] is true.
341
     */
342
    public function generateMessageFileName()
343
    {
344
        $time = microtime(true);
345
        $timeInt = (int) $time;
346
347
        return date('Ymd-His-', $timeInt) . sprintf('%04d', (int) (($time - $timeInt) * 10000)) . '-' . sprintf('%04d', random_int(0, 10000)) . '.eml';
348
    }
349
350
    /**
351
     * @return string the directory that contains the view files for composing mail messages
352
     * Defaults to '@app/mail'.
353
     */
354 5
    public function getViewPath()
355
    {
356 5
        if ($this->_viewPath === null) {
357
            $this->setViewPath('@app/mail');
358
        }
359
360 5
        return $this->_viewPath;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_viewPath could also return false which is incompatible with the documented return type string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
361
    }
362
363
    /**
364
     * @param string $path the directory that contains the view files for composing mail messages
365
     * This can be specified as an absolute path or a [path alias](guide:concept-aliases).
366
     */
367 11
    public function setViewPath($path)
368
    {
369 11
        $this->_viewPath = Yii::getAlias($path);
0 ignored issues
show
Documentation Bug introduced by
It seems like Yii::getAlias($path) can also be of type false. However, the property $_viewPath is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
370
    }
371
372
    /**
373
     * This method is invoked right before mail send.
374
     * You may override this method to do last-minute preparation for the message.
375
     * If you override this method, please make sure you call the parent implementation first.
376
     * @param MessageInterface $message
377
     * @return bool whether to continue sending an email.
378
     */
379 2
    public function beforeSend($message)
380
    {
381 2
        $event = new MailEvent(['message' => $message]);
382 2
        $this->trigger(self::EVENT_BEFORE_SEND, $event);
383
384 2
        return $event->isValid;
385
    }
386
387
    /**
388
     * This method is invoked right after mail was send.
389
     * You may override this method to do some postprocessing or logging based on mail send status.
390
     * If you override this method, please make sure you call the parent implementation first.
391
     * @param MessageInterface $message
392
     * @param bool $isSuccessful
393
     */
394 2
    public function afterSend($message, $isSuccessful)
395
    {
396 2
        $event = new MailEvent(['message' => $message, 'isSuccessful' => $isSuccessful]);
397 2
        $this->trigger(self::EVENT_AFTER_SEND, $event);
398
    }
399
}
400