Массовая рассылка почты

20.12.2016

Ранее я уже рассказывал как создавать модальное окно средствами PHP и AJAX в этой  статье..
Пришло время показать обещанный способ массовой рассылки писем с помощью web-скриптов.
Конечно структура домена у каждого своя, но основной принцип постороения Active Directory одинаков везде.

За основу я взял структуру формата «домен-предприятие-пользователь», из чего и исходил выстраивая логику программы.
Для начала, пользователь выполняющий рассылку писем, выбирает группу предприятий и применяет этот фильтр, получая на выходе предприятия выбранной группы. Затем выбирает нужное предприятие, или через нажатие и удердживание клавиши CTRL — несколько предпритий, либо с помощью SHIFT — выбирает все предприятия группы.
Затем формируется список получателей рассылки, при необходимости фильтруя получателей по любому признаку.
После чего отмечает необходимых и вызывает форму отправки, являющуюся модальным окном.
На ниже показанной записи видна вся цепь операций..
spam
В данной реализации использована верстка страницы 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="\&quot;pred\&quot;" multiple="multiple" name="\&quot;pred[]\&quot;" size="\&quot;36\&quot;">";
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: буду благодарен за «лайк» 🙂

Оставить комментарий