RabbitMQ: Отправка почты

Рассмотрим возможность отправки почты через RabbitMQ средствами PHP. Зачем это нужно? Предположим нам надо отправить несколько тысяч писем с сайта. Когда нажмете «отправить», то сайт зависнет на некоторое время пока не будут разосланы все письма. Идея состоит в том, чтобы вместо отправки писем закодировать данные в JSON и передать на очередь в RabbitMQ. Далее будет сделан отдельный скрипт, который будет получать данные для отправки почты от RabbitMQ (такие скрипты можно запускать через supervisord или systemd). Получается сайт продолжит работать в обычном режиме, а отправка почты будет осуществляться в фоновом режиме.

Сам по себе RabbitMQ ничем не занимается, кроме передачи сообщений между различными приложениями. Таким образом можно связать несколько абсолютно не совместимых программ 🙂

Чтобы создать пользователя выполняем следующие команды:

$ rabbitmqctl add_user <username> <password>
$ rabbitmqctl set_user_tags <username> administrator
$ rabbitmqctl set_permissions -p / <username> ".*" ".*" ".*" 

Пользователь по-умолчанию «guest».

Чтобы получить доступ к веб-интерфейсу на порту 5672 надо включить плагин управления:

$ rabbitmq-plugins enable rabbitmq_management
$ systemctl restart rabbitmq-server

Для начала накидаем форму для отправки почты index.php:

<?php
if(!empty($_GET['sent'])){
?>
<div>
        Message sent!
</div>
<?php
}
?>

<form action="rabbit.php" method="post">
<div>
        <label for="from">From</label>
        <input type="text" name="from" id="from">
</div>
<div>
        <label for="from_email">From email</label>
        <input type="text" name="from_email" id="from_email">
</div>
<div>
        <label for="to_email">To email</label>
        <input type="text" name="to_email" id="to_email">
</div>
<div>
        <label for="subject">Subject</label>
        <input type="text" name="subject" id="subject">
</div>
<div>
        <label for="message">Message</label>
        <textarea name="message" id="message" cols="30" rows="10"></textarea>
</div>
<div>
        <button type="submit">Send</button>
</div>
</form>

Далее установим пакет php-amqplib для связи с RabbitMQ и swiftmailer для отправки почты на SMTP сервер:

$ composer require php-amqplib/php-amqplib
$ composer require swiftmailer/swiftmailer

Теперь напишем скрипт rabbit.php, на которую форма будет делать редирект при нажатии на кнопку:

<?php
require_once __DIR__.'/vendor/autoload.php';

use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;

/* create connection to rabbit */
$conn = new AMQPStreamConnection(
        'localhost',          //server address
        5672,                 //server port
        'username',           //username
        'password',           //password
        '/'                   //vhost
);
/* create new channel */
$chan = $conn->channel();
/* create new queue */
$chan->queue_declare(
        'outgoing-email',       //queue name
        false,                  //check existing exchange
        false,                  //check queue on server crash
        false,                  //check if queue used only by one connection
        false                   //delete queue if last subscriber unsibscribed
);
/* get data from html form */
$data = json_encode($_POST);
/* prepare amqp message */
$msg = new AMQPMessage($data,array('delivery_mode' => 2)); //persistent message mode
/* send message to rabbit */
$chan->basic_publish(
        $msg,                   //message
        '',                     //exchange
        'outgoing-email'        //routing key
);
/* close connection and channel */
$chan->close();
$conn->close();
header('Location: index.php?sent=true');
?>

Напишем последний скрипт recv.php, который будет получать данные от RabbitMQ и отправлять письма на SMTP сервер:

<?php
require_once __DIR__.'/vendor/autoload.php';

use PhpAmqpLib\Connection\AMQPStreamConnection;
use lib\swift_required;

/* create connection to rabbit */
$conn = new AMQPStreamConnection(
        'localhost',    //server
        5672,           //port
        'username',     //user
        'password',     //pass
        '/'             //vhost
);
/* create channel */
$chan = $conn->channel();
/* create queue */
$chan->queue_declare(
        'outgoing-email',
        false,
        false,
        false,
        false
);

echo '[x] waiting for message',"\n";

/* send message to smtp relay */
$callback = function($msg){
        echo '[x] message received',"\n";

        $data = json_decode($msg->body,true);

        $from           = $data['from'];
        $from_email     = $data['from_email'];
        $to_email       = $data['to_email'];
        $subject        = $data['subject'];
        $message        = $data['message'];

        $transport = (new Swift_SmtpTransport('x.x.x.x',25,false))
                ->setUsername(false)
                ->setPassword(false);
        $mailer = new Swift_Mailer($transport);
        $message = (new Swift_Message($transport))
                ->setSubject($subject)
                ->setFrom(array($from_email => $from))
                ->setTo(array($to_email))
                ->setBody($message);
        $mailer->send($message);

        echo '[x] message sent',"\n";
        //send acknowledge to rabbit (tell rabbit that delivery arrived successfully)
        $msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
};
//check if message was processed (something like loadbalance between exchanges)
$chan->basic_qos(null,1,null);
//get message from queue and do something
$chan->basic_consume('outgoing-email','',false,false,false,false,$callback);
while(count($chan->callbacks)){
        $chan->wait();
}
/* close channel and connection */
$chan->close();
$conn->close();
?>

Уточнение по swiftmailer. Где х.х.х.х это адрес SMTP сервера (либо IP-адрес либо доменный). Порт в моем случае 25 и поскольку без шифрования, то после порта стоит false. В setUsername и в setPassword, тоже стоят false поскольку мой SMTP релей не требует ввода логина и пароля.

Запускаем последний скрипт «php recv.php» и смотрим в терминал. В браузере открываем форму, заполняем и отправляем письмо. В терминале должны будете увидеть сообщение, а на почту должно прийти письмо.