#!/usr/bin/perl
# $Id: ljegate.pl,v 1.4 2004/04/02 21:27:55 root Exp $
# unffective, poorly written reply-by-mail enabler for lj
# this script was written by nslu with 1/2 of code taken from
# jsn mail2lj gate, see lj.myxomop.com for details
#
# you can redistribute this script in terms of GPL license,
# see http://www.gnu.org/copyleft/gpl.html for details
use MIME::Parser;
use MIME::Words qw( :all );
use LWP::UserAgent;
use HTTP::Request;
use URI::Escape;
use HTML::TokeParser;
use Unicode::MapUTF8 qw( to_utf8 );
use strict;
use warnings;
# let redirect everything to
open STDERR, "|/var/home/l/lj/logger";
print STDERR "-"x50 . "\n";
print STDERR scalar(localtime) . "\n";
# stuff
my $realver = 20; # increment by 1 on each change
my $ver = 1-1/$realver; # asymptotic -> 1
my $uastr = 'ljegate';
my $ua_timeout = 180;
my $host = 'lj.myxomop.com';
my $rehost = 'myxomop.com'; #host that receives mail from lj
my $ljhost = 'livejournal.com';
my $correct_action = 'http://www.livejournal.com/talkpost_do.bml';
my $lj_reply_uri = "http://www.$ljhost/talkpost_do.bml";
my $lj_entry_uri = "http://www.$ljhost/cgi-bin/log.cgi";
# sub parses attached form from email notfication
# and extrudes all fields
sub get_inputs {
my $s = shift;
my $h = shift;
my $p = HTML::TokeParser->new (\$s) or die "Can't parse: $!";
my $token = $p->get_tag("form");
die("Wrong form") unless ($token->[1]{action} eq $correct_action);
while ($token = $p->get_tag("input") ) {
$h->{$token->[1]{name}} = $token->[1]{value}||'' if ($token->[1]{name});
}
}
sub check_head {
my $head = shift;
return 1;
for (my $i = $head->count('Received')-1; $i>=0; $i--) {
my @rec_h = split(/\n/,$head->get('Received', $i));
return 1 if ($rec_h[0] eq "from livejournal.com ([66.150.15.150])" &&
$rec_h[1] =~ /^\tby $rehost/)
}
return undef;
}
# creates and escapes url string from hash
sub href2string {
my $hr = shift ;
my $i ;
my $s = "" ;
foreach $i (keys %$hr) {
next if $i eq "event" ;
$s .= "&" if $s ;
$s .= $i . "=" . uri_escape($hr->{$i}, "^A-Za-z0-9") ;
}
if ($hr->{event}) {
$s .= "&" if $s ;
$s .= "event=" . uri_escape($hr->{event}, "^A-Za-z0-9") ;
}
return $s ;
}
# here we are, we got a message in a pipe
my $parser = new MIME::Parser;
$parser->output_under("/tmp");
$parser->output_to_core(1);
my $entity = $parser->parse(\*STDIN);
# now we got message
# let's see where it came from and where it is targeted
# to do this we need headers
my $head = $entity->head;
my ($to) = $head->get('To', 0) =~ /([\w.+-]+\@[\w.+-]+)/;
my ($from) = $head->get('From', 0) =~ /([\w.+-]+\@[\w.+-]+)/;
chomp $to;
chomp $from;
print STDERR "From: $from\nTo: $to\n";
# as suggested, there are three themes of 'To' with `From: lj_dontrelpy'
# 1) ljf-$usermail -- for including params in from/to
# 2) ljb-$usermail -- for including params in body
# 3) lji-$usermail -- for including params in message-id
# the last one is the only one i am going to implement now
# `@' in $usermail should be replaced with `+'
# on error behaviour is simple: die
if ($from =~ /^lj_notify\@$ljhost/ &&
$to =~ /^lj[if]-([\w-]+)\+([\w.-]+)\@\w+/) {
# this message is comment notification from livejournal.com, parse
# and resend.
# this is where message will go
my $address = "$1\@$2";
print STDERR "new addr = $address\n";
my ($poster) = $head->get('From') =~ /\(([\w.+-]+)/;
$poster = 'anonymous' if ($poster =~ /^LiveJournal$/ || (!defined $poster));
# yeah, we are promiscuous as hell, but let's at least make sure message
# comes from livejournal.com
unless (check_head($head)) {
$head->print(\*STDERR);
die "Suspicious header:\n";
}
die "Should be exactly two entity parts" unless ($entity->parts == 2) ;
my %hash;
# get hidden form values from second mime entity (counting from zero)
get_inputs ($entity->parts(1)->bodyhandle->as_string, \%hash);
die ("Incomplete form data") unless ($hash{userpost} && $hash{journal} &&
$hash{parenttalkid} && $hash{itemid} &&
$hash{ecphash} );
# now form Message-Id:
my $msgid = "$hash{userpost}+$hash{journal}+$hash{parenttalkid}" .
"+$hash{itemid}+" . substr($hash{ecphash},5) . "\@$host";
print STDERR $msgid . "\n";
# we got a problem here - there is already nice already built entity
# in $entity->parts(1), do we want to build it again? looks like we
# have to -- man is whole fucking 1242 lines long, cant read it.
my $charset = $entity->parts(0)->head->mime_attr('content-type.charset');
my $newfrom;
if ($to =~ /^ljf/) { # reply identified by 'To'
$newfrom = "";
} else { # ($to =~ /^lji/), reply identified by 'In-Reply-To'
$newfrom = "";
}
$newfrom = qq["ljegate ($poster)" $newfrom];
# substr is needed because MIME::Tools is broken
my $subj = decode_mimewords($head->get('Subject'));
$subj = substr($subj,0,30);
chomp $subj;
my $newent = MIME::Entity->build(
'From' => $newfrom,
'Sender' => $newfrom,
'To' => $address,
'Subject' => encode_mimeword($subj, 'B', $charset),
'Content-Type' => "text/plain; charset=$charset",
'Message-Id' => "<$msgid>",
'References' => "<$hash{journal}-$hash{parenttalkid}\@$host>",
'Data' => $entity->parts(0)->bodyhandle->as_string
);
# need to send it now, huh?
$newent->send('sendmail');
$newent->purge();
$entity->purge();
} elsif ($to =~ /^lji\@$host/ ||
$to =~ /^ljf-(\w+)\+(\w+)\+(\w+)\+(\w+)\+(\w+)\@$host/) {
# somebody replied to message we send in previous `if'
my %h;
if ($to =~ /^lji/) {
print STDERR "In-Reply-To: " . ($head->get('In-Reply-To')||'') . "\n";
($h{userpost}, $h{journal}, $h{parenttalkid}, $h{itemid}, $h{ecphash}) =
$head->get('In-Reply-To') =~ /<(\w+)\+(\w+)\+(\w+)\+(\w+)\+(\w+)\@$host>/;
} elsif ($to =~ /^ljf/) {
($h{userpost}, $h{journal}, $h{parenttalkid}, $h{itemid}, $h{ecphash}) =
$head->get('To') =~ /ljf-(\w+)\+(\w+)\+(\w+)\+(\w+)\+(\w+)\@$host/;
}
unless ($h{userpost} && $h{journal} && $h{parenttalkid} &&
$h{itemid} && $h{ecphash})
{
print "malformed id\n";
die "malformed id\n";
}
$h{ecphash} = "ecph-" . $h{ecphash};
$h{usertype} = 'user';
$h{encoding} = $head->mime_attr('content-type.charset');
$h{subject} = decode_mimewords($head->get('Subject'));
$h{body} = $entity->bodyhandle->as_string ;
$entity->purge();
my $req = new HTTP::Request('POST' => $lj_reply_uri) or die $!;
$req->content_type('application/x-www-form-urlencoded');
$req->content(href2string(\%h)) ;
my $ua = new LWP::UserAgent or die $!;
$ua->agent("$uastr/$ver");
$ua->timeout($ua_timeout);
my $res = $ua->request($req);
if ($res->is_success) {
print STDERR "Comment posted\n";
} else {
print "Failed to post comment\n";
print "LJ server response:\n" ;
print $res->content;
print "\n";
die "Failed to post comment\n" . ($res->content) . "\n";
}
} elsif ($to =~ /^lj-post\@$host$/ ||
$to =~ /^lj-post-(\w+)-(\w+)\@$host$/) {
# request to post journal entry
my $req_data = {
webversion => 'full',
security => 'public',
prop_opt_preformatted => 0,
mode => 'postevent',
ver => 1,
subject => decode_mimewords($head->get('Subject'))||''
};
if ($1 && $2) {
$req_data->{user} = $1;
$req_data->{hpassword} = $2;
}
my $users = {
'nobody' => {
auth => 'some key',
password => 'some password'
}
};
my $auth ;
foreach ($entity->bodyhandle->as_lines) {
if (exists $req_data->{event}) {
$req_data->{event} .= $_ ;
next ;
}
next if /^$/ ;
if (/^(\w[\w_]*[\w])\s*[=:]\s*(\S.*)?\s*$/) {
my ($var, $val) = (lc($1), $2) ;
next unless $val; # skip empty headers, eg '^mood:$'
if ($var eq "date") {
if ($val =~ /(\d\d)\.(\d\d)\.(\d{2,4})\s+(\d\d?):(\d\d)/) {
$req_data->{day} = $1 ;
$req_data->{mon} = $2 ;
$req_data->{year} = $3 ;
$req_data->{hour} = $4 ;
$req_data->{min} = $5 ;
$req_data->{year} += 2000 if $req_data->{year} < 100 ;
}
} elsif ($var eq "mood" || $var eq "current_mood") {
$req_data->{prop_current_mood} = $val ;
} elsif ($var eq "music" || $var eq "current_music") {
$req_data->{prop_current_music} = $val ;
} elsif ($var eq "picture" || $var eq "picture_keyword") {
$req_data->{prop_picture_keyword} = $val ;
} elsif ($var eq "formatted" || $var eq "autoformat" || $var eq "format") {
$val = 1 if $val =~ /((on)|(yes))/i ;
$val = 0 if $val =~ /((off)|(no))/i ;
$req_data->{prop_opt_preformatted} = $val ;
} elsif ($var eq "auth") {
$auth = $val ;
} elsif ($var eq "username") {
$req_data->{user} = $val ;
} else {
$req_data->{$var} = $val ;
}
} else {
$req_data->{event} = $_ ;
}
}
my $mood_ids = {
aggravated=>1,
angry=>2,
annoyed=>3,
anxious=>4,
bored=>5,
confused=>6,
crappy=>7,
cranky=>8,
depressed=>9,
discontent=>10,
energetic=>11,
enraged=>12,
enthralled=>13,
exhausted=>14,
happy=>15,
high=>16,
horny=>17,
hungry=>18,
infuriated=>19,
irate=>20,
jubilant=>21,
lonely=>22,
moody=>23,
'pissed off'=>24,
sad=>25,
satisfied=>26,
sore=>27,
stressed=>28,
thirsty=>29,
thoughtful=>30,
tired=>31,
touched=>32,
lazy=>33,
drunk=>34,
ditzy=>35,
mischievous=>36,
morose=>37,
gloomy=>38,
melancholy=>39,
drained=>40,
excited=>41,
relieved=>42,
hopeful=>43,
amused=>44,
determined=>45,
scared=>46,
frustrated=>47,
indescribable=>48,
sleepy=>49,
groggy=>51,
hyper=>52,
relaxed=>53,
restless=>54,
disappointed=>55,
curious=>56,
mellow=>57,
peaceful=>58,
bouncy=>59,
nostalgic=>60,
okay=>61,
rejuvenated=>62,
complacent=>63,
content=>64,
indifferent=>65,
silly=>66,
flirty=>67,
calm=>68,
refreshed=>69,
optimistic=>70,
pessimistic=>71,
giggly=>72,
pensive=>73,
uncomfortable=>74,
lethargic=>75,
listless=>76,
recumbent=>77,
exanimate=>78,
embarrassed=>79,
envious=>80,
sympathetic=>81,
sick=>82,
hot=>83,
cold=>84,
worried=>85,
loved=>86,
awake=>87,
working=>88,
productive=>89,
accomplished=>90,
busy=>91,
blah=>92,
full=>93,
grumpy=>95,
weird=>96,
nauseated=>97,
ecstatic=>98,
chipper=>99,
rushed=>100,
contemplative=>101,
nerdy=>102,
geeky=>103,
cynical=>104,
quixotic=>105,
crazy=>106,
creative=>107,
artistic=>108,
pleased=>109,
bitchy=>110,
guilty=>111,
irritated=>112,
blank=>113,
apathetic=>114,
dorky=>115,
impressed=>116,
naughty=>117,
predatory=>118,
dirty=>119,
giddy=>120,
surprised=>121,
shocked=>122,
rejected=>123,
numb=>124,
cheerful=>125,
good=>126,
distressed=>127,
intimidated=>128,
crushed=>129,
devious=>130,
thankful=>131,
grateful=>132,
jealous=>133,
nervous=>134
};
if (exists $req_data->{prop_current_mood}) {
if (exists $mood_ids->{lc($req_data->{prop_current_mood})}) {
$req_data->{prop_current_moodid} = $mood_ids->{lc($req_data->{prop_current_mood})};
delete $req_data->{prop_current_mood};
}
}
if (lc($req_data->{security}) eq "friends" || lc($req_data->{security}) eq "protected") {
$req_data->{security} = "usemask";
if (!exists $req_data->{allowmask}) {
$req_data->{allowmask} = 1;
}
}
if (!exists $req_data->{year}) {
my @lt = localtime() ;
$req_data->{day} = $lt[3] ;
$req_data->{mon} = $lt[4] + 1 ;
$req_data->{year} = 1900 + $lt[5] ;
$req_data->{hour} = $lt[2] ;
$req_data->{min} = $lt[1] ;
}
if ($auth) {
$req_data->{password} = $users->{$req_data->{user}}->{password}
if exists $users->{$req_data->{user}} &&
$users->{$req_data->{user}}->{auth} eq $auth ;
} elsif (!$req_data->{user} ||
((!$req_data->{password})&&(!$req_data->{hpassword}))) {
print "Failed to add journal entry, user/passwd not specified\n";
die ("No user/password combination specified");
}
my $req = new HTTP::Request('POST' => $lj_entry_uri) or die $!;
$req->content_type('application/x-www-form-urlencoded');
unless ($head->mime_attr('content-type.charset') =~ /^utf-?8$/) {
foreach (keys %{$req_data}) {
$req_data->{$_} = to_utf8(
{ -string => $req_data->{$_},
-charset => $head->mime_attr('content-type.charset')
}
);
}
}
$req->content(href2string($req_data)) ;
$entity->purge();
my $ua = new LWP::UserAgent or die $!;
$ua->agent("$uastr/$ver");
$ua->timeout($ua_timeout);
my $res = $ua->request($req);
if ($res->is_success) {
print STDERR $res->content;
print STDERR "Journal entry posted\n";
foreach (split (/\n/, $res->content)) {
chomp;
if (/^errmsg/) {
print $res->content;
die "Post failed:\n" . $res->content . "\n";
}
}
} else {
print "Failed to post journal entry\nFull server responce below:\n"
. $res->error_as_HTML;
die "Failed to post journal entry\n";
}
} elsif ($from =~ /^.*\@livejournal.com/ &&
$to =~ /^lji-([\w-]+)\+([\w.+-]+)\@\w+/) {
$entity->head->replace('To', "$1\@$2");
$entity->send('sendmail');
$entity->purge();
print STDERR "Resending mail as it is...\n";
} else {
$entity->purge();
print "\n\n\nljegate got really confused by this message\n\n\n\n";
die "Dont know what to do with this message\n";
}
# vim:ai:et:ts=2:sw=2:tw=78: