#!/usr/bin/perl

use CGI::Carp qw(fatalsToBrowser);
use URI::Escape;
use LWP::UserAgent;
use IO::Socket::INET;
use MIME::Base64;
use Time::Local;
use Net::SSH2;
#use Net::SMTP::SSL;
use File::Stat;
use Mail::IMAPClient;
use HTTP::Date;
use Cwd;
use DBI;
use warnings;
use strict;
use strict "refs";
use strict "subs";

my $BLOG_ID = "blogger-blog-id";
my $BLOG_USERNAME = "username\@gmail.com";
my $BLOG_PASSWORD = "password";

my $SMTP_EMAIL = "username\@mac.com";
my $IMAP_SUBJECT = "ESN:0-XXXXXXX";
my $IMAP_SERVER = "mail.somedomain.com";
my $IMAP_USERNAME = "username";
my $IMAP_PASSWORD = "password";
my @IMAP_MAILBOXES = qw(INBOX Incoming/Lists/FindMeSpot);

my $DB_HOSTNAME = "127.0.0.1";
my $DB_DATABASE = "locator";
my $DB_USERNAME = "locator";
my $DB_PASSWORD = "password";

my $XSL_PATH = "http://yourdomain.com/tracklog.xsl";

my $SFTP_HOST = "yourdomain.com";
my $SFTP_USER = "username";
my $SFTP_PASS = "password";
my $SFTP_BASE = "/path/on/webserver/to/html";

my @MONTHS = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);

my %MONTHS = (
	'Jan' => 0,
	'Feb' => 1,
	'Mar' => 2,
	'Apr' => 3,
	'May' => 4,
	'Jun' => 5,
	'Jul' => 6,
	'Aug' => 7,
	'Sep' => 8,
	'Oct' => 9,
	'Nov' => 10,
	'Dec' => 11
);

&main();

sub main
{
	my $self = {env=>\%ENV, args=>\@ARGV};
	my $emails;
	
	print "main().. starting [" . &timestamp() . "]\n";
	
	# check for new emails
	$emails = &doMailCheck();
	
	# connect to the locator database
	$self->{dbh} = DBI->connect("dbi:Pg:dbname=".$DB_DATABASE.";host=".$DB_HOSTNAME, $DB_USERNAME, $DB_PASSWORD, {AutoCommit=>1}) or die "main().. something went wrong, DBI::errstr\n";
	
	# process each new email
	foreach my $email (@$emails) {
		&doProcEmail($self,"history/$email");
	}
	
	print "main().. done [" . &timestamp() ."]\n";
	
	return 0;
}

sub doProcEmail
{
	my $self = shift @_;
	my $file = shift @_;
	my $email;
	
	print "doProcEmail().. parsing email [$file]\n";
	
	# parse the file specified as the first (and only) command line
	# argument. this file should be the raw contents of a FindMeSpot 
	# email.
	if (!defined ($email = &doMailParse($file))) {
		print "doProcEmail().. failed to parse email\n";
		return 0;
	}
	
	if ($email->{From} !~ /noreply\@findmespot\.com/ && $email->{From} !~ /curtis\.jones\@gmail\.com/) {
		print "doProcEmail().. unsupported From [$email->{From}]\n";
		return 0;
	}
	
	print "doProcEmail().. logging location\n";
	
	# we want to save a list of all points that we broadcast, so that
	# we can see the route that we took.
	if (!&doLocationLog($self,$email->{Longitude},$email->{Latitude},$email->{Date})) {
		print "doProcEmail().. location log failed. oh well\n";
	}
	
	# find the nearest interesting feature to our present location.
	# this is only a nicety, so if it fails that's no reason to not do
	# the blog post.
	if (!&doLocationFind($self,$email->{Longitude},$email->{Latitude})) {
		print "doProcEmail().. location lookup failed. oh well\n";
	}
	
	# find the distance traveled sinc the last known location.
	if (!&doLocationDist($self,$email)) {
		print "doProcEmail().. distance lookup failed. oh well\n";
	}
	
	# export an xml file with the tracklog history
	if (!&doXmlExport($self,$email,30)) {
		print "doXmlExport().. xml export failed.\n";
	}
	
	# get an authentication token from blogger
	if (!&doAuthenticate($self)) {
		print "doProcEmail().. failed to authenticate\n";
		unlink($file);
		return 0;
	}
	
	# create the new blog post
	if (!&doBlogPost($self,$email)) {
		print "doProcEmail().. failed to do blog post\n";
		unlink($file);
	}
	
	# send a text message
#	if (!&doTextMessage($self,$email,"curtis.jones\@gmail.com")) {
#		print "doProcEmail().. failed to send text message\n";
#	}
	
	return 1;
}

