четверг, 23 октября 2008 г.

Email рассылка средствами Swift

Email рассылка достаточно часто возникающая, перед разработчиком, задача. В простейшем случае это рассылка новостей сайта. На сайте symfony в The symfony Cookbook для отправки электронной почты предлагается использовать Swift Mailer. И действительно Swift Mailer - это мощная библиотека решающая практически все задачи связанные с отправкой электронных писем. Именно с ее помощью мы и создадим нашу email рассылку.

Установка Swift Mailer

Для начала надо установить Swift Mailer. Если проект находится под контролем Subversion, тогда добавим свойство svn:externals директории lib/vendor, если такой директории еще нет, то можно ее создать.

    $ mkdir -p lib/vendor
    $ svn propedit svn:externals lib/vendor
    
добавим следующую строку

    swift http://swiftmailer.svn.sourceforge.net/svnroot/swiftmailer/tags/php5/3.3.3/lib/
    
и обновим

    $ svn update
    
Если проект не использует Subversion, то просто разместим Swift Mailer в директории lib/vendor, для этого можно выполнить следующие команды:

    $ mkdir -p lib/vendor
    $ svn co http://swiftmailer.svn.sourceforge.net/svnroot/swiftmailer/tags/php5/3.3.3/lib/ lib/vendor/swift
    
На этом установка завершена.

Создание моделей

Теперь определим модели данных, которые нам потребуются. В простейшем случае - это подписчик (Subscriber) и сообщение (Message). Модели могут быть примерно следующими:

    --- config/doctrine/subscribe.yml
    Subscriber:
      columns:
        first_name: string(100)
        middle_name: string(100)
        last_name: string(100)
        email: string(200)
        email_key: string(40)

    Message:
      columns:
        subject: string(255)
        body: string
        sended: timestamp
    
Несколько комментариев. В модели Subscriber пояснить пожалуй стоит только поле email_key. Это хеш, сгенерированный для каждого подписчика и используемый для возможности отписаться от рассыкли. В моделе Message поле sended содержит отметку времени об отправке письма, если это поле null значит письмо еще не было отправлено и его следует разослать подписчикам. Предназначение остальных полей кажется вполне понятным исходя из их названий. Вопрос о создании интерфейса управления сообщениями, подписчиками и т.д. оставим за кадром.

Команда рассылки писем

Для рассылки писем создадим команду (task), для этого воспользуемся командой:

    $ ./symfony generate:task mailer:send 
    
Это создаст команду mailer:send определенную в файле lib/task/mailerSendTask.class.php. Метод configure этой команды может выглядеть примерно так:

    protected function configure()
    {
        $this->namespace        = 'mailer';
        $this->name             = 'send';
        $this->briefDescription = 'Рассылает письма';
        $this->detailedDescription = <<<EOF
[mailer:send|INFO] Отправляет письмо подписчикам.
Используется так:

  [./symfony mailer:send appliaction --env=environment|INFO]

Простой пример:

  [./symfony mailer:send frontend --env=prod|INFO]
EOF;
        $this->addArgument('application', sfCommandArgument::REQUIRED, 'Приложение');
        $this->addOption('env', null, sfCommandOption::PARAMETER_REQUIRED, 'Окружение', 'dev');
    }
    
Добавление аргумента application в данном случае обязательно, так как мы будем использовать базу данных. В дальнейшем команду надо будет запускать примерно так:

    $ ./symfony mailer:send frontend
    

Подготовка к рассылке email

Для начала необходимо получить письмо, которое следует разослать подписчикам. Для этого определим метод getWaitingMessage() в классе MessageTable, он может выглядеть примерно так:

    # lib/model/doctrine/MessageTable.class.php
    public function getWaitingMessage()
    {
        return $this->createQuery('m')
                ->where('m.sended IS NULL')
                ->limit(1)
                ->execute()
                ->getFirst();
    }
    
Также необходимо получить подписчиков, но для этого нам пока достаточно простого:

    Doctrine::getTable('Subscriber')->findAll();
    

Подготовка сообщения для email рассылки

