Category Archive: PHP

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

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

//вывод содержимого объекта на экран
function debugObj($obj){
	echo "<";
    echo "pre style='color:green'>";
	print_r($obj);
	echo "";
}

//получение заголовка письма в виде объекта
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; $icharset;//кодировка
        $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 (2 часть из 3)

Скрипт для скачивания писем (loader.php)
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");
        }

    }

}
?>

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

В данной статье будет описано создание сборщика писем на PHP. Если в один прекрасный день у вас возникла необходимость забирать письма с почтового (почтовых) ящиков и куда-то складывать (скорее всего, в базу данных), то добро пожаловать. Очень надеюсь, что статья вам пригодиться и облегчит жизнь. В третьей части статьи будут приложены все необходимые коды. И так поехали... Сначала определимся с требованиями: 1) Реализация на PHP 2) Чтение писем по протоколу IMAP 3) Запись содержимого писем в базу MySQL 4) Сохранение вложений к письмам в файловой системе 5) Будет реализовано два скрипта: первый проверяет новые письма, а второй закачивает письма 6) Будет таблица, содержащая список почтовых ящик, с которых собираются письма 7) Также будет сохраняться информация о том: от кого письмо, кому письмо, кому переадресовано, кто в копии и кто в скрытой копии Почему именно такой выбор? Письма можно читать с почтового сервера по протоколам IMAP и POP3. Реализация сборщика будет построена с использованием протокола IMAP, т.к. он имеет ряд преимуществ. Не знаю как в реализациях POP3 на других языках, но в PHP мне не удалось получить UID письма, можно получить только порядковый номер письма среди новых. Также минусом при использовании POP3 является то, что письма нужно считывать с первого раза. А у нас будет другая реализация: письма сперва проверяются, а только потом закачиваются, что возможно благодаря IMAP. Вложения к письмам будем хранить в файловой системе, а в базе данных будут храниться только пути к этим файлам (хотя вам никто не запрещает записывать файлы в базу данных) ПРОЕКТИРОВАНИЕ ТАБЛИЦ Таблица почтовых ящиков Создадим в MySQL новую базу данных mail_collector с кодировкой UTF-8 по умолчанию:
CREATE DATABASE mail_collector DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci; 
Создадим таблицу mailboxes, которая будет хранить необходимую информацию для подключения к почтовому ящику.
CREATE TABLE mailboxes
(
	id int PRIMARY KEY NOT NULL AUTO_INCREMENT,
	email varchar(50) NOT NULL,
	password varchar(50) NOT NULL, 
	host varchar(50) NOT NULL, 
	port varchar(50) NOT NULL, 	
	is_ssl bit NOT NULL,   
	is_deleted bit NOT NULL,
	last_message_uid int NOT NULL	
);
Назначение полей: email - почтовый ящик password - пароль от ящика host - адрес почтового сервера. Например, imap.gmail.com или 173.194.71.108 port - порт, по которому работает почтовый сервер is_ssl - если флаг установлен, то подключение будет по SSL is_deleted - если флаг установлен, то почтовый ящик считается удаленным и не участвует в сборке писем last_message_uid - поле используется для хранения UID последнего считанного сообщения Заполним таблицу mailboxes данными трех почтовых ящиков, созданных на разных почтовых серверах: gmail, yandex и mail.ru. В качестве примера к почтовым ящикам на gmail и yandex будем подключаться через SSL, а на mail.ru - без SSL.
INSERT INTO mailboxes(email,password,host,port,is_ssl,is_deleted) VALUES
('mail.collector.kz@gmail.com','mail.collector','imap.gmail.com','993',1,0,0),
('mail.collector.kz@mail.ru','mail.collector','imap.mail.ru','143',0,0,0),
('mail.collector.kz@yandex.ru','mail.collector','imap.yandex.ru','993',1,0,0)
Таблица сообщений Создадим таблицу messages для хранения сообщений:
CREATE TABLE messages
(
	id int PRIMARY KEY NOT NULL AUTO_INCREMENT,
	mailbox_id int NOT NULL,
	uid int NOT NULL,
	subject varchar(255),
	body_text text,
	body_html text,
	attachment_count int,
	header text,
	message_date datetime,
	create_date datetime NOT NULL,
	modify_date datetime,	
	is_ready bit NOT NULL 	
);
Так же создадим внешний ключ на таблицу mailboxes:
ALTER TABLE messages ADD CONSTRAINT fk_messages_user_id 
FOREIGN KEY (mailbox_id) REFERENCES mailboxes(id); 
Назначение полей: mailbox_id - ссылка на почтовый ящик, к которому относится письмо uid - уникальный номер письма в почтовом ящике subject - тема письма body_text и body_html - письмо храниться на сервере в виде обычного текста и html-версии attachment_count - кол-во вложений письма header - технический заголовок письма message_date - дата письма create_date - дата создания записи modify_date - дата изменения записи is_ready - письмо полностью загружено Таблица адресов Создадим таблицу addresses для хранения адресов: адрес отправителя, адреса получателей, адреса получателей скрытой копии и т.д.
CREATE TABLE addresses
(
	id int PRIMARY KEY NOT NULL AUTO_INCREMENT,
	message_id int NOT NULL,
	type varchar(10) NOT NULL,
	email varchar(50) NOT NULL
);
И создадим внешний ключ для связи с таблицей messages:
ALTER TABLE addresses ADD CONSTRAINT fk_addresses_message_id 
FOREIGN KEY (message_id) REFERENCES messages(id); 
Назначение полей: message_id - ссылка на сообщение, к которому относится данный адрес type - тип адресата: "from" (отправитель), "to"(получатель), "cc" (скрытая копия) и т.д. email - адрес электронной почты Таблица вложений Создадим таблицу attachments для хранения информации о файлах прикрепленных к письму:
CREATE TABLE attachments
(
	id int PRIMARY KEY NOT NULL AUTO_INCREMENT,
	message_id int NOT NULL,
	file_name varchar(255) NOT NULL,
	mime_type varchar(255) NOT NULL,
	file_size int NOT NULL,
	location varchar(255) NOT NULL	
);
Создадим внешний ключ для связи с таблицей messages:
ALTER TABLE attachments ADD CONSTRAINT fk_attachments_message_id 
FOREIGN KEY (message_id) REFERENCES messages(id); 
Назначение полей: message_id - привязка к конкретному сообщению file_name - имя файла mime_type - тип данных: "application/zip", "application/pdf", "image/png" и т.д. file_size - размер файла в байтах location - расположение файла СКРИПТЫ Описание работы: первый скрипт подключается к почтовому серверу, записывает в базу данных UID-ы новых писем, а второй скрипт, который будет рассмотрен позднее, по UID писем закачивает содержимое писем с вложениями и другой информацией. Скрипт для чтения новых писем (reader.php)
uid;
			wr("add message $message_uid");
			
			//создаем запись в таблице messages,
			//тем самым поставив сообщение в очередь на загрузку
			$sql = "INSERT INTO messages(mailbox_id,uid,create_date,is_ready)
					VALUES($mailbox_id,$message_uid,now(),0)";
			mysql_query($sql) or die(mysql_error());
		}
		
		if($message_uid != - 1){
			wr("last message uid = $message_uid");
			
			//если появились новые сообщения, 
			//то сохраняем UID последнего сообщения
			$sql = "UPDATE mailboxes 
					SET last_message_uid = $message_uid
					WHERE id = $mailbox_id";
			mysql_query($sql) or die(mysql_error());
		}else{
			//нет новых сообщений
			wr("no new messages");
		}
	}
	
	mysql_query("COMMIT");//завершаем транзакцию

	//закрываем IMAP-поток
	imap_close($mail);
}
?>