sub doSaveArguments
{
	my $self = shift @_;
	
#	open(FILE,">> log.out") or die "doSaveArguments, could not open log file, $!\n";
	
	foreach my $arg (@{$self->{args}}) {
#		print FILE "ARGS = $arg\n";
		print "ARGS = $arg\n";
	}
	
	foreach my $key (keys %{$self->{env}}) {
#		print FILE "$key = $self->{env}->{$key}\n";
		print "$key = $self->{env}->{$key}\n";
	}
	
#	print FILE "\n";
	
#	close(FILE);
}

sub doAuthenticate
{
	my $self = shift @_;
	my $query = "";
	
	$self->{auth} = {};
	
	$query .= "Email=" . $BLOG_USERNAME . "&";
	$query .= "Passwd=" . $BLOG_PASSWORD . "&";
	$query .= "service=blogger&";
	$query .= "accountType=GOOGLE&";
	$query .= "source=curtis-locator-1&";
	
	my $ua = new LWP::UserAgent;
	my $req = new HTTP::Request(POST => "https://www.google.com/accounts/ClientLogin");
	$req->content_type('application/x-www-form-urlencoded');
	$req->content($query);
	my $res = $ua->request($req);
	
	if ($res->is_error) {
		print "doAuthenticate().. http error, " . $res->content() . "\n";
		return 0;
	}
	
	# SID=...
	# LSID=...
	# Auth=...
	
	my @values = split(/\s+/, $res->content());
	
	foreach my $value (@values) {
		my %vars = &parseCGIQuery($value);
		
		foreach my $key (keys %vars) {
			$self->{auth}->{$key} = $vars{$key};
		}
	}
	
	print "doAuthenticate().. authenticated!\n";
	
	return 1;
}

sub doBlogPost
{
	my $self = shift @_;
	my $email = shift @_;
	my $poi = $self->{poi};
	my $xml = "";
	
	my $url = $email->{http};
	$url =~ s/\&/\&amp;/g;
	
	$xml .= "<?xml version='1.0' encoding='utf-8'?>";
	$xml .= "<entry xmlns='http://www.w3.org/2005/Atom'>";
	$xml .=   "<title type='text'>FindMeSpot Locator Alert!</title>";
	$xml .=   "<content type='html'>";
	$xml .=   	&htmlify("<p>Apparently we're still alive:</p>");
	$xml .=   	&htmlify("&nbsp;&nbsp;<b>Location:</b> $email->{Latitude}, $email->{Longitude}<br/>");
	$xml .=   	&htmlify("&nbsp;&nbsp;<b>Timestamp:</b> $email->{Time}<br/>");
	$xml .=   	&htmlify("&nbsp;&nbsp;<b>Map:</b> <a href=\"http:$url\" target=_blank>Google Maps</a><br/>");
	$xml .=   	&htmlify("<p/>");
	
	if ($poi) {
		my ($lat,$lon) = $poi->{location} =~ m/^\(([\-]*\d+[\.]{0,1}\d*),([\-]*\d+[\.]{0,1}\d*)\)$/;
		$xml .=  &htmlify("<p>Closest point of interest:</p>");
		$xml .=  &htmlify("&nbsp;&nbsp;<b>Description:</b> $poi->{description}<br/>");
		$xml .=  &htmlify("&nbsp;&nbsp;<b>Location:</b> " . sprintf("%.05f, %.05f",$lat,$lon) . "<br/>");
		$xml .=  &htmlify("&nbsp;&nbsp;<b>Distance:</b> " . sprintf("%.02f",($poi->{distance}/1609.344)) . " miles<br/>");
		$xml .=  &htmlify("<p/>");
	}
	
	if ($email->{Distance}) {
		$xml .=  &htmlify("<p>Since last time:</p>");
		$xml .=  &htmlify("&nbsp;&nbsp;<b>Distance:</b> " . sprintf("%.02f",($email->{Distance}/1609.344)) . " miles<br/>");
		$xml .=  &htmlify("<p/>");
	}
	
	$xml .=   "</content>";
	$xml .=   "<category scheme=\"http://www.blogger.com/atom/ns#\" term=\"Locator\" />";
	$xml .= "</entry>";
	
	my $ua = new LWP::UserAgent;
	my $req = new HTTP::Request(POST => "http://www.blogger.com/feeds/" . $BLOG_ID . "/posts/default");
	$req->header("Authorization" => "GoogleLogin auth=" . $self->{auth}->{Auth});
	$req->content_type('application/atom+xml');
	$req->content($xml);
	my $res = $ua->request($req);
	
	if ($res->is_error) {
		print "doBlogPost().. http error, " . $res->content() . "\n";
		return 0;
	}
	
	print "doBlogPost().. succeeded!\n";
	
	return 1;
}

