вторник, 28 февраля 2012 г.

Делаем систему массовой рассылки. Удаление "плохох" адресов.


Итак. Сама система генерации и рассылки писем на кучу адресов - дело элементарное. Давайте сейчас рассмотрим вопрос по исключению из последующих рассылок емайл адресов, которые вернулись назад, т.к. такого адреса не существует (ну или по какой либо другой причине). Изначально я хотел писать скрипт, который бы по pop3 протоколу соединялся бы с сервером почты, забирал ее и дальше парсил, но, как известно, лень - двигатель прогресса. В результате немного гугла и организовалось простое и, главное, быстрое решение этой задачи.


В моем случае рассылка производится с выделенного сервера на котором находится сайт организации и завуалирована под новостную. Она и вправду новостная, можно подписываться и отписываться и т.д. но вот мой начальник в нее добавляет кучу адресов электронной почты людей, которые и не думали подписываться, так что кроме как спам рассылкой, это я назвать никак не могу :). На момент написания статьи их уже более 40 тысяч.

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

Для того чтобы получить необходимую нам почту будем использовать fetchmail, а для того, чтобы распарсить - procmail и небольшой скрипт на пхп. Для начала устанавливаем на сервере fechmail и procmail
sudo apt-get install fetchmail procmail
fetchmail необходимо запускать от обычного пользователя и его файл настроек ( .fetchmailrc, который находится в домашней директории) должен быть с правами 710. соответственно:
sudo chmod 710 /usr/home/USER_NAME/.fetchmailrc
sudo chown USER_NAME /usr/home/USER_NAME/.fetchmailrc
далее редактируем этот ./fetchmailrc, а точнее, прописываем у него почтовый сервер сервер гугла и почтовый аккаунт (на который нам будут приходить возвраты). Вот пример моих настрок:

-----------~/.fetchmailrc---------------------------
#File : .fetchmailrc
set postmaster "local_user"

poll pop.gmail.com with proto POP3 and options no dns
user 'MYEMAIL@MYDOMEN.COM' is 'GMAIL_USER' here options ssl
password "PASSWORD"
----------------------------------------------------

теперь настроим procmail, который будет фильтровать почту и ложить на различные почтовые ящики (сохранять в различные файлы). В домашней директории необходимо создать файл настроек procmail - ~/.procmailrc. его содержимое я любезно позаимствовал на сайте документации по gentoo, добавив в него всего лишь одно правило. На всякий случай привожу файл полностью.

-----------~/.procmailrc----------------------------
MAILDIR=$HOME/MuttMail                ##проверьте правильность пути
LOGFILE=$HOME/.procmaillog
LOGABSTRACT=no
#VERBOSE=on... используется только для отладки
VERBOSE=off
FORMAIL=/usr/bin/formail
NL="
"
##условные строки начинаются с :0
##не записывайте комментарии в строки условия
##отредактируйте ненужные условия!
##строки условий начинаются с *, а регулярные выражения ваши лучшие друзья
##условия добавленные после * попадают прямо в egrep
##строка следущая за условиями, в следующем регистре является именем почтового ящика

#отлавливание копий, используя formail
:0 Whc: .msgid.lock
| $FORMAIL -D 16384 .msgid.cache

:0 a
$MAILDIR/duplicates

#люди которые всегда пишут с одного почтового адреса
:0 
* ^From:.*(craig\@hotmail|renee\@local.com)
$MAILDIR/friends 

#выборка некоторого спама
:0  
* ^Subject:.*(credit|cash|money|debt|sex|sale|loan)
$MAILDIR/spam

#никаких html писем
:0
* ^Content-Type:.*html
$MAILDIR/junk

:0 
* ^From:.*@freshmeat\.net
freshmeat

:0
* ^From:.*MAILER-DAEMON@ubuntu\-1004\-lucid\-64\-minimal
returned-emails

###########################################
# последние условие: складирует остальную #
# почту в почтовый ящик по умолчанию      # 
###########################################
:0 
* .*
default

# конец файла
----------------------------------------------------

