<?php
require_once(dirname(__FILE__).'/../../core/class/class.PluginImport.php');
/**
* Import class (SINGLETON see http://en.wikipedia.org/wiki/Singleton_pattern)
*
* This class allows image extraction from messages of a mailbox and the geoblog database update.
* Messages can contain
* -1 pic + 1 subject with coord -> imports location from subject
* -1 pic + no subject -> imports location from pic (EXIF metadata)
* -No pic + 1 subject with coord -> imports location from subject
* -No pic + no Subject -> imports nothing!
* -1 pic + subject is 'drop' -> imports location from pic (EXIF metadata) but not pic
* Current restrictions are:
* -Only 1 pic is expected by message (in case of multi-attachments, only the first pic is imported)
* -ReplyTo is extracted only from the last no-read message found in the mailbox
*
* @author Olivier G <hide@address.com>
* @version 1.0
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
* @link http://gemibloo.fr
*/
final class ImportMailbox extends PluginImport{
private static $_instance;
private $_mbox = null;
private $_reply_toaddress = null;
/**
* __construct
*/
private function __construct() {}
/**
* __destruct
*/
function __destruct() {
if(FALSE === is_null($this->_mbox))
@imap_close($this->_mbox, CL_EXPUNGE);
}
/**
* __clone
*/
private function __clone() {}
/**
* fGetInstance method
*/
public static function fGetInstance()
{
if (!isset(self::$_instance)) {
$c = __CLASS__;
self::$_instance = new $c;
}
return self::$_instance;
}//getInstance
/**
* fGetReplyTo method
*
* @return string email address to reply-to
*/
public function fGetReplyTo(){
return $this->_reply_toaddress;
}
/**
* fImport method
*
* This is the main function of the class. It throws ImportException typed exceptions if something goes wrong.
*/
public function fImport(){
//Connect to mailbox
if(FALSE === ($this->_mbox = imap_open(EMAIL_SERVER, EMAIL_USR, EMAIL_PWD))){
Log::fGetInstance()->fLog(Log::$TYPE['error'] ,__CLASS__, 'Mailbox '.EMAIL_SERVER.' cannot be opened');
throw new ImportException("Mailbox cannot be opened.", ImportException::MAILBOX_REQUEST_FAILED);
}
//Get all messages
$mbox_search = imap_search ($this->_mbox, 'ALL');
if( (FALSE !== $mbox_search) && (0 != count($mbox_search)) ){
Log::fGetInstance()->fLog(Log::$TYPE['info'] ,__CLASS__, 'New message found in '.EMAIL_SERVER);
//Sort in reverse chronological order
rsort($mbox_search);
//Loop through messages
foreach($mbox_search as $mbox_msg){
$lat = 0.0;
$lng = 0.0;
$content = '';
$filepath = '';
$timestamp = time();
$ratioHW = 1.0;
//Get current message header
$msg_head = imap_headerinfo($this->_mbox, $mbox_msg);
$this->_reply_toaddress = $msg_head->reply_to[0]->mailbox.'@'.$msg_head->reply_to[0]->host;
//Get current message subject
$subject = trim($msg_head->subject);
$isPicDropped = FALSE;
if('drop' == strtolower($subject))
$isPicDropped = TRUE;
//Check from where loc must be extracted
$isLocFromEXIF = TRUE;
if( (0 != strlen($subject)) && (0 != strcmp(strtolower($subject), '(pas de sujet)') ) )
$isLocFromEXIF = FALSE;
if(!$isLocFromEXIF){
//Extract loc from header string
if(FALSE === ($coords = $this->_fGetLocationFromSubject($subject))){
Log::fGetInstance()->fLog(Log::$TYPE['error'] ,__CLASS__, 'Location cannot be extracted from the mail subject');
throw new ImportException("Location cannot be extracted from the mail subject.", ImportException::MESSAGE_REQUEST_FAILED);
}
$lat = $coords[0];
$lng = $coords[1];
$content = $coords[2];
//and timestamp from message header
$timestamp = strtotime ($msg_head->date);
}
//Get message structure
$msg_structure = imap_fetchstructure($this->_mbox,$mbox_msg);
$i=1;
//Loop now through this structure
foreach($msg_structure->parts as $part){
//Is current section a plain text?
if('PLAIN' == $part->subtype){
//Extract this text
$rawContent = imap_qprint(imap_fetchbody($this->_mbox, $mbox_msg,$i));
$content = substr($rawContent,0, strlen($rawContent)-2).$content;
}
//Is current section a jpeg picture?
else if('JPEG' == $part->subtype){
//Define a random filename
$filename = md5(time()*$i*$mbox_msg).'.jpg';
$filepath = dirname(__FILE__).'/../../../'.DATA_FOLDER.$filename;
//Storespicture on file system
if (!$handle = fopen($filepath, "w")){
Log::fGetInstance()->fLog(Log::$TYPE['error'] ,__CLASS__, 'Image cannot be created on file system');
throw new ImportException("Image cannot be created on file system.", ImportException::WRITING_FAILED);
}
if (FALSE === fwrite($handle, base64_decode(imap_fetchbody($this->_mbox, $mbox_msg,$i)))){
Log::fGetInstance()->fLog(Log::$TYPE['error'] ,__CLASS__, 'Image cannot be written on file system');
throw new ImportException("Image cannot be written on file system.", ImportException::WRITING_FAILED);
}
fclose($handle);
//Extract usefull data from EXIF metadata (loc, timestamp)
if(FALSE !== ($coords = $this->_fGetDataFromExif($filepath)))
{
//Height /Width ratio
$ratioHW = $coords[3];
if($isLocFromEXIF)
{
$lat = $coords[0];
$lng = $coords[1];
$timestamp = $coords[2];
}//if
}//else
else{
Log::fGetInstance()->fLog(Log::$TYPE['error'] ,__CLASS__, 'Coordinates cannot be extracted from the picture');
throw new ImportException("Coordinates cannot be extracted from the picture.", ImportException::MESSAGE_REQUEST_FAILED);
}
//Is current pic must be used in the blog entry?
if($isPicDropped){
//Delete current image which was necessary only to get geodata
unlink($filepath);
$filename = '';
}
break;//Only one pic is expected by image
}//else if
$i++;
}//foreach
//Reverse geocode
$placemark = $this->_fReverseGeocode($lat,$lng);
//Inserts all extracted contents in database
$this->_fInsert($lat, $lng, $content, $timestamp, $filename, $ratioHW, $placemark);
$this->_newData = true;
//Delete current message
imap_delete($this->_mbox, $mbox_msg);
Log::fGetInstance()->fLog(Log::$TYPE['info'] ,__CLASS__, 'All new messages deleted');
}//foreach
return TRUE;
}else{
Log::fGetInstance()->fLog(Log::$TYPE['info'] ,__CLASS__, 'No message found in '.EMAIL_SERVER);
return FALSE;
}
}//fImport
//////////////////////////////////////////////////////////////////////////////////
//HELPER FUNCTIONS (all private)
//////////////////////////////////////////////////////////////////////////////////
/**
* _fGetLocationFromSubject method
*
* @param string $aSubject subject of a message (should contain location either as address or coordinates)
* @return array|FALSE If successfull coordinates as array of [lat, lng] extracted from the subject
*/
private function _fGetLocationFromSubject($aSubject){
//location can be either a (lat,lng) pair or an address
$coords = split(',', $aSubject);
if(2 == count($coords)){
$coords[0] = floatval(trim($coords[0]));
$coords[1] = floatval(trim($coords[1]));
if( ($coords[0]<90.0) && ($coords[0]>-90.0) && ($coords[1]<180.0) && ($coords[1]>-180.0)){
//location is highly probably given as a (lat,lng) pair.
$coords[] = '';
return $coords;
}
}
//At this stage assumes loc is given as an address
$coords = array();
//try geocoding
if(FALSE === ($csv = file_get_contents(GOOGLE_URL_GEOCODING.urlencode($aSubject))))
return FALSE;
$csvSplit = split(",", $csv);
if (strcmp($status, $csvSplit[2]) == 0) {
$coords[] = $csvSplit[2];//lat
$coords[] = $csvSplit[3];//lng
}else
return FALSE;
//current address will be appended to message body
$coords[] = ' (@ '.$aSubject.')';
return $coords;
}//_fGetLocationFromSubject
/**
* _fGetDataFromExif method
*
* @param string $aFilename path of the pic (on server file system) to analyse
* @return array|FALSE If successfull coordinates as array of [lat, lng,timestamp, and height/width ratio] extracted from the subject
*/
private function _fGetDataFromExif($aFilename){
//Extract EXIF structure
if(FALSE === ($exif = @exif_read_data($aFilename, 0, true)))
return FALSE;
//Get file width
if(isset($exif['COMPUTED'])){
if(isset($exif['COMPUTED']['Width']))
$width = intval($exif['COMPUTED']['Width']);
else
return FALSE;
//Get file height
if(isset($exif['COMPUTED']['Height']))
$height = intval($exif['COMPUTED']['Height']);
else
return FALSE;
//...and computes pic size ratio
$ratio = $height / $width;
}
else
return FALSE;
//Get timestamp
if(isset($exif['EXIF']) && isset($exif['EXIF']['DateTimeOriginal']))
$timestamp = strtotime($exif['EXIF']['DateTimeOriginal']);
else
return FALSE;
//Get GPS data
if(isset($exif['GPS'])){
//Processe longitude first
if(isset($exif['GPS']['GPSLongitude'])){
//Format extracted longitude
$lng = $this->_fGetCoordinatesFromExif($exif['GPS']['GPSLongitude']);
//Check longitude sign
if(isset($exif['GPS']['GPSLongitudeRef'])){
if('W' == $exif['GPS']['GPSLongitudeRef'])
$lng = -$lng;
}else
return FALSE;
}else
return FALSE;
//Processe latitude
if(isset($exif['GPS']['GPSLatitude'])){
//Format extracted latitude
$lat = $this->_fGetCoordinatesFromExif($exif['GPS']['GPSLatitude']);
//Check latitude sign
if(isset($exif['GPS']['GPSLongitudeRef'])){
if('S' == $exif['GPS']['GPSLatitudeRef'])
$lat = -$lat;
}else
return FALSE;
}else
return FALSE;
}else
return FALSE;
return array($lat, $lng, $timestamp, $ratio);
}//_fGetDataFromExif
/**
* _fGetCoordinatesFromExif method
*
* @param array $aInput array of coordinates as [deg, mn, sec] (each item can be a quotient of numbers (See http://www.exif.org/Exif2-2.PDF for more explanations)
* @return float|FALSE If successfull coordinates as decimal degrees
*/
private function _fGetCoordinatesFromExif($aInput){
$dd_f=strpos($aInput[0],'/');
$mm_f=strpos($aInput[1],'/');
$ss_f=strpos($aInput[2],'/');
//degrees
if(FALSE === $dd_f)
$dd = $aInput[0];
else{
$denominator = floatval(substr($aInput[0],$dd_f+1));
if(fIsEqual($denominator,0.0))
$dd= 0.0;
else
$dd=floatval(substr($aInput[0],0,$dd_f))/$denominator;
}
//minutes
if(FALSE === $mm_f)
$mm= $aInput[1];
else{
$denominator = floatval(substr($aInput[1],$mm_f+1));
if(fIsEqual($denominator,0.0))
$mm= 0.0;
else
$mm=floatval(substr($aInput[1],0,$mm_f))/$denominator;
}
//seconds
if(FALSE === $ss_f)
$ss= $aInput[2];
else{
$denominator = floatval(substr($aInput[2],$ss_f+1));
if(fIsEqual($denominator,0.0))
$ss= 0.0;
else
$ss=floatval(substr($aInput[2],0,$ss_f))/$denominator;
}
//Return current coordinates converted in decimal degrees
return $dd + 0.0166666667 * $mm + 2.7777778e-4 * $ss;
}//_fGetCoordinatesFromExif
/**
* _fReverseGeocode method
*
* This method uses Curl library to connect to Google reverse geocoder
*
* @param float $aLat latitude as decimal degrees
* @param float $aLng longitude as decimal degrees
* @return string location as 'city (country)'
*/
private function _fReverseGeocode($aLat, $aLng){
$curl_handle = curl_init();
curl_setopt($curl_handle, CURLOPT_CONNECTTIMEOUT, 2);
curl_setopt($curl_handle, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl_handle, CURLOPT_URL, GOOGLE_URL_REVERSEGEOCODING.$aLat.','.$aLng);
$buffer = curl_exec($curl_handle);
curl_close($curl_handle);
if(FALSE !== ($rg = json_decode ($buffer, true))){
if(200 !== $rg['Status']['code'])
return '';
$rg = $rg['Placemark'][0]['AddressDetails']['Country'];
return $rg['AdministrativeArea']['SubAdministrativeArea']['Locality']['LocalityName'].' ('.$rg['CountryName'].')';
}
else
return '';
}//_fReverseGeocode
}//Import_Mailbox
?>