Pobranie listy email z Gmail używając PHP i IMAP

gmail

Jeżeli czujecie potrzebę (na pewno ją czujecie!) pobierania swoich emaili ze swojej skrzynki Gmail z wykorzystaniem IMAP, to mam coś dla Was!

Poniżej znajdziecie (naprawdę) prostą  funkcję, która pozwala pobierać wiadomości od wujka Google. Pamiętajcie jednak, że wykorzystuje ona IMAP i konieczne jest zainstalowanie rozszerzenia php_imap oraz włączenia go w swoim php.ini, a następnie zrestartowanie serwera.

function loadGmailMails($from = 0, $to = 0) {
    $hostname = '{imap.gmail.com:993/imap/ssl}INBOX';
    $username = 'USERNAME';
    $password = 'PASSWORD';

    $inbox = imap_open($hostname, $username, $password) or die('Cannot connect to Gmail: '.imap_last_error());
    $e = imap_search($inbox, 'ALL');
    $emails = array();
    if ($e) {
        rsort($e); // najnowsze na gorze

        if ($to == 0) {
			$to = sizeof($e);
		}

        for ($i = $from; $i < $to; $i++) {
            $overview = imap_fetch_overview($inbox, $e[$i], 0); // pobieramy naglowek
            $message = imap_fetchbody($inbox, $e[$i], 1); // pobieramy tresc

            preg_match('/(?P<name>[a-zA-Z ]+)<(?P<address>.+)>/', $overview[0]->from, $match); // wyciagamy nadawce oraz jego email
            $name = isset($match['name']) ? trim($match['name']) : '';
			$address = isset($match['address']) ? trim($match['address']) : '';

			// tworzymy tablice z danymi konkretnego emaila
            $emails[] = array(
				'read' => $overview[0]->seen,
				'subject' => $overview[0]->subject,
				'from' => array(
					'name' => $name,
					'address' => $address
				),
				'date' => $overview[0]->date, 
				'message' => $message
			);
        }
    }
    imap_close($inbox);

    return $emails;
}

Autoryzacja API – HMAC PHP

Autoryzacja API

Autoryzacja REST API jest czymś nieco innym niżeli autoryzacja użytkownika na stronie internetowej. Mechanizm  HTTP Basic Auth także tutaj nie pasuje. W przypadku naszego API nie tworzy się żadna sesja, która pozwalałaby na identyfikację konkretnego użytkownika / klienta korzystającego z dostępnych metod. Wykorzystujemy przecież protokół bezstanowy REST.

W minionym tygodniu zmierzyłem się z podobnym problemem w przypadku API, które piszę na potrzeby aplikacji mobilnej. Rozważane były tak naprawdę dwa rozwiązania: OAuth oraz wykorzystanie kluczy publicznych i prywatnych. Otwarty protokół OAuth wydawałby się idealnym rozwiązaniem. Okazuje się jednak, że jest kilka ALE, które komplikują jego implementację:

  • brak standaryzacji – Facebook i Twitter implementują w swoich API OAuth, ale jedno i drugie rozwiązanie różni się od siebie
  • skomplikowana implementacja po stronie serwera oraz aplikacji
  • spory narzut wydajnościowy

Finalne rozwiązanie oparte zostało o zasadę kluczy publicznych i prywatnych, ale zostało ono dostosowane do wymogów aplikacji mobilnej oraz możliwości jakie daje wewnętrzny framework. Chciałbym Wam dzisiaj jednak pokazać rozwiązanie oparte o metodę HMAC, która została zaimplementowana w języku PHP (>= 5.1.2) pod postacią funkcji hash_hmac.

Działanie HMAC

  1. stworzenie tak zwanego content hash przy użyciu private key, który znany jest tylko użytkownikowi (oraz systemowi)
  2. content hash przesyłany jest na serwer razem z public key
  3. serwer odczytuje public key i na jego podstawie szuka użytkownika, który pasuje do owego klucza , a następnie pobiera jego private key
  4. serwer używa pobranego private key do wykonania funkcji hash_hmac, która hashuje treść przesłaną w żądaniu HTTP i porównuje ją z content hash

Klucz publiczny i prywatny