Прежде, чем отправлять письмо, его необходимо подготовить. Например, если мы создаем сообщение через wysiwyg редактор, то нам как минимум необходимо вложить все изображения и изменить на них ссылки, а так же надо проставить email_key в ссылку по которой можно отписаться от рассылки. Для этого определим метод getMessage() в классе нашей команды mailerSendTask, в простейшем случае он может выглядеть примерно так:

    protected $message;
    public function getMessage()
    {
        // получаем сообщение для отправки
        $this->message = Doctrine::getTable('Message')->getWaitingMessage();
        if (!$message)
        {
            throw new sfException('Не найдено сообщений для отправки');
        }

        // создаем email сообщение используя Swift
        $mimeMessage = new Swift_Message($this->message['subject']);
        $mimeMessage->setCharset('UTF-8');

        // находим все изображения в сообщении.
        $images = array();
        if (preg_match_all('/<img[^>]*?src=(?:\'|")(.*?)(?:\'|")/umsi', $this->message['body'], $matches))
        {
            foreach($matches[1] as $match)
            {
                $images[$match] = new Swift_Message_Image
                (
                    new Swift_File($webDir . DIRECTORY_SEPARATOR . $match)
                );
            }
        }

        // приаттачиваем найденные изображения и меняем ссылки в сообщении
        if (!empty($images))
        {
            foreach($images as $src => $image)
            {
                $attach = $mimeMessage->attach($image);
                $this->message['body'] = str_replace($src, $attach, $this->message['body']);
            }
        }

        // наконец прикрепляем собственно сообщение.
        $mimeMessage->attach
        (
            new Swift_Message_Part
            (
                $this->message['body'], 
                'text/html', 
                'base64'
            )
        );

        return $mimeMessage;

    }
    
Если с заменой изображений все понятно, то вот со ссылкой позволяющей отписаться мы пока подождем, так как она должна быть своей для каждого подписчика.

Составление списка получателей

Прежде чем отправлять письмо необходимо определить список получателей. Для этого воспользуемся классом Swift_RecipientList и определим метод getRecipients в классе нашей команды, код может выглядеть примерно так:

    // lib/task/mailerSendTask.class.php
    protected $subscribers;
    // ...
    public function getRecipients()
    {
        $this->subscribers = Doctrine::getTable('Subscriber')->findAll();

        $recipients = new Swift_RecipientList;
        $encoder = new Swift_Message_Encoder;

        foreach($subscribers as $subscriber)
        {
            $recipients->addTo
            (
                $subscriber['email'],
                sprintf
                (
                    '=?utf-8?B?%s=',
                    $encoder->base64Encode
                    (
                        sprintf("%s %s", $subsciber['first_name'], $subscriber['last_name'])
                    )
                )
            );
        }

        return $recipients;
    }
    
Здесь используется Swift_Message_Encoder для кодирования получателей. Это необходимо, так как если в имени получателей содержатся например кириллические символы, то могут быть проблемы. Во избежание этих проблем и для адекватного отображения почтовыми клиентами мы и используем кодирование имен.

Отправка email сообщения

Для отправки нам так же необходимы некоторые настройки, которые лучше вынести в файл настроек приложения app.yml Выглядеть он может примерно так:

    mail:
      subscribe:
        host: localhost
        port: 25
        from: robot@example.com
    
Сам код отправки email сообщения может выглядеть примерно так:

    public function execute($arguments=array(), $options=array())
    {
        $dbManager = new sfDatabaseManager($this->configuration);

        // получаем настройки
        $config = sfConfig::get('app_mail_subscribe');

        // создаем экземпляр мейлера
        $mailer = new Swift(new Swift_Connection_SMTP($config['host'], $config['port']));

        // получаем список получателей
        $recipients = $this->getRecipients();

        try
        {
            // создаем объект пакетной рассылки и отправляем письмо
            // по списку получателей
            $batch = Swift_BatchMailer($mailer);
            $batch->send($this->getMessage(), $recipients, $config['from']);

            // после отправки письма, ставим отметку у сообщения
            $message = Doctrine::getTable('Message')->find($this->message['id']);
            $message->sended = date('Y-m-d H:i');
            $message->save();
        }
        catch (Exception $e)
        {
            new throw sfException($e->getMessage());
        }
    }
    
Мы используем Swift_BatchMailer для отправки email сообщения по списку получателей.

Персонализация сообщения в email рассылке