тут я лишь добавил условие:
:0
* ^From:.*MAILER-DAEMON@ubuntu\-1004\-lucid\-64\-minimal
returned-emails
которое говорит, что письма содержащие в поле From адрес MAILER-DAEMON@ubuntu-1004-lucid-64-minimal (это от моего сервера) помещать в ящик returned-emails
теперь, если запустить fetchmail командой:
fetchmail -k -m "/usr/bin/procmail -d %T"
то в директории ~/MuttMail создаться файл returned-emails с почтой, которая была послана нашим сервером но по какой-то причине вернулась. ключ -k указывает fetchmail что почту необходимо оставлять на сервере. Теперь нам нужно получить из него email адреса, на которые была почта так неудачно послана. Для этого я написал небольшой скрипт на php.

------------wrong_emails.php------------------------
#!/usr/bin/php
query("UPDATE `".$config['ros_db']."`.`ems_email_list` SET `wrong_email`='1', `date_wrong`=NOW() WHERE `email`='".$value."'");
            $updated_rows+=mysql_affected_rows();
        }
// show result
        echo "Strings readed: ".$str_counter."\nWrong emails: ".count($wrong_emails)." Updated in DB: ".$updated_rows."\n";
    }else{
        echo "Error: Can't open ".$_EMAILS_FILE." file.\n";
    }
?>
----------------------------------------------------

здесь мы просто читаем построчно файл полученной от procmail почты и ищем в нем строки "----- The following addresses had permanent fatal errors -----", в следующей за ней строке содержится адрес электронной почты, на который не смогла быть доставлена рассылка. Мы его запоминаем в массив $wrong_emails а после того, как просмотрели весь файл - соединяемся с базой данных, в которой у нас содержаться email адреса рассылки и помечаем полученные адреса как wrong_email, а также, дату, когда мы этот адрес пометили как неверный. Дата нам нужна для того, чтобы потом в базе можно было эти адреса отыскать. Удалять эти адреса из базы нам нежелательно, дабы не добавить их потом снова (я же говорил, что это не совсем нормальная рассылка - пользователи не сами подписываются а добавляются моим начальством через скрипт экспорта, эти адреса они выдирают откуда только возможно. даже есть специальный сканер для визиток и т.д.).

Подключаемый файл "class.rose_mysql.php" - это небольшой класс обертки над mysql из моего личного проекта - панели управления для MMO Ragnarok Online сервера: http://ro-se.sourceforge.net. Вот его код:

<?php
////////////////////////////////////////////////////////////////////////////////
//
//     Ragnarok Online Site Engine (ROse)
//     Copyright (c) 2006-2012 Fantik (aka Tamahome, aka Uvadzucumi)
//
////////////////////////////////////////////////////////////////////////////////
// ROSE MySQL Class
//
// Methods:
//      private function __construct($db_host, $db_user, $db_password, $db_name='', $debug=false)
//
//      public static function connect($db_host, $db_user, $db_pass, $db_name='', $debug = false)
//
// mysql escape row, check MQ
// TODO - check MQ moved to get POST data in ROSE main class 
//      public function escape($source, $remove_html_tags=false)
//
// create and run insert query:
// $table - database table
// $data - associated array with insert "field_name"=>"field_value"
//      public function insert_one($table, $data)
//
// create and run update query
// $table - table name
// $data - associated array with insert "field_name"=>"field_value"
// $id - update records uniq value
// $id_field_name - updated records uniq field - default `id`
//      public function update_one($table, $data, $id, $id_field_name="`id`");
//
// Return array contained all query result
//      public function GetAll($query);
//

class ROSE_MYSQL{

    private $_dbh;  
    private $_debug = false;
    private static $_instance;
    private $_error;
    private $_last_result;
    private $_magic_quotes;

