Массовая рассылка почты
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: буду благодарен за «лайк» 🙂