У нас уже отправляются письма по списку подписчиков. Но мы еще не решили одной важной проблемы - вставка ссылки для возможности отписаться от рассылки. Конечно можно было бы пойти простым путем и сделать ссылку для всех одинаковой, а уже на этой странице запрашивать email по которому отправлять письмо с нужной ссылкой. Но это слишком не удобно. Более того, хотелось бы хоть как-то персонализировать сообщения и начинать их например с приветствия по имени каждого подписчика. Для этого мы используем плагин для Swift - Swift_Plugin_Decorator. Который позволяет производить замены в тексте сообщения для каждого получателя. Нам для этого необходимо создать массив замен по следующему принципу:

    $replacements = array
    (
        'email@address.com' => array('{key}' => 'value', '{key2}' => 'value 2'),
        // ...
    );
    
Все ключи (они не обязательно должны быть обрамлены {}, можно использовать что угодно, например %%key%%) заменяются на значения. Для использования плагина необходимо для начала создать массив замен. Для простоты будем создавать массив замен в момент получения списка получателей. Для этого добавим свойство классу команды mailerSendTask - $replacements и изменим код получения списка получателей.

    // lib/task/mailerSendTask.class.php
    protected $subscribers;
    protected $replacements = array();
    // ...
    public function getRecipients()
    {
        $this->subscribers = Doctrine::getTable('Subscriber')->findAll();

        $recipients = new Swift_RecipientList;
        $encoder = new Swift_Message_Encoder;

        foreach($subscribers as $subscriber)
        {
            $recipients->addTo
            (
                $subscriber['email'],
                sprintf
                (
                    '=?utf-8?B?%s=',
                    $encoder->base64Encode
                    (
                        sprintf("%s %s", $subsciber['first_name'], $subscriber['last_name'])
                    )
                )
            );

            $this->replacements[$subscriber['email']] = array
            (
                '{first_name}' => $subscriber['first_name'],
                '{middle_name}' => $subscriber['middle_name'],
                '{last_name}' => $subscriber['last_name'],
                '{email_key}' => $subscriber['email_key']
            );
        }

        return $recipients;
    }
    
Теперь немного модифицируем код отправки сообщения, указав, что мы хотим использовать плагин:

    public function execute($arguments=array(), $options=array())
    {
        $dbManager = new sfDatabaseManager($this->configuration);

        // получаем настройки
        $config = sfConfig::get('app_mail_subscribe');

        // создаем экземпляр мейлера
        $mailer = new Swift(new Swift_Connection_SMTP($config['host'], $config['port']));

        // получаем список получателей
        $recipients = $this->getRecipients();

        // подключаем плагин 
        $mailer->attachPlugin(new Swift_Plugin_Decorator($this->replacements), 'decorator');

        try
        {
            // создаем объект пакетной рассылки и отправляем письмо
            // по списку получателей
            $batch = Swift_BatchMailer($mailer);
            $batch->send($this->getMessage(), $recipients, $config['from']);

            // после отправки письма, ставим отметку у сообщения
            $message = Doctrine::getTable('Message')->find($this->message['id']);
            $message->sended = date('Y-m-d H:i');
            $message->save();

        }
        catch (Exception $e)
        {
            new throw sfException($e->getMessage());
        }
    }
    
Теперь в теле сообщения все вставки вида {first_name}, {middle_name}, {last_name}, {email_key} будут заменяться на соответствующие значения. Т.е. письмо может выглядеть примерно так:

    Здравствуйте, {first_name} {last_name}!
    ...

    ---
    Если вы хотите отписаться от данной рассылки, перейдите по <a href="http://example.com/unsubscribe/{email_key}">этой ссылке</a>
    
На этом все. За кадром остались многие вопросы создания хорошей email рассылки, например управление подписчиками, собщениями, составление писем, создание нескольких рассылок, запланированная рассылка и т.д. Но главной целью данного поста было показать как персонализировать письма при отправке по списку получателей.

2 комментария:

Vit228 комментирует...

Очепятка и ашипка!

"Если вы хотите отписаться от данной рассылки перейдти по"

надо

Если вы хотите отписаться от данной рассылки, перейдите по...

В остальном: материал хороший, СВИФТ отличная библиотека :)

Sergey комментирует...

Спасибо.

Поправил. :)