By wygenerować public key i private key można posłużyć się kilkoma metodami. Poniżej pokażę dwie, chyba najprostsze, a zarazem skuteczne sposoby.

  1. Wykorzystanie OpenSSL
    $hash = hash('sha256', openssl_random_pseudo_bytes(32));

    OpenSSL nie jest domyślnym rozszerzeniem w PHP, dlatego też poniższa metoda powinna zastąpić wykorzystanie tej biblioteki.

  2. Wykorzystanie funkcji mt_rand:
    $hash = hash('sha256', mt_rand());

Oba przypadki jako funkcję hashującą wykosztują sha256, której obce są kolizje znane chociażby z funkcji skrótu jaką jest MD5.

Przykład wykorzystania HMAC w PHP

Czas na zabawę, czyli przejście do praktyki. Poniżej kod klienta i serwera, który może posłużyć jako przykład REST API.

Przykład wykorzystuje mechanizm cURL, który dostępny jest raczej w większości instalacji PHP. Zawsze można jednak użyć bardziej niskopoziomowej implementacji z socketami.

Kod klienta

<?php
	$publicKey  = '3441df0babc2a2dda551d7cd39fb235bc4e09cd1e4556bf261bb49188f548348';
	$privateKey = 'e249c439ed7697df2a4b045d97d4b9b7e1854c3ff8dd668c779013653913572e';
	$content    = json_encode(array(
		'test' => 'content'
	));

	$hash = hash_hmac('sha256', $content, $privateKey);

	$headers = array(
		'X-Public: '.$publicKey,
		'X-Hash: '.$hash
	);

	$ch = curl_init('http://test.localhost:8080/api-test/');
	curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
	curl_setopt($ch, CURLOPT_POSTFIELDS, $content);

	$result = curl_exec($ch);
	curl_close($ch);

	echo "RESULT\\n======\\n".print_r($result, true)."\\n\\n";
?>

Kod serwera

Po stronie serwera wykorzystam Slim Framework, który jest tak naprawdę mikroframeworkiem. Idealnie nadaje się on do małych aplikacji, które nie wymagają super mocy. Instalujemy Slim poprzez dodanie do swojego composer.json poniższych linijek:

{
    "require": {
        "slim/slim":"2.*"
    }
}

Composer zainstaluje nam cały framework w katalogu vendor/.

Więcej o Composer znajdziesz na: getcomposer.org

Nasz klient przesyłał do serwera nagłówki X-Public oraz X-Hash, które musimy pobrać po stronie serwera. Private key jest oczywiście hard-coded bezpośrednio w kodzie. W rzeczywistym systemie należałoby odpytać bazę danych i pobrać taką wartość z obiektu użytkownika.

<?php
require_once 'vendor/autoload.php';

$app = new \\Slim\\Slim();
$app->post('/', function() use ($app) {

    $request = $app->request();

    $publicHash  = $request->headers('X-Public');
    $contentHash = $request->headers('X-Hash');
    $privateKey  = 'e249c439ed7697df2a4b045d97d4b9b7e1854c3ff8dd668c779013653913572e';
    $content     = $request->getBody();

    $hash = hash_hmac('sha256', $content, $privateKey);

    if ($hash == $contentHash){
        echo "match!\n";
    }
});
?>

PS. Staram się by wpis miał przyjemną formę i był łatwy do czytania. Udało mi się? Okaże się tym samym kto dotarł do końca.

PHP i określenie wielkości pliku / katalogu

bytes

W moim ostatnim wpisie pisałem o operacjach na plikach i katalogach w języku PHP. Opisałem kilka funkcji, w tym jedną, która pozwalała na określenie rozmiaru pliku lub całego katalogu.

Chciałbym ją nieco rozwinąć i pokazać bardziej rozbudowane użycie, które pozwoli w ludzki sposób formatować ilość bajtów otrzymanych w ramach obliczeń.

Przypomnienie funkcji – rozmiar pliku lub katalogu

/**
 * Rozmiar pliku lub katalogu
 *
 * UWAGA: Na systemach 32bitowych mozemy otrzymac niespodziewane wyniki dla plikow majacych rozmiar ponad 2GB
 *
 * @param string $path sciezka do pliku lub katalogu
 * @return int rozmiar (w bajtach)
 */
