Monthly Archives: Июль 2014

Пишем свой сборщик писем на PHP (3 часть из 3)

Описание использованных функций (func.php):

<?php
 
//установим часовой пояс по умолчанию,
//чтобы вызов функции date() не вызывал предупреждений
date_default_timezone_set('Asia/Almaty');
 
//директория для хранения вложений писем
$FILES = "files";
//если директория не существует, то создаем её
if(!file_exists($FILES)) mkdir($FILES);
 
//директория для хранения логов
$LOG_DIR = "logs";
if(!file_exists($LOG_DIR)) mkdir($LOG_DIR);
 
//логи за один день будем хранить в одном файле.
//Например: logs/2014-07-31.log
$LOG_FILE = $LOG_DIR . DIRECTORY_SEPARATOR . date('Y-m-d') . ".log";
 
//функция для ведения логов
//логи пишутся в файл log.txt, который будет находится
//в той же директории что и скрипт, пишущий лог
function wr($msg){
    global $LOG_FILE;
	$date = date('d/m/Y H:i:s');
	file_put_contents($LOG_FILE, "$date $msg\n", FILE_APPEND);
}
 
//вывод на сообщения на экран
function pr($msg){
	echo "$msg<br>";
}
 
//вывод содержимого объекта на экран
function debugObj($obj){
	echo "<";
    echo "pre style='color:green'>";
	print_r($obj);
	echo "</";
    echo "pre>";
}
 
//получение заголовка письма в виде объекта
function getHeader($mbox,$uid){
    return imap_rfc822_parse_headers(getHeaderRaw($mbox,$uid));
}
 
//получение заголовка письма
function getHeaderRaw($mbox,$uid){
    return imap_fetchbody($mbox, $uid, '0', FT_UID);
}
 
//раскодировка заголовка
function getDecodedHeader($text){
    //imap_mime_header_decode - декодирует элементы MIME-шапки в виде массива
    //У каждого элемента указана кодировка(charset) и сам текст(text)
    $elements = imap_mime_header_decode($text);
    $ret = "";
    //перебираем элементы
    for ($i=0; $i<count($elements); $i++) {
        $charset = $elements[$i]->charset;//кодировка
        $text = $elements[$i]->text;//закодированный текст
        if($charset == 'default'){
            //если элемент не кодирован, то значение кодировки default
            $ret .= $text;
        }else{
            //приводим всё кодировке к UTF-8
            $ret .= iconv($charset,"UTF-8",$text);
        }
    }
    return $ret;
}
 
//получение содержимого письма в виде простого текста
function getTextBody($imap,$uid){
    return getPart($imap, $uid, "TEXT/PLAIN");
}
 
//получение содержимого письма в виде формате html
function getHtmlBody($imap,$uid){
    return getPart($imap, $uid, "TEXT/HTML");
}
 
//получение части письма
function getPart($imap, $uid, $mimetype) {
    //получение структуры письма
    $structure = imap_fetchstructure($imap, $uid, FT_UID);
    if ($structure) {
        if ($mimetype == getMimeType($structure)) {
            $partNumber = 1;
            //imap_fetchbody - извлекает определённый раздел тела сообщения
            $text = imap_fetchbody($imap, $uid, $partNumber, FT_UID);
 
            $charset = $structure->parameters[0]->value;//кодировка символов
 
            //0 - 7BIT; 1 - 8BIT; 2 - BINARY; 3 - BASE64; 4 - QUOTED-PRINTABLE; 5 - OTHER
            switch ($structure->encoding) {
                case 3:
                    //imap_base64 - декодирует BASE64-кодированный текст
                    $text = imap_base64($text);
                    break;
                case 4:
                    //imap_qprint - конвертирует закавыченную строку в 8-битную строку
                    $text = imap_qprint($text);
                    break;
            }
 
            if($mimetype == 'TEXT/PLAIN'){
                $text = iconv($charset,"UTF-8",$text);
            }
 
            if($mimetype == 'TEXT/HTML'){
                $text = iconv($charset,"UTF-8",$text);
            }
 
            return $text;
        }
    }
    return false;
}
 
//MIME-тип передается числом, а подтип - текстом.
//Функция приводит все в текстовый вид.
//Например: если type = 0 и subtype = "PLAIN",
//то функция вернет "TEXT/PLAIN".
//TEXT - 0, MULTIPART - 1, .. , APPLICATION - 3 и т.д.
function getMimeType($structure) {
    $primaryMimetype = array("TEXT", "MULTIPART", "MESSAGE", "APPLICATION", 
        "AUDIO", "IMAGE", "VIDEO", "OTHER");
    if ($structure->subtype) {
        return $primaryMimetype[(int)$structure->type] . "/" . $structure->subtype;
    }
    return "TEXT/PLAIN";
}
 
