BaseMailer.php 13.9 KB
Newer Older
1 2 3 4 5 6 7
<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

Paul Klimov committed
8
namespace yii\mail;
9

10
use Yii;
11 12
use yii\base\Component;
use yii\base\InvalidConfigException;
13
use yii\base\ViewContextInterface;
Qiang Xue committed
14
use yii\web\View;
15 16

/**
Qiang Xue committed
17 18
 * BaseMailer serves as a base class that implements the basic functions required by [[MailerInterface]].
 *
19
 * Concrete child classes should may focus on implementing the [[sendMessage()]] method.
20 21 22
 *
 * @see BaseMessage
 *
23 24
 * @property View $view View instance. Note that the type of this property differs in getter and setter. See
 * [[getView()]] and [[setView()]] for details.
Carsten Brandt committed
25
 * @property string $viewPath The directory that contains the view files for composing mail messages Defaults
26
 * to '@app/mail'.
27 28 29 30
 *
 * @author Paul Klimov <klimov.paul@gmail.com>
 * @since 2.0
 */
31
abstract class BaseMailer extends Component implements MailerInterface, ViewContextInterface
32
{
33
    /**
34 35
     * @event MailEvent an event raised right before send.
     * You may set [[MailEvent::isValid]] to be false to cancel the send.
36 37 38
     */
    const EVENT_BEFORE_SEND = 'beforeSend';
    /**
39
     * @event MailEvent an event raised right after send.
40 41
     */
    const EVENT_AFTER_SEND = 'afterSend';
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
    /**
     * @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;
99

100 101 102 103
    /**
     * @var \yii\base\View|array view instance or its array configuration.
     */
    private $_view = [];
104 105 106 107
    /**
     * @var string the directory containing view files for composing mail messages.
     */
    private $_viewPath;
108

109

110
    /**
111 112
     * @param array|View $view view instance or its array configuration that will be used to
     * render message bodies.
113 114 115 116 117 118 119 120 121
     * @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;
    }
122

123 124 125 126 127 128 129 130
    /**
     * @return View view instance.
     */
    public function getView()
    {
        if (!is_object($this->_view)) {
            $this->_view = $this->createView($this->_view);
        }
131

132 133
        return $this->_view;
    }
134

135 136
    /**
     * Creates view instance from given configuration.
137 138
     * @param array $config view configuration.
     * @return View view instance.
139 140 141 142 143 144
     */
    protected function createView(array $config)
    {
        if (!array_key_exists('class', $config)) {
            $config['class'] = View::className();
        }
145

146 147
        return Yii::createObject($config);
    }
148

Qiang Xue committed
149 150
    private $_message;

151 152 153 154 155 156 157 158 159 160 161 162 163 164 165
    /**
     * 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");
166
     * - a relative view name (e.g. "contact") located under [[viewPath]].
167
     *
168
     * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file.
169 170 171 172 173
     * @return MessageInterface message instance.
     */
    public function compose($view = null, array $params = [])
    {
        $message = $this->createMessage();
Qiang Xue committed
174 175 176 177 178
        if ($view === null) {
            return $message;
        }

        if (!array_key_exists('message', $params)) {
179
            $params['message'] = $message;
Qiang Xue committed
180 181 182 183 184 185 186
        }

        $this->_message = $message;

        if (is_array($view)) {
            if (isset($view['html'])) {
                $html = $this->render($view['html'], $params, $this->htmlLayout);
187
            }
Qiang Xue committed
188 189
            if (isset($view['text'])) {
                $text = $this->render($view['text'], $params, $this->textLayout);
190
            }
Qiang Xue committed
191 192 193 194 195 196 197 198 199 200 201 202 203
        } 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)) {
204
            if (preg_match('~<body[^>]*>(.*?)</body>~is', $html, $match)) {
Qiang Xue committed
205
                $html = $match[1];
206
            }
207 208 209 210 211 212 213 214
            // remove style and script
            $html = preg_replace('~<((style|script))[^>]*>(.*?)</\1>~is', '', $html);
            // strip all HTML tags and decoded HTML entities
            $text = html_entity_decode(strip_tags($html), ENT_QUOTES | ENT_HTML5, Yii::$app ? Yii::$app->charset : 'UTF-8');
            // improve whitespace
            $text = preg_replace("~^[ \t]+~m", '', trim($text));
            $text = preg_replace('~\R\R+~mu', "\n\n", $text);
            $message->setTextBody($text);
215 216 217
        }
        return $message;
    }
218

219 220 221 222 223 224 225 226 227 228 229 230 231
    /**
     * 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;
        }
232
        $config['mailer'] = $this;
233 234
        return Yii::createObject($config);
    }
235

236 237 238 239 240 241
    /**
     * 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.
242 243
     * @param MessageInterface $message email message instance to be sent
     * @return boolean whether the message has been sent successfully
244 245 246 247 248 249
     */
    public function send($message)
    {
        if (!$this->beforeSend($message)) {
            return false;
        }
250

251 252 253 254 255
        $address = $message->getTo();
        if (is_array($address)) {
            $address = implode(', ', array_keys($address));
        }
        Yii::info('Sending email "' . $message->getSubject() . '" to "' . $address . '"', __METHOD__);
256

257 258 259 260 261 262
        if ($this->useFileTransport) {
            $isSuccessful = $this->saveMessage($message);
        } else {
            $isSuccessful = $this->sendMessage($message);
        }
        $this->afterSend($message, $isSuccessful);
263

264 265
        return $isSuccessful;
    }
266

267 268 269 270 271 272 273
    /**
     * 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.
     *
274
     * @param array $messages list of email messages, which should be sent.
275 276 277 278 279 280 281 282 283 284
     * @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++;
            }
        }
285

286 287
        return $successCount;
    }
288

289 290 291
    /**
     * Renders the specified view with optional parameters and layout.
     * The view will be rendered using the [[view]] component.
292 293 294 295
     * @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.
296 297 298 299 300
     */
    public function render($view, $params = [], $layout = false)
    {
        $output = $this->getView()->render($view, $params, $this);
        if ($layout !== false) {
Qiang Xue committed
301
            return $this->getView()->render($layout, ['content' => $output, 'message' => $this->_message], $this);
302 303 304 305 306 307 308 309
        } else {
            return $output;
        }
    }

    /**
     * Sends the specified message.
     * This method should be implemented by child classes with the actual email sending logic.
310 311
     * @param MessageInterface $message the message to be sent
     * @return boolean whether the message is sent successfully
312 313 314 315 316
     */
    abstract protected function sendMessage($message);

    /**
     * Saves the message as a file under [[fileTransportPath]].
317 318
     * @param MessageInterface $message
     * @return boolean whether the message is saved successfully
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
     */
    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';
    }

    /**
347 348 349 350 351 352 353 354 355 356 357 358 359 360
     * @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.
361
     */
362
    public function setViewPath($path)
363
    {
364
        $this->_viewPath = Yii::getAlias($path);
365 366 367 368 369 370
    }

    /**
     * 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.
371 372
     * @param MessageInterface $message
     * @return boolean whether to continue sending an email.
373 374 375 376 377 378 379 380 381 382 383 384 385 386
     */
    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
387
     * @param boolean $isSuccessful
388 389 390 391 392 393
     */
    public function afterSend($message, $isSuccessful)
    {
        $event = new MailEvent(['message' => $message, 'isSuccessful' => $isSuccessful]);
        $this->trigger(self::EVENT_AFTER_SEND, $event);
    }
394
}