    private function __construct($db_host, $db_user, $db_password, $db_name='', $debug=false){
        $this->_debug = $debug;
        $this->_dbh = @mysql_connect($db_host, $db_user, $db_password);
        if ($this->_dbh == false){ // if not connected
            if(mysql_error()){
                $this->_error = mysql_error();
            }else if(isset($php_errormsg)){
                $this->_error = $php_errormsg;
            }else{
                $this->_error = 'Error conection to database: check setup parameters.';
            }
        }else{ // if connected
// Select database, if needed
            if($db_name!=""){
                if(!@mysql_select_db($db_name,$this->_dbh)){
                    die('Error select detabase '.$db_name);
                }
            }
            $this->query("SET NAMES 'utf8' COLLATE 'utf8_general_ci'");
            return $this->_dbh;
        }
        $this->_magic_quotes=get_magic_quotes_gpc();
    }

// run sql $query, return sql resource
    public function query($query){
        if($this->_debug){ echo "
".$query."
\n";} if(is_resource($this->_last_result)){ // clear previous result mysql_free_result($this->_last_result); } $result = mysql_query($query, $this->_dbh); if(!$result){ $this->_error=mysql_error(); if($this->_debug){ echo $this->_error."\n"; echo "query:".$query."\n"; } } $this->_last_result=$result; return $result; // false, true, resource } // connect to database public static function connect($db_host, $db_user, $db_pass, $db_name='', $debug = false){ if(self::$_instance === null){ self::$_instance = new self($db_host, $db_user, $db_pass, $db_name, $debug); } return self::$_instance; } // mysql escape row, check MQ // TODO - check MQ moved to get POST data in ROSE main class public function escape($source, $remove_html_tags=false){ if($this->_magic_quotes){ // remove quotes if MQ enabled $str = stripslashes($source); } if($remove_html_tags){ // remove html tags if needed // $source=htmlspecialchars($source, ENT_QUOTES); $source=str_replace('<','<',$source); $source=str_replace('>','>',$source); } return mysql_real_escape_string($source); } // create and run insert query: // $table - database table // $data - associated array with insert "field_name"=>"field_value" public function insert_one($table, $data){ $query = "INSERT INTO $table SET "; $insert = array(); foreach($data as $field => $value){ if($value!='NOW()'){ $insert[] = "`$field` = '".$this->escape($value)."'"; }else{ $insert[] = "`$field` = $value"; } } $query .= implode(', ', $insert); return $this->query($query); } // create and run update query // $table - table name // $data - associated array with insert "field_name"=>"field_value" public function update_one($table, $data, $id, $id_field_name="`id`"){ $query = "UPDATE $table SET "; $update = array(); foreach($data as $field => $value){ if($value!='NOW()'){ $update[] = "`$field` = '".$this->escape($value)."'"; }else{ $update[] = '`'.$field.'` = '.$value; } } $query .= implode(', ', $update); $query .= " WHERE ".$table.".".$id_field_name." = $id"; return $this->query($query); } // public function delete($table, $id, $id_field_name='`id`'){ $where=''; if(is_array($id)){ foreach($id as $id_value){ if($where!=''){ $where.=' OR '; } $where.="$id_field_name = '".(int)($id_value)."'"; } }else{ $where="$id_field_name = '".(int)($id)."'"; } return $this->query("DELETE FROM $table WHERE ".$where); } // Return array contained all query result public function GetAll($query){ $ret=array(); $result=$this->query($query); if($result){ $num=mysql_num_rows($result); for($n=0;$n<$num;$n++){ $ret[]=mysql_fetch_assoc($result); } return $ret; }else{ return false; } } // Return array contained first query result public function GetOne($query){ $ret=false; $result=$this->query($query); if($result){ return mysql_fetch_assoc($result); }else{ return false; } } // create where string with logic operator public function create_where($fld_name,$values,$logic_operator=' OR '){ $where=''; foreach($values as $v){ if($where!=''){ $where.=$logic_operator; } $where.='`'.$fld_name."`='".$v."'"; } return $where; } } ?>


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

----------------parse_mail.sh-----------------------
#!/bin/bash
fetchmail -k -m "/usr/bin/procmail -d %T"
./wrong_emails.php
rm ~/MuttMail/returned-emails
exit
----------------------------------------------------

Вот, пожалуй, и все, что касается относительно отслеживания неверных email адресов из списка нашей рассылки.

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

  1. Вот как по мне http://StandartSend.ru Отличный сервис для массовой рассылке писем, использую его постоянно и пока нареканий нет, очень качественно и грамотно всё делают, рекомендую попробовать

    ОтветитьУдалить
  2. Классный пост. Очень хорошо расписано всё. Надо поробывать. Я делаю через mutt и smtp. Инструкция здесь
    http://itc-life.ru/massovaya-rassylka-iz-konsoli-s-pomoshhyu-mutt/

    ОтветитьУдалить
  3. Сам имею предпочтение к программе StandartMailer, брал отсюда - http://StandartMailer.ru, уж очень хитро фильтры спама она вскрывает, отсюда и эффективность.

    ОтветитьУдалить