Установка Apache, подключение PHP и MySQL

Первая статья в этом новом 2014 году 🙂 В очередной раз после переустановки Windows возникла необходимость в установке Apache с поддержкой PHP и MySQL. Установка Apache Скачаем последную версию Apache с официального сайта http://httpd.apache.org/download.cgi Найдите и скачайте MSI-инсталлятор: httpd-2.0.65-win32-x86-no_ssl.msi (Скачать). Если нужен Apache с OpenSQL, то выберите: httpd-2.0.65-win32-x86-openssl-0.9.8y.msi (Скачать) Запустите установщик httpd-2.0.65-win32-x86-no_ssl.msi В окне приветствия нажмите Next: 01 - welcome Согласившись с лицензионным соглашением, выберите пункт "I accept the terms in the license agreement" и нажмите Next: 02 - agree В следующем информационном окне просто нажмите Next: 03 - info Далее следует заполнить информацию о вашем сервере. Можно заполнить как в данном примере. Выберите первый рекомендуемый пункт и нажмите Next, это будет означать, что Apache будет установлен как сервис для всех пользователей и будет работать по 80-му порту: 04 - as service Выберите обычную установку (Typical) и нажмите Next: 05 - typical Измените директорию установки на диск C: и нажмите Next 06 - dest Всё готово для установки, нажмите Install: 07 - install После завершения процесса установки нажмите кнопку Finish: 08 - finish
Примечание: Процесс установки может завершиться некорректно из-за того, что 80 порт может быть занят другим приложением, и нужно будет выбрать другой порт. Например: 8421. Откроем конфигурационный файл: C:\Apache2\conf\httpd.conf Заменим строку Listen 80 на Listen 8421. Запустим установщик httpd-2.0.65-win32-x86-no_ssl.msi заново. В окне приветствия нажмите Next. В следующем окне выберите Repair и нажмите Next: 09 - repair Всего готово к починке, нажав кнопку Install, Apache должен починиться.
Если установка прошла успешно, все равно рекомендую поменять 80-ый порт на другой, чтобы не было конфликтов ни сейчас, ни в будущем. Как было описано ранее нужно в файле C:\Apache2\conf\httpd.conf заменить строку Listen 80 на Listen 8421. Остается перезапустить службу Apache2 и наш веб-сервер доступен по 8421-му порту. В браузере введите адрес: http://localhost:8421 Должна появиться стартовая страница Apache: 10 - ready Подключение PHP C официального сайта http://windows.php.net/download/ скачайте zip-архив с PHP или скачайте здесь. Если у вас 64-битная Windows, то можете выбрать x64 (лучшая производительность) или x86. Если у вас 32-битная Windows, то вам подойдет только x86. Распакуйте zip-архив php-5.3.5-Win32-VC6-x86.zip в папку C:\Php Откройте конфигурационный файл C:\Apache2\conf\httpd.conf И в начало этого файла добавить следующие строки:
PHPIniDir "C:/Php/"  
LoadModule php5_module C:/Php/php5apache2.dll
AddType application/x-httpd-php phtml php 
В строке LoadModule php5_module C:/Php/php5apache2.dll вам нужно будет указать вашу версию библиотеки. Будьте внимательны! Указывайте библиотеку, соответствующую вашей версии Apache. В нашем случае версия Apache 2.0, поэтому мы указали php5apache2.dll. Для версии 2.2 - php5apache2_2.dll, для версия 2.4 - php5apache2_4.dll и т.д. Если укажите не ту версию, то Apache не запуститься, а в логах будет примерно такая ошибка:
Syntax error on line 2 of C:/Apache2/conf/httpd.conf
В httpd.conf найдите строку:
DirectoryIndex index.html index.html.var
Замените ее на:
DirectoryIndex index.php index.html index.html.var
В DirectoryIndex определяется порядок файлов по умолчанию. Теперь приступим к настройке самого PHP. Найстройки PHP храняться в файле C:\Php\php.ini, этого файла пока нет, но есть два других
  • php.ini-development (для разработки)
  • php.ini-production (для промышленного использования)
Нас устраивает php.ini-development, поэтому просто переименуем его в php.ini Теперь необходимо в файле php.ini включить расширения mysql и mysqli. Для этого найдите строки:
;extension=php_mysql.dll
;extension=php_mysqli.dll
Где строчки помеченные символом ; отключены Для включения расширений php_mysql.dll и php_mysqli.dll нужно убрать ;
extension=php_mysql.dll
extension=php_mysqli.dll
Также найдите строку:
; extension_dir = "ext"
Замените её на следующую:
extension_dir = "c:/php/ext"
MySQLi является обновленной версией драйвера PHP MySQL, и дает различные улучшения в работе с базами данных. Также поддержка сокращенного тега <? по умолчанию отключена, и вам нужно использовать полную версию тега <?php Для это пропишите настройку так:
short_open_tag = On
Сохраните сделанные изменения и перегрузите Apache.