sub doTextMessage
{
	my $self = shift @_;
	my $email = shift @_;
	my $rcpt = shift @_;
	my $poi = $self->{poi};
	my $data = "";
	
	$data .= "Location: " . sprintf("%.05f, %.05f",$email->{Latitude},$email->{Longitude}) . "; ";
	$data .= "Near: $poi->{description} (" . sprintf("%.02f",($poi->{distance}/1609.344)) . " miles away.\n";
	
	my $smtp = Net::SMTP->new("127.0.0.1", Timeout=>60, Debug=>1) or die "doTextMessage().. could not connect, $!\n";
                            
	$smtp->mail($SMTP_EMAIL);
	$smtp->to($rcpt);

	$smtp->data();
	$smtp->datasend("To: $rcpt\n");
	$smtp->datasend($SMTP_EMAIL);
	$smtp->datasend("Subject: Hello!\n");
	$smtp->datasend("\n");
	$smtp->datasend($data);
	$smtp->dataend();

	$smtp->quit();
	
	print "doTextMessage().. succeeded! [$rcpt]\n";
	
	return 1;
}

sub doXmlExport
{
	my $self = shift @_;
	my $email = shift @_;
	my $days = shift @_;
	
	if (!open(XML,"> tracklog.xml")) {
		print "doXmlExport().. failed to open tracklog.xml, $!\n";
		return 0;
	}
	
	print XML "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
	print XML "<?xml-stylesheet type=\"text/xsl\" href=\"$XSL_PATH\"?>\n";
	print XML "<tracklog>\n";
	
	{
		my $sql = "SELECT * FROM tracklog_get(10)";
		
		my $sth = $self->{dbh}->prepare($sql);
		
		my $res = $sth->execute();
		
		if (!$res) {
			print STDERR "doXmlExport().. select failed\n";
			return 0;
		}
		
		while (my $event = $sth->fetchrow_hashref) {
			my ($lat,$lon) = $event->{location} =~ m/^\(([\-]*\d+[\.]{0,1}\d*),([\-]*\d+[\.]{0,1}\d*)\)$/;
			
			print XML "  <event>\n";
			print XML "    <tracklog_id>"          . check($event->{tracklog_id})    . "</tracklog_id>\n";
			print XML "    <created>"              . checkForNull($event->{created}) . "</created>\n";
			print XML "    <longitude>"            . sprintf("%.05f",check($lon))    . "</longitude>\n";
			print XML "    <latitude>"             . sprintf("%.05f",check($lat))    . "</latitude>\n";
			print XML "    <altitude>"             . check($event->{altitude})       . "</altitude>\n";
			print XML "    <description><![CDATA[" . checkForNull($event->{description}) . "]]></description>\n";
			print XML "    <comment><![CDATA["     . check($event->{comment})     . "]]></comment>\n";
			
			($lat,$lon) = $event->{cp_location} =~ m/^\(([\-]*\d+[\.]{0,1}\d*),([\-]*\d+[\.]{0,1}\d*)\)$/;
			
			print XML "    <cp_id>"          . check($event->{cp_id})          . "</cp_id>\n";
			print XML "    <cp_longitude>"   . sprintf("%.05f",check($lon))    . "</cp_longitude>\n";
			print XML "    <cp_latitude>"    . sprintf("%.05f",check($lat))    . "</cp_latitude>\n";
			print XML "    <cp_altitude>"    . check($event->{cp_altitude})    . "</cp_altitude>\n";
			print XML "    <cp_distance>"    . sprintf("%.02f",($event->{cp_distance}/1609.344)) . " miles</cp_distance>\n";
			print XML "    <cp_description><![CDATA[" . checkForNull($event->{cp_description}) . "]]></cp_description>\n";
			print XML "  </event>\n";
		}
		
		$sth->finish();
	}
	
	print XML "</tracklog>\n";
	
	close(XML);
	
	sftpFile("tracklog.xml");
	
	print "doXmlExmport().. succeeded!\n";
	
	return 1;
}