function sizeRecursive($path) {
    $size = 0;
    if (is_dir($path)) {
        $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path));

        foreach ($iterator as $file) {
            $size += $file->getSize();
        }
    } else {
        $size = filesize($path);
    }

    return $size;
}

W ramach rozwinięcia powyższego, zbuduję klasę, która pozwoli mi zrealizować formatowanie zwracanych danych.

/**
 * Klasa do zarzadzania plikami / katalogami w systemie plikow
 */
class FileManagment {

    /**
     * Tablica jednostek wielkosci
     *
     * @var array
     */
    private static $units = array('B', 'KB', 'MB', 'GB', 'TB');

    /**
     * Funkcja automatycznie obliczajaca najbardziej odpowiednia jednostke 
     * oraz ograniczajaca liczbe miejsc po przecinku do wskazanej w parametrze $precision ilosci
     * 
     * @param string $path          sciezka do pliku lub katalogu
     * @param integer $precision    precyzja podawanego wyniku (po przecinku)
     * @return mixed                false - gdy niepoprawny parametr $precision
     *                              string - wynik obliczen
     */
    public static function autoSize($path, $precision = 2) {
        if (!is_numeric($precision)) {
            return false;
        }
        $bytes = self::sizeRecursive(trim($path));
        $bytes = max($bytes, 0);
        $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
        $pow = min($pow, count(self::$units) - 1);
        $bytes /= pow(1024, $pow);
        $retValue = round($bytes, $precision).' '.self::$units[$pow];

        return $retValue;
    }

    /**
     * Funkcja obliczajaca rozmiar wedle konkretnie zadanej jednostki
     * 
     * @param string $path          sciezka do pliku lub katalogu
     * @param string $unitType      jednostka w jakiej zwrocony ma byc wynik
     * @param integer $precision    precyzja podawanego wyniku (po przecinku)
     * @return mixed                false - gdy niepoprawny parametr $precision
     *                              string - wynik obliczen
     */
    public static function sizeWithOption($path, $unitType = 'KB', $precision = 2) {
        if (!is_numeric($precision)) {
            return false;
        }
        $bytes = self::sizeRecursive(trim($path));
        switch($unitType) {
            case self::$units[0]: 
                $size = number_format($bytes, $precision) ; 
                break;
            case self::$units[1]: 
                $size = number_format(($bytes / 1024), $precision) ; 
                break;
            case self::$units[2]: 
                $size = number_format(($bytes / 1024 / 1024), $precision) ; 
                break;
            case self::$units[3]: 
                $size = number_format(($bytes / 1024 / 1024 / 1024), $precision) ; 
                break;
            case self::$units[4]: 
                $size = number_format(($bytes / 1024 / 1024 / 1024 / 1024), $precision) ; 
                break;
        }
        $retValue = $size.' '.$unitType;

        return $retValue;
    }

    /**
     * Rozmiar pliku lub katalogu
     *
     * UWAGA: Na systemach 32bitowych mozemy otrzymac niespodziewane wyniki dla plikow majacych rozmiar ponad 2GB
     *
     * @param string $path          sciezka do pliku lub katalogu
     * @return int                  rozmiar (w bajtach)
     */
    private static function sizeRecursive($path) {
        $size = 0;
        if (is_dir($path)) {
            $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path));

            foreach ($iterator as $file) {
                $size += $file->getSize();
            }
        } elseif (is_file($path)) {
            $size = filesize($path);
        }

        return $size;
    }

}

Przykład użycia

require_once('FileManagment.php');
$fileManagment = new FileManagment();

echo 'autoSize: '.$fileManagment::autoSize('Zdjecia');
// autoSize: 41.89 MB
echo 'sizeWithOption: '.$fileManagment::sizeWithOption('Zdjecia', 'GB', 4);
// sizeWithOption: 0.0409 GB

Mam nadzieję, że przykład ten jest na tyle jasny, że nie wymaga on dalszego omawiania. Jeżeli jednak pojawią się jakieś pytania, to jak zawsze odpowiem w komentarzach poniżej.

Operowanie na katalogach i plikach – kilka przydatnych funkcji w PHP

File-management

