Загрузка фотографий на сайт c помощью электронной почты

    Это мой первый пост на Хабре, по этому не судите строго.

    Задача.


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

    Алгоритм


    Пользователь отправляет письмо с фотографиями на адрес типа userXXX_eventYYY@mysite.com, где eventYYY — ID события; userXXX — ID пользователя. Такого e-mail адреса НЕ СУЩЕСТВУЕТ. По этому все письма отправленные на несуществующие адреса перенаправляем на image_upload@mysite.com. Потом, при считывании почты с этого адреса, парсим заголовки и узнаем на какой адрес изначально было отправлено письмо. Распарсив полученный адрес, узнаем КУДА складывать файлы и кто их залил.

    Использование PEAR


    Для работы с POP3 сервером использовалась библиотека PEAR, в частности класс Net_POP3, который находится в файле path/to/pear/Net/POP3.php. Для его успешной работы необходим класс Net_Socket (path/to/pear/Net/Socket.php) и, собственно, PEAR.php. Все эти файлы находятся на сайте http://pear.php.net и доступны для скачивания.
    Если PEAR у вас не установлена — просто скопируйте POP3.php и Socket.php в вашу папку с библиотеками и перепропишите пути в них. Файл PEAR.php есть в папке path/to/pear/.

    Дополнительные функции


    Для обработки прикрепленных файлов мне понадобились еще несколько функций. Не буду приписывать себе их авторство. Они взяты с сайта http://webi.ru/webi_articles/6_12_f.html и немного переделаны (совсем немного).
    В моем коде они выглядят так:
    /* START FUNCTIONS BLOCK */
    // Функция для выдергивания метки boundary из заголовка Content-Type
    function get_boundary($ctype){
     if(preg_match('/boundary[ ]?=[ ]?(["]?.*)/i',$ctype,$regs)) {
      $boundary = preg_replace('/^\"(.*)\"$/', "\\1", $regs[1]);
      return trim("--$boundary");
     }
    }

    // если письмо будет состоять из нескольких частей (текст, файлы и т.д.)
    // то эта функция разобьет такое письмо на части (в массив), согласно разделителю boundary
    function split_parts($boundary,$body) {
     $startpos = strpos($body,$boundary)+strlen($boundary)+2;
     $lenbody = strpos($body,"\r\n$boundary--") - $startpos;
     $body = substr($body,$startpos,$lenbody);
     return explode($boundary."\r\n",$body);
    }

    // Эта функция отделяет заголовки от тела и возвращает массив с заголовками и телом 
    function fetch_structure($email) {
     $ARemail = Array();
     $separador = "\r\n\r\n";
     $header = trim(substr($email,0,strpos($email,$separador)));
     $bodypos = strlen($header)+strlen($separador);
     $body = substr($email,$bodypos,strlen($email)-$bodypos);
     $ARemail["header"] = $header;
     $ARemail["body"] = $body;
     return $ARemail;
    }

    // разбирает все заголовки и выводит массив, в котором каждый элемент является соответсвующим заголовком
    function decode_header($header) {
     $headers = explode("\r\n",$header);
     $decodedheaders = Array();
     foreach($headers as $header_item){
      $thisheader = trim($header_item);
      if(!empty($thisheader))
      {
       if(!ereg("^[A-Z0-9a-z_-]+:",$thisheader))
        $decodedheaders[$lasthead] .= " $thisheader";
       else {
        $dbpoint = strpos($thisheader,":");
        $headname = strtolower(substr($thisheader,0,$dbpoint));
        $headvalue = trim(substr($thisheader,$dbpoint+1));
        if($decodedheaders[$headname] != "")
         $decodedheaders[$headname] .= "; $headvalue";
        else
         $decodedheaders[$headname] = $headvalue;
        $lasthead = $headname;
       }
      }
     }
     return $decodedheaders;
    }

    // перекодировщик тела письма.
    // Само письмо может быть закодировано и данная функция приводит тело письма в нормальный вид.
    // Так же и вложенные файлы будут перекодироваться этой функцией.
    function compile_body($body,$enctype,$ctype) {
     $enctype = explode(" ",$enctype); $enctype = $enctype[0];
     if(strtolower($enctype) == "base64")
     $body = base64_decode($body);
     elseif(strtolower($enctype) == "quoted-printable")
     $body = quoted_printable_decode($body);
     if(ereg("koi8", $ctype)) $body = convert_cyr_string($body, "k", "w");
     return $body;
    }
    /* END FUNCTIONS BLOCK */


    * This source code was highlighted with Source Code Highlighter.


    Основной код (email_upload.php)


    Теперь, когда все готово, пришло время писать скрипт для получения писем и их обработки.
    Для начала, создадим объект $pop3 и подсоединимся к серверу.

    $user='username';
    $pass='secure';
    $host='mysite.com';
    $port="110";

    // Создание объекта
    $pop3 =& new Net_POP3();

    // Соединение с сервером
    if(PEAR::isError( $ret= $pop3->connect($host , $port ) )){
      echo "ERROR: " . $ret->getMessage() . "\n";
      exit();
    }

    // Авторизация на сервере
    if(PEAR::isError( $ret= $pop3->login($user , $pass,'USER' ) )){
      echo "ERROR: " . $ret->getMessage() . "\n";
      exit();
    }


    * This source code was highlighted with Source Code Highlighter.


    Теперь все готово для получения списка писем и их обработки. Это и делаем

    $message_list=$pop3->getListing(); // Получаем массив писем.

    * This source code was highlighted with Source Code Highlighter.


    Пройдемся по всем письмам в цикле:

    foreach($message_list as $message){
     $filenames[] = array();
     /* START GET PARSED HEADERS */
     // Получаем ИД текущено сообщения
     $message_id = $message['msg_id'];
     // Парсим заголовки
     $headers = $pop3->getParsedHeaders($message_id);
     
     $type = $ctype = $headers['Content-Type'];
     $ctype = split(";",$ctype);
     $types = split("/",$ctype[0]);
     $maintype = trim(strtolower($types[0]));
     $subtype = trim(strtolower($types[1]));
     
     /* END GET PARSED HEADERS */
     /* START CREATE FILE LOCATION AND SQL DATA*/
     
     // Получили данные с заголовка
     $from_user_info = $headers['Delivered-To'];
     
     // Далее получаем нужные АйДишники
     preg_match('/(user[0-9]+)_(event[0-9]+)@mysite.com/', $from_user_info, $matches);
     $user_id = str_replace("user","", $matches[1]);
     $event_id = str_replace("event","", $matches[2]);
     
     // Путь куда поместим файл
     $file_location = "/path/to/upload";
     
     // Помещаем данные для SQL запроса
     $table_data = array(
      'user_id' => $user_id,
      'event_id' => $event_id
     );
     
     /* END CREATE FILE LOCATION AND SQL DATA*/
     
     /* START GET BODY */ 
     // Получаем тело сообщения
     $message_text = htmlspecialchars($pop3->getBody($message_id));
     
     // Проверяем его тип (на содержание прикрепленных файлов)
     if($maintype=="multipart" && ereg($subtype,"signed,mixed,related")) // Если есть
     {
      // получаем метку-разделитель частей письма
      $boundary=get_boundary($headers['Content-Type']);
     
      // на основе этого разделителя разбиваем письмо на части
      $part = split_parts($boundary,$message_text);
      
      //Ищем файлы
      foreach($part as $part_item){
       // разбиваем текущую часть на тело и заголовки   
       $email = fetch_structure($part_item);
       $header = $email["header"];
       $body = $email["body"];
       
       // разбираем заголовки на массив
       $headers = decode_header($header);
       $ctype = $headers["Content-Type"];
       $cid = $headers["content-id"];
       $Actype = split(";",$ctype);
       $types = split("/",$Actype[0]);
       $rctype = strtolower($Actype[0]);
       
       // теперь проверяем, является ли эта часть прикрепленным файлом
       $is_download = (ereg("name=",$headers["content-disposition"].$headers["content-type"]) || $headers["X-Attachment-Id"] != "" || $rctype == "message/rfc822");
     
       if($is_download) {
        // Имя файла можно выдернуть из заголовков Content-Type или Content-Disposition
        $cdisp = $headers["content-disposition"];
        $ctype = $headers["content-type"];
        $ctype2 = explode(";",$ctype);
        $ctype2 = $ctype2[0];
        $Atype = split("/",$ctype);
        $Acdisp = split(";",$cdisp);
        $fname = $Acdisp[1];
        if(ereg("filename=\"(.*)\"",$fname,$regs))
         $filename = $regs[1];
        if($filename == "" && ereg("name=(.*)",$ctype,$regs))
         $filename = $regs[1];
        $filename = ereg_replace("\"(.*)\"","\\1",$filename);
        $filename = ereg_replace(""(.*)"","\\1",$filename);
      
        //читаем файл в переменную.
        $body = compile_body($body,$headers["content-transfer-encoding"],$ctype);
       
        // Указываем КУДА записать файл
        $filename = $file_location.trim($filename);
        
        // Формируем список файлов для записи в базу
        $filenames[] = $filename;    
        // Собственно сохраняем
        $ft=fopen($filename,"wb");
        fwrite($ft,$body);
        fclose($ft);
       }
      }
     }
     
     // НА основе $filenames[] и $table_data создаем запросы и выполняем их.
     $query = "INSERT INTO event_foto(event_id, user_id, image_name) VALUES ";
     $total_fotos = count($filenames);
     $current = 1;
     foreach($filenames as $file){
      $query .= "('$event_id','$user_id','$file')";
      if($total_fotos>$current)
       $query .= ", ";
      $current++;
     }
     mysql_query($query);
     //И не забываем удалить текущее письмо
     $pop3->deleteMsg($message_id);
    }


    * This source code was highlighted with Source Code Highlighter.


    Конец


    Собственно все. Модуль работает. Осталось только повесить этот файлик на CRON (например, каждый час).
    Спасибо на внимание. Желаю удачи!
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 26

      +1
      Метод интересный. Для полноты ещё можно привести примеры, когда может возникнуть необходимость закачки изображений через почту. А там глядишь и какое-нибудь неожиданное/нестандартное применение может найти техника…
        0
        Простите, а где стояла такая задача?

        И по теме — а может лучше:
        mailto:user@site.com

        Message content:
        Мой отпуск (это может быть топик в блоге, раздел в галерее)
        Как я весело провел отпуск (кратенький дескрипшн)

        и фотки в аттачах. Дескрипшн соотв. необязательный параметр.
          0
          Ну и по последним трендам ссылку в твиттер чтоб кидало.

          А как применение может быть — MMS -> Email -> Блог/ Галерея…
            0
            Да. Но пока сделали только так
            0
            Такая задача стояла у меня (не буду называть компанию).
            Например, пользователь на сайте становится участником некоторого события (концерта). И прямо с концерта он со своего КПК или смартфона отправляет фотки в фотоленту события.

            На данном этапе можно отправить только фотки. Подписи к ним — вопрос будущего.
              0
              Подпись к картинке пихать в тему письма.
                0
                Да. Согласен. А если картинка не одна?
                К стати, на сайте в фотоленте фотка не подписывается. Только видно КТО ее запостил.
              0
              Например, в фейсбуке можно заливать фотки в альбом таким же способом. Мне кажется очень удобно, ведь почти у всех в телефоне есть камера и gprs, сфоткал -> отправил.
              0
              «Для решение такой задачи я решил воспользовался» — Как то не звучит. Исправь.
                0
                Спасибо. Исправил.
                +2
                мисье знает толк в извращеньях
                  0
                  Если вариант получше? С удовольствием послушаю!
                    0
                    ищем в гугле qpsmtpd, читаем доки и пишем свой плагин который делает все нужные проверки не закрывая smtp сессии и постобработку по quit.
                    у меня есть отличный пример на эту тему, может как нибудь напишу.
                  0
                  А если я злобный хакер? Как проверяется, что почта точно отправлена откуда надо?
                    0
                    Этот вопрос частично решает валидация пользователя (по ИД, e-mail). Но все еще не совсем совершенная схема. Проверку пользователя в статью не включил.
                      0
                      Но именно этот вопрос меня озадачил большего всего:)
                        0
                        Валидация через e-mail не особо хорошо работает :) слишком уж просто её обойти… хотя, например, для внутрикорпоративных решений вполне подойдёт. Интересно было бы узнать всё-таки как решилась проблема с защитой :)
                          0
                          Пока остановились на варианте поменять формат e-mail (предложил шеф).

                          Отсылать фотографии на email вида [GUID]@mysite.com, где [GUID] генерируется уникально для каждой пары «пользователь+события» и он (пользователь) копирует / получает этот адрес для отправки после подписки на мероприятие.
                      +2
                      for($i=0;$i<count($headers);$i++) {
                      for($i=0;$i<count($part);$i++) {
                      Во-первых, count() внутри for() — плохо, т.к. при каждой интерации будет вычисляться размер массива. Во-вторых, в данных случаях лучше использовать foreach, никакой нужды в for нет.

                      Зачем вам использовать разные функции для работы в регулярными выражениями (ereg* и preg*)? Функции ereg* в настоящее время объявлены устаревшими (в PHP 5.3), а в версии 6 их вообще не будет. Следовательно, даже в PHP 5.2 их использование не приветствуется. Нужно переходить на PCRE (функции preg*).

                      Upload фотографий на сайт
                      Реализовать возможность upload фотографий в профайл
                      Чем вам простое слово «загрузка» не угодило?

                      Такого e-mail адреса НЕСУЩЕСТВУЕТ
                      Частица не с глаголами пишется отдельно, этому еще в начальной школе учат.

                      Ну и с пунктуацией у вас просто беда, не очень приятно читать.
                        0
                        Итерации, конечно, а не интерации, глупая опечатка.
                          0
                          Спасибо! Кое-что исправил.
                        +1
                        мощный велосипед
                        тока колёс многовато
                          –1
                          Спасибо, автор
                            0
                            Если почта расположена на вашем VDS, или просто есть доступ к /etc/aliases, то можно не лазать по каждый раз по POP3, а прописать скрипт как фильтр для ящика:
                            image_upload: |/path/to/script

                            В итоге все письма будем получать на stdin скрипта.
                              0
                              не безопасно, с точки зрения атаки…
                              лучше своеим чередом без воздействия снаружи выгребать из очереди (ящика) письма в порядке их поступления и обрабатывать во столько потоков, сколько ваш сервер физически может вытянуть.
                              0
                              Из каких соображений решили отправителя и событие указывать в виде несуществующего адреса? Зачем этот лишний этап с разруливанием несуществующих адресов? Почему не указывать отправителя и событие просто в теме письма?

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

                              Самое читаемое