//перевести текст в дату MySQL
function strToMysqlDate($text){
    $unixTimestamp=strtotime($text);
    return date("Y-m-d H:i:s", $unixTimestamp);
}
 
//заполняем ассоциативный массив, где ключом является тип адреса,
//а значение массив адресов
function getAddress($header,$type,&$map){
    //проверка существования типа в заголовке
    if(property_exists($header,$type)){
        $arr = $header->$type;
        if(is_array($arr) && count($arr) > 0){
            $map[$type] = $arr;
        }
    }
}
 
//загрузка вложений
function loadAttaches($mbox,$uid,$message_id){
    //получаем структуру сообщения
    $struct = imap_fetchstructure($mbox,$uid,FT_UID);
    $attachCount = 0;
    if(!$struct->parts) return $attachCount;
    //перебираем части сообщения
    foreach($struct->parts as $number => $part){
        //ищем части, у которых ifdisposition равно 1 и disposition равно ATTACHMENT,
        //все остальные части игнорируем. Также стоит заметить, что значение поля
        //disposition может быть как в верхнем, так и в нижнем регистрах,
        //т.е. может быть "attachment" и "ATTACHMENT". Поэтому в коде всё приведено
        //к верхнему регистру
        if(!$part->ifdisposition 
            || strtoupper($part->disposition) != "ATTACHMENT")continue;
        //получаем название файла
        $filename = getDecodedHeader($part->dparameters[0]->value);
        //получаем содержимое файла в закодированном виде
        $text = imap_fetchbody($mbox, $uid, $number + 1, FT_UID);
        //декодирование содержимого файла
        switch ($part->encoding) {
            case 3:
                $text = imap_base64($text);
                break;
            case 4:
                $text = imap_qprint($text);
                break;
        }
        //оригинальное название файла будем сохранять в базе данных.
        //Разные письма могут иметь вложения с одинаковыми названиями,
        //поэтому в файловой системе будем сохранять файла с уникальным именем,
        //сохранив при этом расширение файла
        $file_path = getStoreDirectory() . getUid() . getFileExtension($filename);
        file_put_contents($file_path,$text);
 
        $content_type = getMimeType($part);//MIME-тип файла
        $filesize = strlen($text);//размер файла
 
        //записываем информацию о файле в базу данных. Напомню, что в
        //базу сохраняется не сам файл, а относительный путь к файлу
        $sql = "INSERT INTO attachments(message_id,file_name,mime_type,
            file_size,location)" .
            "VALUES('$message_id',
            '" . mysql_real_escape_string($filename) . "',
            '" . mysql_real_escape_string($content_type) . "',
            $filesize,
            '" . mysql_real_escape_string($file_path) . "')";
        $res_ins = mysql_query($sql) or die(mysql_error());
        $attachCount++;
    }
    return $attachCount;
}
 
//Функция для получения пути к директории, где будут храниться файлы.
//Файлы будут сохраняться в поддиректории, созданной по
//текущей дате. Например, 2014-07-31. Это позволит
//не держать файлы в одной директории. Много файлом в
//одной директории замедляет чтение директории
function getStoreDirectory(){
    global $FILES;
    $date_folder = "$FILES/" . date('Y-m-d') . "/";
    if(!file_exists($date_folder)) mkdir($date_folder);
    return $date_folder;
}
 
//генерация уникального идентификатора
function getUid(){
    if (function_exists('com_create_guid')){
        return str_replace("}", "", str_replace("{", "", com_create_guid()));
    } else {
        mt_srand((double)microtime()*10000);//optional for php 4.2.0 and up.
        $charid = strtoupper(md5(uniqid(rand(), true)));
        $hyphen = chr(45);// "-"
        $uuid =
            substr($charid, 0, 8).$hyphen
            .substr($charid, 8, 4).$hyphen
            .substr($charid,12, 4).$hyphen
            .substr($charid,16, 4).$hyphen
            .substr($charid,20,12);
 
    }
    return strtolower($uuid);
}
 
//получаем расширение файла
function getFileExtension($filename){
    $arr = explode(".",$filename);
    return count($arr) > 1 ? "." . end($arr) : "";
}
?>

Файл для подключения к базе данных (connect.php):

<?php
//настройки подключания базе данных MySQL
$con_host = "localhost";
$con_port = "3306";
$con_user = "root";
$con_pass = "YOUR_PASSWORD";
$sel_base = "mail_collector";
mysql_connect($con_host.":".$con_port,$con_user,$con_pass)
    or die("Error of connection to MySQL");
mysql_select_db($sel_base) or dir("Error of selecting database");//выбор базы данных
mysql_query("SET NAMES 'utf8'");//установка кодировки UTF-8
?>

Исходный код можно скачать здесь