Są w PHP takie rzeczy, które jeśli raz już ktoś gdzieś napisał, to później już nikt tego nie dotyka. Wierzcie mi lub nie, ale sam napisałem kilka takich klas / funkcji, do których nie chciałbym wracać. Oczywiście nie dlatego, że zostały źle napisane, a z prostego powodu – pisze się to raz, testuje oraz wykorzystuje i nie pamięta jakich funkcji konkretnie trzeba było użyć.

Przykładem takich funkcji, które wpisują się w pierwsze zdanie dzisiejszego artykułu są operacje na pojedynczych plikach oraz całym systemie plików. Postaram się Wam dzisiaj pokazać kilka ciekawych (tak myślę!) operacji, które pozwolą stworzyć sobie małą bibliotekę do zarządzania plikami lub katalogami.

Usuniecie pojedynczego pliku / rekursywne usuniecie całego katalogu

/**
 * Usuniecie pliku / rekursywne usuniecie calego katalogu
 *
 * @param string $path sciezka do pliku / katalogu do usuniecia
 * @return void
 */
function deleteRecursive($path) {
    if (is_dir($path)) {
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS),
            RecursiveIteratorIterator::CHILD_FIRST
        );

        foreach ($iterator as $file) {
            if ($file->isDir()) {
                rmdir($file->getPathname());
            } else {
                unlink($file->getPathname());
            }
        }

        rmdir($path);
    } else {
        unlink($path);
    }
}

Kopiowanie pojedynczego pliku / rekursywne kopiowanie katalogu

/**
 * Kopiowanie pojedynczego pliku / rekursywne kopiowanie katalogu
 *
 * @param string $source    sciezka do zrodla
 * @param string $dest      sciezka przeznaczenia
 * @return void
 */
function copyRecursive($source, $dest) {
    if (is_dir($source)) {
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($source, RecursiveDirectoryIterator::SKIP_DOTS), 
            RecursiveIteratorIterator::SELF_FIRST
        );

        foreach ($iterator as $file) {
            if ($file->isDir()) {
                mkdir($dest . DIRECTORY_SEPARATOR . $iterator->getSubPathName());
            } else {
                copy($file, $dest . DIRECTORY_SEPARATOR . $iterator->getSubPathName());
            }
        }
    } else {
        copy($source, $dest);
    }
}

Rozmiar pliku lub katalogu

/**
 * Rozmiar pliku lub katalogu
 *
 * UWAGA: Na systemach 32bitowych mozemy otrzymac niespodziewane wyniki dla plikow majacych rozmiar ponad 2GB
 *
 * @param string $path sciezka do pliku lub katalogu
 * @return int rozmiaru (w bajtach)
 */
function sizeRecursive($path) {
    $size = 0;
    if (is_dir($path)) {
        $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path));

        foreach ($iterator as $file) {
            $size += $file->getSize();
        }
    } else {
        $size = filesize($path);
    }

    return $size;
}

Funkcja tail (*unix) – odczytanie kilku (ostatnich) linii z pliku

/**
 * Funkcja tail (*unix) - odczytanie kilku (ostatnich) linii z pliku
 *
 * UWAGA: Ponizsza funkcja dziala dla plikow majacych zakonczenie linii rowne CRLF, LF lub CR
 *
 * @param string $file  sciezka do pliku
 * @param int $lines    ilosc linii do zwrocenia przez funkcje
 * @return string
 */
function tail($file, $lines) {
    if ($lines < 1) {
        return '';
    }

    $line = '';
    $line_count = 0;
    $prev_char = '';
    $fp = fopen($file, 'r');
    $cursor = -1;

    fseek($fp, $cursor, SEEK_END);
    $char = fgetc($fp);

    while ($char !== false) {
        if ($char === "\\n" || $char === "\\r") {
            fseek($fp, --$cursor, SEEK_END);
            $next_char = fgetc($fp);

            if ($char === "\\n" && $next_char === "\\r") {
                $line_count++;
            } elseif ($char === "\\r" && $prev_char !== "\\n") {
                $line_count++;
            } elseif ($char === "\\n") {
                $line_count++;
            }

            fseek($fp, ++$cursor, SEEK_END);
        }

        if ($line_count == $lines) {
            break;
        }

        $line = $char . $line;
        $prev_char = $char;
        fseek($fp, --$cursor, SEEK_END);
        $char = fgetc($fp);
    }

    fclose($fp);

    return $line;
}

Dane geograficzne na podstawie adresu IP w PHP