sub htmlify
{
	my $string = shift @_;
	
	$string =~ s/\&/\&amp;/g;
	$string =~ s/\</\&lt;/g;
	$string =~ s/\>/\&gt;/g;
	
	return $string;
}

sub timestamp
{
	my @parts = localtime(time());
	
	sprintf("%04d-%02d-%02d %02d:%02d:%02d", ($parts[5]+1900), ($parts[4]+1), $parts[3], $parts[2], $parts[1], $parts[0]);
}

sub doErrorAndQuit
{
  my $error = shift @_;
  
  print "Content-Type: text/html\r\n\r\n";
  print "An error occurred: $error\r\n";
  
  exit(0);
}

sub parseCGIQuery
{
	my $qstr = shift @_;
	my %vars = ();
	
	my @pairs = split(/&/, (!$qstr ? "" : $qstr));
	
	foreach my $pair (@pairs) {
		my ($name, $value) = split(/=/, $pair);
		
		if ($value) {
			$value =~ tr/+/ /;
			$value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
		}
		
		$vars{$name} = $value;
	}
	
	return %vars;
}

sub parseHTTPCookie
{
	my $qstr = shift @_;
	my %vars = ();
	
	my @pairs = split(/;\s*/, (!$qstr ? "" : $qstr));
	
	foreach my $pair (@pairs) {
		my ($name, $value) = split(/=/, $pair);
		
		if ($value) {
			$value =~ tr/+/ /;
			$value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
		}
		
		$vars{$name} = $value;
	}
	
	return %vars;
}

sub nameToMimeType
{
	my $name = shift @_;
	
	if ($name =~ /\.html$/i) {
		return "text/html";
	}
	elsif ($name =~ /\.txt$/i) {
		return "text/plain";
	}
	elsif ($name =~ /\.pdf$/i) {
		return "application/pdf";
	}
	elsif ($name =~ /\.jpg/i) {
		return "image/jpeg";
	}
	elsif ($name =~ /\.gif/i) {
		return "image/gif";
	}
}

sub dateToTimestamp
{
	my $date = shift @_;
	
	# Wed, 13 May 2009 20:22:20 +0000 (GMT)
	# $time = timelocal($sec,$min,$hour,$mday,$mon,$year);
	
	my @parts = $date =~ /^([A-Za-z]{3}), ([0-9]{2}) (\S+) (\d+) (\d+):(\d+):(\d+)/;
	my $time = timegm($parts[6], $parts[5], $parts[4], $parts[1], $MONTHS{$parts[2]}, $parts[3]);
	
	@parts = localtime($time);
	
	return sprintf("%04d-%02d-%02d %02d:%02d:%02d", ($parts[5]+1900), ($parts[4]+1), $parts[3], $parts[2], $parts[1], $parts[0]);
}

sub doRandomString
{
  my $size = shift @_;
  my $string = "";
  
  if ($size > 100) {
    $size = 100;
  }
  elsif (!$size) {
    my $size = 10;
  }
  
	my @chars = ('a'..'z','A'..'Z','0'..'9');
	
	srand();
	
	foreach (1..$size) {
		$string .= $chars[rand @chars];
	}
	
  return $string;
}

sub doHashAndBase64Encode
{
	my $string = shift @_;
	my $hash;
	my $sha2obj = new Digest::SHA2 512;
	
	$sha2obj->add($string);
	$hash = $sha2obj->b64digest();
	$sha2obj->reset();
	
	return $hash;
}