Пишем свой сборщик писем на PHP (2 часть из 3)

Скрипт для скачивания писем (loader.php)

<?php
require_once "func.php";//подключаем функции
require_once "connect.php";//подключаем настройки к MySQL
 
wr("\n=============================\n");
 
//некоторые строки уже прокомменитированы в коде (см. reader.php),
//поэтому оставим их без комментариев
set_time_limit(0);
$lock = "loader.lock";
$aborted = file_exists($lock) ? filemtime($lock) : false;
$fp = fopen($lock,'w');
register_shutdown_function(function() use ($fp, $lock) {
    wr("shutdown");
    flock($fp, LOCK_UN);//снимаем блокировку с файла
    fclose($fp);//закрываем файл
    unlink( __DIR__ . DIRECTORY_SEPARATOR . $lock);//удаляем файл
});
if(!flock($fp,LOCK_EX|LOCK_NB)){
    wr("busy\n");//пишем в лог, что занято
}else{
    if($aborted){
        wr("Aborted\n");
    }
 
    //составим список только тех почтовых ящиков,
    //сообщения которых еще не скачаны
    $sql = "SELECT b.*
            FROM mailboxes b
            JOIN(
                SELECT mailbox_id FROM messages GROUP BY mailbox_id
            )m ON m.mailbox_id = b.id";
    $res = mysql_query($sql);
    $mailboxes = array();
    while($row = mysql_fetch_array($res)){
        array_push($mailboxes,$row);
    }
 
    //перебор почтовых ящиков
    foreach($mailboxes as $mailbox){
        $mailbox_id = $mailbox['id'];
        $host = $mailbox['host'];//адрес почтового сервера
        $port = $mailbox['port'];//порт почтового сервера
        $user = $mailbox['email'];//имя пользователя (почтовый ящик)
        $password = $mailbox['password'];//пароль к почтовому ящику
        $ssl = $mailbox['is_ssl'] ? "/ssl" : "";
        //строка подключения
        $conn = "{{$host}:{$port}{$ssl}}";
        wr("Read $user, conn = $conn");
 
        //открываем IMAP-поток
        $mail = imap_open($conn,$user,$password);
        if(!$mail){
            //пишем в лог сообщение о неудачной попытке подключения
            wr("Error opening IMAP. " . imap_last_error());
            continue;//переходим к следующему ящику
        }
 
        mysql_query("SET AUTOCOMMIT=0");
        mysql_query("START TRANSACTION");
 
        //получаем список сообщений, которые необходимо скачать с почтового ящика
        $sql = "SELECT *
                FROM messages
                WHERE mailbox_id = $mailbox_id AND is_ready = false";
        $res = mysql_query($sql);
        while($message = mysql_fetch_array($res)){
            $message_id = $message['id'];//ID письма в базе данных
            $message_uid = $message['uid'];//уникальный номер письма
            $headerRaw = getHeaderRaw($mail,$message_uid);//технический заголовок письма
            $header = getHeader($mail,$message_uid);//заголовок письма
            $subject = getDecodedHeader($header->subject);//тема письма
            $headerDate = strToMysqlDate($header->date);//дата письма
            $body_text = getTextBody($mail,$message_uid);//содержимое письма в виде простого текста
            $body_html = getHtmlBody($mail,$message_uid);//содержимое письма в формате html
 
            //получение адресов из заголовка письма
            $address_map = array();
            $address_types = array('to','from','reply_to','sender','cc','bcc');
            foreach($address_types as $address_type){
                getAddress($header,$address_type,$address_map);
            }
 
            foreach($address_map as $key => $arr){
                foreach($arr as $obj){
                    $type = $key;
                    $address = "$obj->mailbox@$obj->host";//склеиваем email
                    $sql = "INSERT INTO addresses(message_id,type,email)
                            VALUES($message_id,
                            '" . mysql_real_escape_string($type) . "',
                            '" . mysql_real_escape_string($address) . "')";
                    mysql_query($sql) or wr(mysql_error());
                }
            }
 
            //считываем вложения и получаем кол-во вложений,
            //которое записываем в базу данных
            $attachCount = loadAttaches($mail,$message_uid,$message_id);
 
            $sql = "UPDATE messages
                    SET subject = '" . mysql_real_escape_string($subject) . "',
                        body_text = '" . mysql_real_escape_string($body_text) . "',
                        body_html = '" . mysql_real_escape_string($body_html) . "',
                        header = '" . mysql_real_escape_string($headerRaw) . "',
                        message_date = '" . mysql_real_escape_string($headerDate) . "',
                        attachment_count = $attachCount,
                        modify_date = now(),
                        is_ready = true
                    WHERE id = $message_id";
            mysql_query($sql) or wr(mysql_error());
            mysql_query("COMMIT");
        }
 
    }
 
}
?>