geolocation

Dzisiejszym wpisem chciałbym Wam pokazać, w jaki sposób uzyskać można dane geograficzne na podstawie adresu IP.

Wykorzystam w tym celu serwis locatorhq.com, który udostępnia API pozwalające na identyfikację danych geograficznych.  Usługa jest darmowa i może z niej skorzystać każdy po założeniu konta.

Początek

Pierwszym krokiem jest rejestracja w serwisie: http://www.locatorhq.com/ip-to-location-api/signup.php poprzez podanie kilku podstawowych danych. Po chwili – na podany adres email przyjdzie wiadomość, której zawarte będą dane – API_KEY i nazwa użytkownika, które będą potrzebne podczas wysyłania zapytań do API.

Użycie

Jeżeli posiadasz już swój API_KEY i nazwę użytkownika, to jesteś w stanie wykonać swoje pierwsze zapytanie do API serwisu.

http://api.locatorhq.com/?user={YOURUSERNAME}
  &key={YOURAPIKEY}&ip={IPADDRESSTOLOOKUP}

, gdzie podać musimy następujące parametry:

  • YOURUSERNAME – nazwa użytkownika
  • YOURAPIKEY – API_KEY otrzymane w email z serwisu po rejestracji
  • IPADDRESSTOLOOKUP – adres IP, który chcemy “sprawdzić”

Istnieje jeszcze 4 parametr, a mianowicie format danych, który ma nam zostać zwrócony po wykonaniu zapytania.

  • &format=text
  • &format=xml
  • &format=json – dostępne wkrótce

Domyślna wartość parametru to: text.

Przykład

Czas na prosty przykład wykorzystania. Sprawdzę w ten sposób adres IP jednego z serwerów Google: 74.125.236.196.

Wykonując:

http://api.locatorhq.com/?user=Username&key=APIKEY&ip=74.125.236.196

otrzymamy taką oto odpowiedź:

US,United States,California,Mountain View,34.305,-86.2981
Kod krajuUS
KrajUnited States
StanCalifornia
MiejscowośćMountain View
Szerokość geograficzna34.305
Długość geograficzna-86.2981 

Parsowanie surowego tekstu nie jest zbyt wygodne, dlatego też wykorzystam format XML do pobrania danych i sparsowania ich po stronie kodu PHP.

Url zapytania będzie miał teraz postać:

http://api.locatorhq.com/?user=Username&key=APIKEY
  &ip=74.125.236.196&format=xml

Otrzymamy w ten sposób odpowiedź o treści:

<ip2locationapi>
  <countryCode>US</countryCode>
  <countryName>United States</countryName>
  <region>California</region>
  <city>Mountain View</city>
  <lattitude>34.305</lattitude
  <longitude>-86.2981</longitude>
</ip2locationapi>

Mając już strukturę XML mogę przejść do właściwego kodu, który będzie w stanie pobrać i sparsować wszystkie dane.

<?php

$ipAddress = $_SERVER['REMOTE_ADDR']; // adres IP
$user = MYUSERNAME;
$apiKey = MYAPIKEY;
$locationUrl = "http://api.locatorhq.com/?user={$user}&key={$apiKey}&ip={$ipAddress}&format=xml";

// pobieramy xml zwrocony przez API LocatorHQ
$xml = simplexml_load_file($locationUrl); 

$countryCode = $xml->countryCode; // kod kraju
$countryName = $xml->countryName; // kraj
$region = $xml->region;           // stan
$city = $xml->city;               // miejscowosc
$lattitude = $xml->lattitude;     // szerokosc geograficzna
$longitude = $xml->longitude;     // dlugosc geograficzna

?>

Już od Was zależy co zrobicie z tym dalej. Możliwości jest jednak wiele.

  • personalizacja naszej strony pod konkretny język
  • analiza wejść na naszą stronę – analiza demograficzna
  • geotargetowanie reklam
  • automatyczne wypełnianie formularzy na podstawie danych geograficznych

To tylko kilka przykładów.

Sama usługa LocatorHQ nie jest idealna. Czasami zwraca ona niepoprawne wyniki. Wszystko zależy tak naprawdę od ustawień sieci, w której znajduje się dany adres IP. API nadaje się jednak świetnie do eksperymentowania z usługami GEO.