sub doSessionTimedOut
{
  my $self = shift @_;
  my $error = shift @_;
  
  print "Content-Type: text/html\r\n\r\n";
  print "Your session has timed out.\r\n";
  print "Error: $error\r\n";
}

sub doParseTemplate
{
  my $self = shift @_;
  my $file = shift @_;
  
  if (!$file) {
    return;
  }
  
  open my $fin, "< files/$file" || return;
  my $data = join "", <$fin>;
  close($fin);
  
  my $off = 0;
  my $ndx = 0;
  my $ndx2 = 0;
  my @parts;
  my @names;
  
  while (-1 != ($ndx = index($data, "%%", $off))) {
    last if (-1 == ($ndx2 = index($data, "%%",  $ndx+1)) || $ndx2 - $ndx > 100);
    
    push(@parts, substr($data,$off,$ndx-$off));
    push(@names, substr($data,$ndx+2,$ndx2-($ndx+2)));
    
    $off = $ndx2 + 2;
  }
  
  if ($off < length($data)) {
    push(@parts, substr($data,$off))
  }
  
  return {parts=>\@parts, names=>\@names};
}

sub doFillParsedTemplate
{
  my $self = shift @_;
  my $tpl  = shift @_;
  my $vals = shift @_;
  my $data = "";
  
  for (my $i = 0; $i <= $#{$tpl->{parts}}; $i++) {
    $data .= (defined $tpl->{parts}->[$i] ? $tpl->{parts}->[$i] : "") . 
             ($i <= $#$vals && defined $vals->[$i] ? $vals->[$i] : "");
  }
  
  return $data;
}

sub doPrintCookies
{
  my $self = shift @_;
  
  foreach my $x (keys %{$self->{cookies}}) {
    print "Set-Cookie: $x=" . $self->{cookies}->{$x} . "\r\n";
  }
}

sub check
{
	my $string = shift @_;
	
	if (! defined $string || $string eq "") {
		return "";
	}
	
	return uri_escape($string);
}

sub checkForNull
{
	my $string = shift @_;
	
	if (! defined $string || $string eq "") {
		return "";
	}
	
	return $string;
}

sub sftpFile
{
	my $file = shift @_;
	my $ssh2 = Net::SSH2->new();
	
	$ssh2->connect($SFTP_HOST) or die "Unable to connect Host $@ \n";
	$ssh2->auth_password($SFTP_USER, $SFTP_PASS) or die "Unable to login $@ \n";

	my $sftp = $ssh2->sftp();
	$ssh2->scp_put($file, "$SFTP_BASE/tracklog.xml");

	return 1;
}

# TODO: in a subsequent version we should start with a small bounding
#       box and increase its size until we have at least one point
#       contained within the box
#
sub doLocationFind
{
	my $self = shift @_;
	my $lon = shift @_;
	my $lat = shift @_;
	my $poi;
	
	{
		my $point = "$lat $lon";
		my $sql = "SELECT " .
							"  id, " .
							"  POINT(location) AS location, " .
							"  altitude, " .
							"  description, " .
							"  ST_Distance_Spheroid(location,GeomFromText('POINT($point)', 4326),'SPHEROID[\"WGS 84\",6378137,298.257223563]') AS distance " .
							"FROM " .
							"  location " .
#							"WHERE " .
#							"  location && GeomFromText('POLYGON((33 -80, 33 -160, 65 -160, 65 -80, 33 -80))',4326) " .
							"ORDER BY " .
							"  ST_Distance_Spheroid(location,GeomFromText('POINT($point)', 4326),'SPHEROID[\"WGS 84\",6378137,298.257223563]') ASC " .
							"LIMIT 1 ";
		
		my $sth = $self->{dbh}->prepare($sql);
		
		my $res = $sth->execute();
		
		if (!$res) {
			print STDERR "doLocationFind().. select failed\n";
			return;
		}
		
		$poi = $sth->fetchrow_hashref;
		$self->{poi} = $poi;
		
		$sth->finish();
	}
	
	return $poi;
}

sub doLocationLog
{
	my $self = shift @_;
	my $lon = shift @_;
	my $lat = shift @_;
	my $date = shift @_;
	
	{
		my $sql = "INSERT INTO tracklog ( " .
							"  location, " .
							"  created, " .
							"  modified " .
							") VALUES ( " .
							"  ST_GeomFromText('POINT($lat $lon)',4326), " .
							" '" . dateToTimestamp($date) . "', " .
							" '" . dateToTimestamp($date) . "' " .
							")";
		
		my $sth = $self->{dbh}->prepare($sql);
		
		my $res = $sth->execute();
		
		if (!$res) {
			print STDERR "doLocationLog().. insert failed\n";
			return;
		}
		
		$sth->finish();
	}
	
	return 1;
}

sub doLocationDist
{
	my $self = shift @_;
	my $email = shift @_;
	
	{
		my $sql = "SELECT " .
							"  ST_Distance_Spheroid ( " .
							"    (select location from tracklog order by id desc limit 1 offset 0), " .
							"    (select location from tracklog order by id desc limit 1 offset 1), " .
							"    'SPHEROID[\"WGS 84\",6378137,298.257223563]') AS distance ";
		
		my $sth = $self->{dbh}->prepare($sql);
		my $res = $sth->execute();
		
		if (!$res) {
			print STDERR "doLocationDisc().. select failed\n";
			return;
		}
		
		$email->{Distance} = $sth->fetchrow_hashref->{distance};
		$sth->finish();
	}
	
	return 1;
}

sub doMailCheck
{
	my $emails = ();
	my $query = "";
	my $latest = &doMailFindLatest();
	
	my $imap = Mail::IMAPClient->new(
		Server => $IMAP_SERVER,
		User => $IMAP_USERNAME,
		Password => $IMAP_PASSWORD)
		or die "doMailCheck().. failed to connect, $!";
	
	print "doMailCheck().. connected\n";
	
	if (defined $latest && $latest ne "") {
		$query = "SUBJECT \"$IMAP_SUBJECT\" SINCE $latest";
	}
	else {
		$query = "SUBJECT \"$IMAP_SUBJECT\"";
	}
	
	print "doMailCheck().. query = $query\n";
	
	foreach my $mailbox (@IMAP_MAILBOXES) {
		print "doMailCheck().. checking mailbox, '$mailbox'\n";
		
		$imap->select($mailbox) or die "doMailCheck().. failed to select '$mailbox', $!\n";
		
		my @messages = $imap->search($query) or next;
		
		foreach my $message (@messages) {
			my $uid = $imap->message_uid($message);
			
			if (-e "history/$uid.txt") {
				next;
			}
			
			print "doMailCheck().. message id = " . $imap->message_uid($message) . "\n";
			
			open(my $file,">history/$uid.txt") or die "doMailCheck().. failed to open 'history/$uid.txt', $!\n";
			$imap->message_to_file($file,$message);
			close($file);
			
			push(@$emails, "$uid.txt");
		}
	}
	
	$imap->logout();
	
	return $emails;
}

sub doMailFindLatest
{
	my $latest = 0;
	
	opendir(my $fh, "history");
	my @emails = readdir($fh);
	closedir($fh);
	
	foreach my $file (@emails) {
		next if $file =~ /^\./;
		
		my $email = &doMailParse("history/$file");
		my $time = str2time($email->{Date});
		
		if ($time > $latest) {
			$latest = $time;
		}
	}
	
	if ($latest == 0) {
		return;
	}
	
	# back us up to the previous day
	$latest -= 86400;
	
	my @parts = localtime($latest);
	
	return $parts[3] . "-" . $MONTHS[$parts[4]] . "-" . ($parts[5]+1900);
}

sub doMailParse
{
	my $file = shift @_;
	my $email = {};
	my $lastkey;
	
	open(FILE,"<$file") or die "doMailParse().. failed to open '$file', $!\n";
	
	while (<FILE>) {
		my $line = $_;
		
		if ($line =~ /^\s+/ && defined $lastkey && defined $email->{$lastkey}) {
			$email->{$lastkey} .= chomp($line);
		}
		else {
			$email->{$1} = $2 if $line =~ /^(.+?):\s*(.+?)\s*$/;
			$lastkey = $1;
		}
	}
	
	close(FILE);
	
	return $email;
}

1;
