Массовая рассылка почты
20.12.2016Ранее я уже рассказывал как создавать модальное окно средствами PHP и AJAX в этой статье..
Пришло время показать обещанный способ массовой рассылки писем с помощью web-скриптов.
Конечно структура домена у каждого своя, но основной принцип постороения Active Directory одинаков везде.
За основу я взял структуру формата «домен-предприятие-пользователь», из чего и исходил выстраивая логику программы.
Для начала, пользователь выполняющий рассылку писем, выбирает группу предприятий и применяет этот фильтр, получая на выходе предприятия выбранной группы. Затем выбирает нужное предприятие, или через нажатие и удердживание клавиши CTRL — несколько предпритий, либо с помощью SHIFT — выбирает все предприятия группы.
Затем формируется список получателей рассылки, при необходимости фильтруя получателей по любому признаку.
После чего отмечает необходимых и вызывает форму отправки, являющуюся модальным окном.
На ниже показанной записи видна вся цепь операций..

В данной реализации использована верстка страницы DIV-ами, копонент jquery.dataTables, использованы массивы данных, объектная технология jQuery а так же возможности PHP для отправки e-mail сообщения как с вложениями, так и без него.
Эта реализация не является универсальной, поскольку некоторые параметры я заложил в объекты вручную, хотя можно было и это автоматизировать…
Не буду подробно описывать разметку страницы, остановлюсь лишь на некоторых моментах.
1. Список предприятий формируется в элементе типа SELECT:
<div id="sidebar" class="sidebar"><select id="pred" style="margin-top: 4px; width: 191px;" multiple="multiple" name="pred[]" size="36"> <option disabled="disabled" value="0">Список предприятий из AD</option> </select></div>
Как видно из кода, элементу задан размер, положение, параметр multiple, а так же первый элемент списка в качестве оглавления (не доступен для выбора пользователем).
2. Список пользователей формируется в таблице типа dataTables, она сформирована так:
</pre> <div id="content" class="content"> <table id="example" class="display" width="99%" cellspacing="0"> <thead> <tr> <th>Пред.</th> <th>ФИО пользователя</th> <th>Должность</th> <th>Электронная почта</th> <th>Телефон</th> <th>Город</th> </tr> </thead> <tbody id="refresh"></tbody> </table> </div> <pre>
3. Как вы заметили, для осуществления выполнения основных фунций использовано 4 основных кнопки, — функционал остальных, думаю, и так понятен.
3.1. Кнопка получения списка предприятий по фильтру:
<button id="showpred">Применить фильтр</button>
По нажатию на нее отрабатывает прописаный в хедере страницы скрипт, выполняющий запрос к AD.
Что делает скрипт? Чтобы это понять посмотрите на структуру одного из блоков верстки страницы:
<div class="menu"><span style="color: #ffffff;"> Группа предприятий : <input id="p1" name="p1" type="checkbox" value="ДИ*" />ДИ <input id="p2" name="p2" type="checkbox" value="ВЧДЭ*" />ВЧДЭ <input id="p3" name="p3" type="checkbox" value="ПЧ*" />ПЧ <input id="p4" name="p4" type="checkbox" value="ШЧ*" />ШЧ <input id="p5" name="p5" type="checkbox" value="ДПМ*" />ДПМ </span></div>
Здесь видно, что каждый из чекбоксов имеет некое значение, которое и считывается скриптом при его выполнении:
$('#showpred').click( function () {
var objSel = document.getElementById("pred");
var checked = [];
var count = 5;
var i = 0;
var str="?pr=";
loading = 'Загрузка...<img class="centered" src="712.gif" />'; //гифка ожидания загрузки
while(++i <= count) {
if(document.getElementById("p" + i).checked) {
checked.push(document.getElementById("p" + i).value);
}
}
str=str+checked;
// тут нужно отослать запрос ajax скрипту выбора предприятий и вернуть выхлоп
$.ajax({
type: "GET",
url: "preds.php"+str,
cache: false,
beforeSend: function() { $('#sidebar').html(loading); },
success: function(responce){ $('#sidebar').html(responce); }
});
})
Скрипт отсылает запрос файлу preds.php методом GET, а результат выполнения принимает в объект div с id=»sidebar».
PHP-скрипт preds.php выполняет запрос к AD, откуда и возвращаются значения…
Он предельно прост и не должен вызвать дополнительных вопросов. Код preds.php:
$preds = array();
$com_arr=array();
$str=$_GET["pr"];
$pr = explode(",", $str);
$cnt=count($pr);
$i=0;
while ($i < $cnt) {
$arrstr=$arrstr."\"".$pr[$i]."\"".",";
$preds[]=$pr[$i];
$i++;
}
$arrstr = substr($arrstr, 0, -1);
$ds=ldap_connect("ldap-сервер"); // Необходимо указать корректный LDAP сервер
ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3);
if ($ds) {
$r=ldap_bind($ds, "логин к домену" ,"пароль"); // " не анонимная" привязка,
foreach ( $preds as $pred ) {
$sst=iconv('cp1251','utf-8', $pred);
$sr=ldap_search($ds, "OU=ESRR, DC=esrr, DC=oao, DC=rzd","company=".$sst );
$info = ldap_get_entries($ds, $sr);
for ($i=0; $i<$info["count"]; $i++) {
$company=iconv('utf-8', 'cp1251',$info[$i]["company"][0]);
$com_arr[]=$company;
}
}
}
$com_arr=Array_Unique($com_arr);
sort($com_arr);
echo " <select id="\"pred\"" multiple="multiple" name="\"pred[]\"" size="\"36\"">";
echo "
<option disabled="disabled">Список предприятий из AD</option>";
while (list($key, $val) = each($com_arr)) {
echo "
<option>" . iconv('cp1251','utf-8',$val) . "</option>";
}
echo "
</select>";
//Закрытие соединения
ldap_close($ds);
Строка get-запроса может содержать более одного параметра — все они успешно обрабатываются данным скриптом.
Так мы сформировали список предприятий по заданному фильтру (фильтрам).
Теперь можно выбрать одно или несколько, либо же все загруженные по фильтру предприятия и отправить запрос к Active Directory с просьбой выгрузить в таблицу все связанные с этим предприятием (-ями) учетные записи пользователей.
Понятно, что домен содержит громадный объем информации по каждому пользователю, а нам ведь нужны лишь некоторые параметры, такие как ФИО, e-mail, должность, телефон и место расположения (город), поэтому скрипту возвращающему информацию задаем необходимые фильтры запроса.
3.2. Кнопки формирования списка работников выбранных предприятий и создания письма:
<div class="footer"><button id="newlist" style="margin-bottom: -30px;"><img style="vertical-align: middle;" src="W.bmp" /> Сформировать список</button> <button id="newmail" style="margin-bottom: -30px;"><img style="vertical-align: middle;" src="m1.gif" />Создать рассылку</button></div>
И так, кнопки для управления рассылкой писем созданы, — наполним их функционалом:
Функция формирования таблицы со списком всех работников предприятий ранее выбранных:
Наполняет нашу таблицу набором данных о возможных получателях рассылки.
$('#newlist').click( function () {
var loading;
loading2 = '<img class="centered" src="712.gif" />';
var pstr="?up=";
var prSel = $("#pred").val() || [];
pstr=pstr+prSel.join(",");
if(pstr != "?up="){
$('#refresh').html(loading2);
table.ajax.url('users.php'+pstr).load();
}else{
alert("Не выбрано предприятие !");
}
})
Функция формирования массива электронных адресов получателей письма:
Формирует массив электронных адресов и вкладывает его в скрытое поле «кому» нашего письма.
$('#newmail').click( function (event) {
selrow = table.rows( '.selected').indexes().toArray();// массив номеров выбранных строк (1-я строка=0)
mailarr=[];
//subArr для выборки почты.. rows(i)-индекс из массива номеров строк таблицы
for (var i = 0; i < selrow.length; i++) {
idx=selrow[i];
temp = table.rows(idx).data().join();
subArr = temp.split(",");
umail = subArr[3];
mailarr.push(umail);
}
// а это формирование значения для скрытого поля формы
// - в него заложены адреса получателей из массива.
$('#marr').val(mailarr);
Таким образом мы выполнили первый этап подготовки письма. Где четвертая заявленная кнопка, спросите…
Она просто не описана. Ее функция заключается в том, чтобы после применения фильтра или без него, выбрать всех отображенных получателей. Вот код ее применения и заодно двух ранее не описаных:
$('#selall').click( function () {
table.rows({ page: 'current'}).select();
} );
$('#all').click( function () {
table.rows().select();
} );
$('#deselall').click( function () {
table.rows().deselect();
} );
Думаю все понятно из кода..,
4. Таблица данных и ее наполнение.
Как я ранее писал, мною была использована jquery.dataTables.
Ее параметры заданы в данном примере так:
<script class="init" type="text/javascript">
$(document).ready(function() {
var loadfiles;
var loading;
var mailarr = [];
var subArr = [];
var liststr = [];
var umail;
var cnt;
var selrow;
var table=$('#example').DataTable( {
deferRender: true,
scrollY: 440,
scrollX: 395,
scroller: true,
select: {
style: 'multi'
},
order: [[ 1, "asc" ]],
sAjaxSource: ""
} );
Для наполнения таблицы был использован срипт users.php?up=ххх
Он, так же как и ранее описаный preds.php, делает запрос к домену в качестве параметра используя имя предприятия, а поскольку их может быть несколько — процедура выполняется в цикле.
Приводить здесь код не буду, т.к. аналогичный пример я уже рассматривал в другой статье.
5. Создание и отправка сообщения.
Для удобства использования, я применил модальную форму, на которой и разместил визуальную часть формирования письма. Об этом я так же ранее рассказывал. Содержимое модального окна таково:
<div id="modal_form"><span id="modal_close">X</span><form id="mf" enctype="multipart/form-data" name="mailer"> <h3 id="titleform">Создание сообщения электронной почты</h3> <div id="mfc"> Тема: <input id="tema" style="font-size: 16px;" name="tema" size="64" type="text" value="" /> Файл: <span id="ff"><input id="fil" style="font-size: 16px;" multiple="multiple" name="fil[]" size="51" type="file" /></span> Текст: <textarea id="mess" style="font-size: 14px;" cols="59" name="mess" rows="9"></textarea> Контактное лицо: <input id="kont" style="font-size: 16px;" name="kont" size="56" type="text" value="" /> <input id="marr" name="marr" type="hidden" value="" /></div> </form> <div align="center"><button id="knopka" style="font-size: 20px; color: red;">Отправить сообщение</button></div> </div>
Здесь все просто и понятно. Основной интерес вызывает сам процесс отправки сообщения.
Обратите внимание на идентификатор кнопки отправки сообщения: button id=»knopka»
И привязанный к ее нажатию код:
$('#knopka').click( function(){
var formData = new FormData($('#mf')[0]); //Получение данных из формы
$.ajax({ // отправляем данные
url: 'test.php',
type: 'post',
processData: false,
contentType: false,
data: formData,
success: function(data){ // после выполнения отправки:
$('#mf')[0].reset(); // очищаем поля формы
$("#fil").remove(); // очищаем поле с инфой об отправляемых файлах
$("#ff").append("<input id="fil" style="font-size: 16px;" multiple="multiple" name="fil[]" size="51" type="file" />");
alert("Отправка завершена! Отправлено "+data+" писем.");
}
});
});
И вот он, самый интересный момент! А именно скрипт отправляющий письмо (здесь test.php).
При отправлении письма, я учел некоторую особенность нашего почтового сервера, да наверное почти везде так, — большое количество получателей одновременно, может быть распознано как спам, — именно по этой причине, мой скрипт отправляет почту по одному адресату, циклом, пока не пройдет всех получателей. Это несколько затормаживает процесс отправки, но не засчитывается как спам, и что может быть полезным, — каждый получивший письмо не видит остальных получателей.
Изначально я пытался выполнять отправку стандартными методами, но так или иначе возникали ошибки с вложениями или с кодировкой писем. Метод научного тыка и гугления привел к решению использовать классы, их в начале скрипта и описываем:
class Send_mail {
private $_params = array(
'email' => '',
'from_name' => '',
'from_mail' => '',
'subject' => '',
'message' => '',
'files' => array(),
'charset' => 'windows-1251',
'content_type' => 'plain',
'time_limit' => 30
);
private $_error = true;
private $_error_text = '
<span style="color: #f00;">';
public function __call($name, $param)
{
if(!isset($this->_params[$name]))
{
$this->_error_text .= 'Некорректный параметр! '.$name.'()';
$this->_error = false;
}
else if(count($param) > 1)
{
$this->_error_text .= 'Ожидается 1 параметр в '.$name.'()!';
$this->_error = false;
}
else
{
$this->_params[$name] = isset($param[0]) ? $param[0] : '';
}
return $this;
}
public function send()
{
if($this->_error_email() === false)
echo $this->_error_text;
else
$this->_send();
}
private function _send()
{
$from_name = '=?'.$this->_params['charset'].'?B?'.base64_encode($this->_params['from_name']).'?=';
$subject = '=?'.$this->_params['charset'].'?B?'.base64_encode($this->_params['subject']).'?=';
$header = "From: ".$from_name." <".$this->_params['from_mail'].">\r\n";
$header .= "MIME-Version: 1.0\r\n";
// Если есть прикреплённые файлы
if(!empty($this->_params['files']))
{
if(!is_array($this->_params['files']))
$this->_params['files'] = array($this->_params['files']);
$bound = md5(uniqid(time())); // Разделитель
$header .= "Content-Type: multipart/mixed; boundary=\"".$bound."\"\r\n";
$header .= "This is a multi-part message in MIME format.\r\n";
$message = "--".$bound."\r\n";
$message .= "Content-Type: text/".$this->_params['content_type']."; charset=".$this->_params['charset']."\r\n";
$message .= "Content-Transfer-Encoding: quoted-printable\r\n\r\n";
$message .= $this->_params['message']."\r\n\r\n";
$finfo = finfo_open(FILEINFO_MIME_TYPE);
foreach($this->_params['files'] as $file_name)
{
$name = preg_replace('~.*([^/|\\\]+)$~U', '$1', $file_name);
$name = iconv('UTF-8','cp1251' , $name);
//$name = iconv('cp1251', 'UTF-8', $name);
$name = "=?".$this->_params['charset']."?B?".base64_encode($name)."?=";
$message .= "--".$bound."\r\n";
$message .= "Content-Type: ".finfo_file($finfo, $file_name)."; name=".$name."\r\n";
$message .= "Content-Transfer-Encoding: base64\r\n";
$message .= "Content-Disposition: attachment; filename=\"".$name."\"; size=".filesize($file_name).";\r\n\r\n";
$message .= chunk_split(base64_encode(file_get_contents($file_name)))."\r\n";
}
$message .= $bound."--";
} else // Если нет файлов
{
$header .= "Content-type: text/".$this->_params['content_type']."; charset=".$this->_params['charset']."\r\n";
$message = $this->_params['message'];
}
set_time_limit($this->_params['time_limit']);
// Отправка сообщения
if(is_array($this->_params['email']))
{
foreach($this->_params['email'] as $email)
@mail($email, $subject, $message, $header);
}
else
{
@mail($this->_params['email'], $subject, $message, $header);
}
} }
Ну и дальше просто приведу ход отправки сообщения в этом же скрипте.
Код приведен ASIS — полностью рабочий, используемый в «живой» системе.
$files =array();
foreach($_FILES['fil']['name'] as $k=>$f) {
if (!$_FILES['file']['error'][$k]) {
if (!empty($_FILES['fil']['tmp_name'][$k]))
{
$path = "sendfiles/".$_FILES['fil']['name'][$k];
if (copy($_FILES['fil']['tmp_name'][$k], $path)) {
$files[] = $path;
}
}
}}
$IP = $_SERVER['REMOTE_ADDR'];
$host = gethostbyaddr($_SERVER['REMOTE_ADDR']);
$host = str_replace("-", "_", $host);
$host= stristr($host, '.', true);
$send_mail = new Send_mail();
$otpr='Сервис рассылки ВСДИ';
$eotpr='noreply';
$subj=iconv('utf-8','cp1251', $_POST['tema']);
$mess= iconv('utf-8','cp1251', $_POST['mess']);
$mess = nl2br($mess);
$mess.='<br>---<br>';
$mess.=' Контактное лицо: '.iconv('utf-8','cp1251', $_POST['kont']);
$mess='Письмо отправлено с компьютера IP : '.$IP.' Учетная запись : '.$host;
$j=0;
$mailarr = array();
$str=$_POST['marr'];
$str_arr=explode(",",$str);
$size=count($str_arr);
for($i=0;$i<$size;$i++) { $email= $str_arr[$i]; if($email !=""){$j++;} $send_mail->email($email) // Адресат
->from_name($otpr) // Имя отправителя
->from_mail($eotpr) // Адрес отправителя
->subject($subj) // Тема сообщения
->message($mess) // Тело сообщения
->files($files) // Путь до прикрепляемого файла (можно массив)
->charset('windows-1251') // Кодировка (по умолчанию utf-8)
//->time_limit(30) // set_time_limit (по умолчанию == 30с.)
->content_type('html') //(html) тип сообщения (по умолчанию 'plain')
->send(); // Отправка почты
}
foreach($_FILES['fil']['name'] as $k=>$f) {
unlink("sendfiles/".$_FILES['fil']['name'][$k]); // удаляет загруженный файл
}
Удачного применения!
P.S: буду благодарен за «лайк» 🙂