#! /usr/bin/perl -w
#
# The script to post mail messages to LiveJournal
# (see http://mail2lj.nichego.net/ for original).
#
# Changes by LG (all are labelled by '# Changed by LG' string):
# - Removed all references to Mail2LJ::Config and $cfg (just as author's
# comment below says).
# - Changed $host definition.
# - Changed location of mimemtmp subdirectory from $HOME to /tmp
# - Changed location and name of log file to $HOME/mail/mail2lj.log
# - In bounces and responces replaced charset from Windows-1251 to koi8-r
# - Added comment-parsing settings (keyword Comments: can be "no" or "off"
# to forbid comments, or "noemail" to not email comments). If not set,
# falls back to Journal's Default, obviously.
# - Removed "[mail2lj]" label in the subject.
#
# ! - Added command line parsing. Now all the keywords can be specified
# on the command line (see '-h' for help). Collected options are passed
# on to the posting subroutine and *override* corresponding body keywords
# values (e.g., now you can specify '--usejournal' when posting via
# 'hpost-(user)-(MD5Hash)' alias). As an added bonus, now it's possible
# to post COMPLETELY without body keywords (via either 'post',
# 'post-(user)-(password) or 'hpost-(user)-(MD5Hash)' aliases), so you
# can use the script as a general purpose mail-to-LJ-anywhere gateway.
# E.g. it'll work great in procmail.
#
# ! - Changed recipient of bounce messages in send_bounce() function to allow
# optional designation of custom error recipient (as opposed to strictly
# original From: address). This is convenient when you want to notify
# script maintainer instead of the poster (exactly what I need).
#
#
# NB: to generate MD5 hash of your password, use the following command:
# perl -MDigest::MD5 -e 'print Digest::MD5::md5_hex("yourpassword")."\n"'
#
#
# Adopted by Lev Gorenstein <lev@ledorub.poxod.com> from the original
# script by jason@nichego.net (http://livejournal.com/users/jsn/) which
# is available at http://mail2lj.nichego.net/
#
# Original script seems to be distributed as freeware, so I stick to that
# decision. No warranty whatsoever, of course - use at your own risk ;-).
#
# Changes by Boris Veytsman - added --cut option
#
# ------------------------------------------------------------------------
use strict ;
use Getopt::Long;
use LWP::UserAgent ;
use HTTP::Request ;
use URI::Escape ;
use MIME::Parser ;
use MIME::Words qw/decode_mimewords encode_mimeword/ ;
use Unicode::MapUTF8 qw/to_utf8 from_utf8/ ;
use HTML::TokeParser ;
# Changed by LG - commented out configs.
# use Mail2LJ::Config ; # you can just remove every line mentioning
# # Mail2LJ::Config or $cfg
#
# my $cfg = $Mail2LJ::Config::conf ;
# Changed by LG - added shorname and version.
(my $shortname = $0) =~ s/^.*\///; # script name without path
my $Version = "0.9"; # Version number
my $LGmod = "-LG"; # Version modifier by LG
my $post_uri = "http://www.livejournal.com/cgi-bin/log.cgi" ;
my $ljcomment_action = 'http://www.livejournal.com/talkpost_do.bml';
# my $host = $ENV{MAIL2LJ_DOMAIN} || "mail2lj.nichego.net" ; # Changed by LG
# my $host = $ENV{MAIL2LJ_DOMAIN} || `hostname -f` ; # Changed by LG
my $host = $ENV{MAIL2LJ_DOMAIN} || "ledorub.poxod.com" ; # Changed by LG
# my $home = $ENV{HOME} || "/home/mail2lj" ; # Changed by LG
my $home = $ENV{HOME} || "/tmp/mail2lj" ;
# Changed by LG. Specifies the default incoming and outgoing charset for
# all e-mails (i.e, the posts CONTENT and the script replies).
# For incoming mails, the MIME header is analyzed and actual MIME charset
# overrides the default, of course.
# my $MailCharset = "cp1251";
my $MailCharset = "koi8-r";
# Changed by LG. Specifies the charset in which non-English characters
# FROM THE COMMAND LINE are entered. I.e. if I give a command line option
# '--subject ôÅÓÔ', the script needs to know the encoding to properly convert
# it to UTF8. I'm too lazy to analyze current locale, so I'll make it the
# user's responsibility. Override via '--charset' option.
# my $SystemCharset = "cp1251";
# my $SystemCharset = "utf8";
my $SystemCharset = "koi8-r";
# Translation table for smstrip_data() function. Only used whith aliases
# ljreply-... and ljreplys-...
my %tr = (
'á' => 'A', 'â' => 'B', '÷' => 'V', 'ç' => 'G', 'ä' => 'D', 'å' => 'E', '³' =>
'E', 'ö' => 'Zh', 'ú' => 'Z', 'é' => 'I', 'ê' => 'J', 'ë' => 'K', 'ì' => 'L',
'í' => 'M', 'î' => 'N', 'ï' => 'O', 'ð' => 'P', 'ò' => 'R', 'ó' => 'S', 'ô' =>
'T', 'õ' => 'U', 'æ' => 'F', 'è' => 'H', 'ã' => 'C', 'þ' => 'Ch', 'ý' => 'Sch',
'û' => 'Sh', 'ø' => '\'', 'ù' => 'Y', 'ÿ' => '\'', 'ü' => 'E', 'à' => 'Yu',
'ñ' => 'Ya', 'Á' => 'a', 'Â' => 'b', '×' => 'v', 'Ç' => 'g', 'Ä' => 'd', 'Å' =>
'e', '£' => 'e', 'Ö' => 'zh', 'Ú' => 'z', 'É' => 'i', 'Ê' => 'i', 'Ë' => 'k',
'Ì' => 'l', 'Í' => 'm', 'Î' => 'n', 'Ï' => 'o', 'Ð' => 'p', 'Ò' => 'r', 'Ó' =>
's', 'Ô' => 't', 'Õ' => 'u', 'Æ' => 'f', 'È' => 'h', 'Ã' => 'c', 'Þ' => 'ch',
'Û' => 'sh', 'Ý' => 'sch', 'Ø' => '\'', 'Ù' => 'y', 'ß' => '\'', 'Ü' => 'e',
'À' => 'yu', 'Ñ' => 'ya'
);
# ------------------------------------------------------------------------ #
# End configuration settings.
# ------------------------------------------------------------------------ #
# ------------------------------------------------------------------------ #
# Changed by LG - added parsing of command line.
# Changed by BV - added options cut
# ------------------------------------------------------------------------ #
my %Opt = (); # Main options go here
my $opt_h ; # Help flag
my $opt_bounces ; # Alternative error recipient flag
my $opt_addfrom ; # Add the From field to the post
my $opt_addfromh ; # Add the htmlized From to the post
my $opt_keepspaces ; # HTML-encode multiple spaces in e-mail
my @opt_taglist ; # command-line taglist first goes here
my $opt_ljcut ; # Add lj-cut after line number N
my $ljcut_delta = 5 ; # No lj-cut if less lines left after it
my $opt_ljcut_text ; # A text for lj-cut.
my $Parse = GetOptions( \%Opt,
'user|u=s',
'password|passwd|p=s',
'hpassword|hpasswd|hp=s',
'date|d=s',
'security|sec=s',
'prop_opt_preformatted|formatted|f!',
'prop_opt_backdated|backdated|back-dated|backdate|back-date|back!',
'subject|subj|s=s',
'taglist|tags|tag|t=s' => \@opt_taglist, # Will tweak
'notaglist|notags|notag|not|no-taglist|no-tags|no-tag|no-t' => sub {undef @opt_taglist},
'usejournal|use-journal|use|journal|j=s',
'prop_current_mood|current_mood|mood=s',
'prop_current_music|current_music|music=s',
'prop_picture_keyword|picture_keyword|picture|pic|userpic=s',
'comments|comment|c=s', # Will tweak below
'charset|enc=s' => \$SystemCharset,
'bounces|bounce|b=s' => \$opt_bounces,
'addfrom|add-from|from!' => \$opt_addfrom,
'addfromh|add-fromh|fromh!' => \$opt_addfromh,
'ljcut|lj-cut|cut|l=i'=>\$opt_ljcut,
'ljcut-text|lj-cut-text|cut-text|ljcuttext|cuttext=s'=>\$opt_ljcut_text,
'keep-spaces|keep-space|keepspaces|keepspace|spaces|space!' => \$opt_keepspaces,
'help|h' => \$opt_h,
);
# Handle bad options
if ( ! $Parse ) {
print_usage('short');
die "Run with '-h' for more help.\n\n";
}
# Print help if requested.
print_usage('long'), exit 0 if ($opt_h);
# Check if '--date' was specified and convert hash value to proper format
# for LJ request.
if ( exists $Opt{'date'} ) {
# Note: "DD.MM.YYYY HH:MM". Single-digit day, month and hour are allowed.
# Double-digit "YY" is also allowed and considered "2000 + YY"
if ( $Opt{'date'} =~ /(\d\d?)\.(\d\d?)\.(\d{2,4})\s+(\d\d?):(\d\d)/ ) {
$Opt{'day'} = $1 ;
$Opt{'mon'} = $2 ;
$Opt{'year'} = $3 ;
$Opt{'hour'} = $4 ;
$Opt{'min'} = $5 ;
$Opt{'year'} += 2000 if $Opt{'year'} < 100 ;
} else {
print STDERR "can't parse date '$Opt{'date'}', using current.\n" ;
}
delete $Opt{'date'} ; # And remove the old element.
}
# Comments option is 'comments yes/no/nomail', but LJ wants
# 'prop_opt_*no*comments' property. Keep command line human-readable and
# switch to proper value in the hash.
if ( exists $Opt{'comments'} ) {
if ( $Opt{'comments'} =~ /^s*((on)|(yes)|(default))\s*$/i ) {
$Opt{'prop_opt_nocomments'} = "" ;
} elsif ( $Opt{'comments'} =~ /^\s*(noe?mails?)\s*$/i ) {
$Opt{'prop_opt_nocomments'} = "" ;
$Opt{'prop_opt_noemail'} = 1 ;
} elsif ( $Opt{'comments'} =~ /^\s*((off)|(no))\s*$/i ) {
$Opt{'prop_opt_nocomments'} = 1
} else {
$Opt{'prop_opt_nocomments'} = $Opt{'comments'} ;
}
delete $Opt{'comments'} ; # And remove the old element.
}
# Convert taglist array into a single string and store it
# with other parameters.
$Opt{'prop_taglist'} = join( ", ", @opt_taglist ) if ( @opt_taglist ) ;
# Convert all command line options to unicode.
# Function href2utf8() uses a reference to input hash, so %Opt is
# being modified "in-place".
href2utf8( \%Opt, $SystemCharset) ;
# Changed by LG - set a restrictive umask (we're talking mail files here!)
umask 077 ;
# Changed by LG - moved from above.
my $alias = shift @ARGV || "none" ;
my $mp = new MIME::Parser() or die "new MIME::Parser(): $!\n" ;
# Changed by LG - changed directory.
# $mp->output_dir("$home/mimetmp") ;
$mp->output_dir("/tmp/mimetmp-".$ENV{USER}) ;
mkdir $mp->output_dir if not -d $mp->output_dir ; # Create it if missing
# Get the whole mail.
my $me = $mp->parse(\*STDIN) ;
END { $me && $me->purge() } ;
# Changed by LG - different log file name.
# open(STDERR, ">>$home/generic.log") or die "open(`log'): $!\n" ;
my $logdir = "$home/mail" ;
mkdir $logdir if not -d $logdir ; # Create it if missing
open(STDERR, ">>$logdir/mail2lj.log") or die "open(`log'): $!\n" ;
my $users = {} ;
# $users = $cfg->{users} ;
# Get mail header.
my $mh = $me->head() ;
$me->dump_skeleton(\*STDERR) ;
# Changed by LG - added chomping of "To:" field.
my $to = $me->get('To') || "" ;
chomp $to ;
print STDERR "Alias: $alias\n", "To: $to\n",
"Charset: ", $mh->mime_attr("content-type.charset") || "NONE", "\n" ;
my $xmailer = $mh->get('X-Mailer') || "unknown" ;
if ($xmailer =~ /EPOC/ || $xmailer =~ /Eudora.+PalmOS/) {
# too bad. they do violate standards there.
$mh->mime_attr("content-type.charset" => "windows-1251") ;
print STDERR "Charset changed to 'windows-1251' (hopefully)\n" ;
}
# And here we do posting.
if ($alias =~ /MAILER-DAEMON/i) {
exit 0 ;
} elsif ($alias =~ /^post$/) {
# my $req = post_me2req($me, "windows-1251") ; # Changed by LG
my $req = post_me2req($me, "$MailCharset", { %Opt }) ; # Changed by LG
my $ljres = submit_request($req) ;
if ($ljres->{'success'} eq "OK") {
print STDERR "journal updated successfully\n" ;
} else {
print STDERR "error updating journal: $ljres->{errmsg}\n" ;
send_bounce($ljres->{errmsg}, $me, $mh->mime_attr("content-type.charset")) ;
}
} elsif ($alias =~ /^post-(\w+)-(\w+)$/) {
my $l = $1 ;
my $p = $2 ;
# my $req = post_me2req($me, "windows-1251", { # Changed by LG
# user => $l,
# password => $p
my $req = post_me2req($me, "$MailCharset", { # Changed by LG
user => $l,
password => $p,
%Opt # Changed by LG
}) ;
my $ljres = submit_request($req) ;
if ($ljres->{'success'} eq "OK") {
print STDERR "journal updated successfully\n" ;
} else {
print STDERR "error updating journal: $ljres->{errmsg}\n" ;
send_bounce($ljres->{errmsg}, $me, $mh->mime_attr("content-type.charset")) ;
}
} elsif ($alias =~ /^hpost-(\w+)-(\w+)$/) {
my $l = $1 ;
my $hp = $2 ;
# my $req = post_me2req($me, "windows-1251", { # Changed by LG
# user => $l,
# hpassword => $hp
my $req = post_me2req($me, "$MailCharset", { # Changed by LG
user => $l,
hpassword => $hp,
%Opt # Changed by LG
}) ;
my $ljres = submit_request($req) ;
if ($ljres->{'success'} eq "OK") {
print STDERR "journal updated successfully\n" ;
} else {
print STDERR "error updating journal: $ljres->{errmsg}\n" ;
send_bounce($ljres->{errmsg}, $me, $mh->mime_attr("content-type.charset")) ;
}
} elsif ($alias =~ /^ljreply-(\S+)$/ || $alias =~ /^ljreplys-(\S+)$/) {
my $email = $1 ;
$email =~ s/\.\./\@/ ;
if ($mh->get('From') !~ m/lj_dontreply\@livejournal.com/ &&
$mh->get('From') !~ m/lj_notify\@livejournal.com/) {
# someone just picked our email from livejournal.com site
print STDERR "no livejournal signature found, bouncing to $email\n";
$mh->replace('To', $email) ;
$me->send("sendmail") ;
exit 0 ;
}
die "ljreply doesn't look like a 2-part message.\n"
unless $me->parts() == 2 ;
my $formdata = ljcomment_form2string
$me->parts(1)->bodyhandle->as_string() ;
# Changed by LG - changed to a variable.
# my $charset =
# ($me->parts(0)->head->mime_attr('content-type.charset') ||
# "windows-1251") ;
my $charset =
($me->parts(0)->head->mime_attr('content-type.charset') ||
"$MailCharset") ;
my $data = $me->parts(0)->bodyhandle->as_string() ;
my $nicefrom = "Mail2LJ-translated comment" ;
if ($mh->get("From") =~ /\(([^\)]+)\)/) {
$nicefrom = $1 ;
}
print STDERR "nicefrom is '$nicefrom'\n" ;
if ($alias =~ /^ljreplys/) {
print STDERR "stripping content...\n" ;
$data = to_utf8({ -string => $data, -charset => $charset})
if $charset !~ /^utf-?8$/i ;
# Changed by LG - changed to a variable.
# $data = from_utf8({ -string => $data, -charset => "cp1251"}) ;
# $charset = "windows-1251" ;
$data = from_utf8({ -string => $data, -charset => "$MailCharset"}) ;
$charset = "$MailCharset" ;
$data = smstrip_data $data ;
}
my $msg = build MIME::Entity(
'From' => "ljfrom-$formdata\@$host",
# 'Sender' => "ljfrom-$formdata\@$host",
'To' => $email,
'Subject' => normalize_header($mh->get('Subject'), $charset),
'Content-Type' => "text/plain; charset=$charset" ,
'Data' => $data
);
$msg->send("sendmail") ;
$msg->purge() ;
} elsif ($alias =~ /^ljfrom-(\S+)$/) {
my $formdata = $1 ;
my $hr = ljcomment_string2form($formdata) ;
my $req = new HTTP::Request('POST' => $ljcomment_action)
or die "new HTTP::Request(): $!\n" ;
$hr->{usertype} = 'user' ;
# Changed by LG.
# $hr->{encoding} = $mh->mime_attr('content-type.charset') ||
# "cp1251" ;
$hr->{encoding} = $mh->mime_attr('content-type.charset') ||
"$MailCharset" ;
$hr->{subject} = decode_mimewords($mh->get('Subject'));
$hr->{body} = $me->bodyhandle->as_string() ;
$req->content_type('application/x-www-form-urlencoded');
$req->content(href2string($hr)) ;
my $ljres = submit_request($req, "comment") ;
if ($ljres->{'success'} eq "OK") {
print STDERR "journal updated successfully\n" ;
} else {
print STDERR "error updating journal: $ljres->{errmsg}\n" ;
send_bounce($ljres->{errmsg}, $me, $mh->mime_attr("content-type.charset")) ;
}
}
print STDERR "-------------------------------------------------------------\n" ;
# ------------------------------------------------------------------------- #
# All done.
# ------------------------------------------------------------------------- #
exit 0 ;
# ------------------------------------------------------------------------- #
# Subroutines from now down.
# ------------------------------------------------------------------------- #
sub href2utf8 {
my ($hr, $e) = @_ ;
my $i ;
foreach $i (keys %$hr) {
$hr->{$i} = to_utf8({ -string => $hr->{$i}, -charset => $e}) ;
}
return $hr ;
}
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 ;
}
sub post_body2href {
my $fh = shift ;
my ($l, $auth) ;
my $req_data = {
webversion => 'full',
ver => 1,
security => 'public',
prop_opt_preformatted => 0,
mode => 'postevent'
} ;
while ($l = $fh->getline()) {
if (exists $req_data->{event}) {
$req_data->{event} .= $l ;
next ;
}
next if $l =~ /^$/ ;
if ($l =~ /^(\w[\w_]*[\w])\s*[=:]\s*(\S.*)$/) {
my ($var, $val) = (lc($1), $2) ;
if ($var eq "date") {
# Changed by LG.
# Note: "DD.MM.YYYY HH:MM". Single-digit day, month and
# hour are allowed. Double-digit "YY" is also allowed
# and considered "2000 + YY".
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 ;
} else {
print STDERR "can't parse date '$val', will use current\n" ;
}
} 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") {
$val = 1 if $val =~ /^\s*((on)|(yes))\s*$/i ;
$val = 0 if $val =~ /^\s*((off)|(no))\s*$/i ;
# Changed by LG - "autoformat" is opposite to "formatted".
# Add 0 to make sure it's the number.
$val = 0 + (not $val) if ($var eq "autoformat") ;
$req_data->{prop_opt_preformatted} = $val ;
} elsif ($var eq "auth") {
$auth = $val ;
# Changed by LG - added 'backdated' option. Remember,
# Livejournal currently prohibits backdated entries in the
# communities (as opposed to individual journals).
} elsif ($var =~ /^back-?dated?$/ || $var eq "opt_backdated") {
$val = 1 if $val =~ /^\s*((on)|(yes))\s*$/i ;
$val = 0 if $val =~ /^\s*((off)|(no))\s*$/i ;
$req_data->{prop_opt_backdated} = $val ;
# Changed by LG - added comment-parsing settings.
# Comments: default/on/yes | off/no | nomail
# Assembled based on data from form values in the browser
# and from info on
# http://www.livejournal.com/doc/server/ljp.csp.flat.postevent.html
# http://www.livejournal.com/doc/server/ljp.csp.proplist.html
} elsif ($var eq "comments" || $var eq "comment"
|| $var eq "comment_settings"
|| $var eq "comments_settings" ) {
if ( $val =~ /^\s*((on)|(yes)|(default))\s*$/i ) {
# Journal default
$val = "" ;
$req_data->{comment_settings} = $val ;
$req_data->{prop_opt_nocomments} = $val ;
} elsif ( $val =~ /^\s*(noe?mails?)\s*$/i ) {
# No emails
$val = "1" ;
$req_data->{prop_opt_nocomments} = (not $val) + 0;
$req_data->{prop_opt_noemail} = $val ;
} elsif ( $val =~ /^\s*((off)|(no))\s*$/i ) {
# No comments
$val = "1" ;
$req_data->{prop_opt_nocomments} = $val ;
} else {
# Anything else.
$req_data->{comment_settings} = $val ;
}
# Changed by LG - added 'tags' option.
} elsif ($var =~ /^tags?$/ || $var eq "taglist") {
$req_data->{prop_taglist} = $val;
# Changed by LG - added 'notags' option. Empty the preceding
# taglist if set to true, otherwise do nothing
} elsif ($var =~ /^no-?tags?$/ || $var eq "no-?taglist") {
$req_data->{prop_taglist} = "" if $val =~ /^\s*((on)|(yes))\s*$/i ;
# Anything else - just assign.
} else {
$req_data->{$var} = $val ;
}
} else {
$req_data->{event} = $l ;
}
}
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 ;
}
return $req_data ;
}
sub hdr2utf8 {
my ($s, $e) = @_ ;
my $r = "" ;
my $i ;
foreach $i (decode_mimewords $s) {
$r .= to_utf8({
-string => $i->[0],
-charset => ($i->[1] || $e)
}) ;
}
return $r ;
}
sub post_me2req {
my ($me, $e, $hints) = @_ ;
my $mebh = $me->bodyhandle() or die "post_message(): no body?\n" ;
my $mehh = $me->head() ;
my $charset = $mehh->mime_attr("content-type.charset") || $e ;
my $subject = hdr2utf8($me->get('Subject') || "", $charset) ;
chomp $subject ; # Changed by LG
# Changed by LG
my $from = hdr2utf8($me->get('From') || "", $charset) ;
chomp $from ;
my $hr = href2utf8(post_body2href($mebh->open("r")), $charset) ;
my $req = new HTTP::Request('POST', $post_uri) or
die "new HTTP::Request(): $!\n" ;
if ($hints) {
my $i ;
foreach $i (keys %$hints) {
# Changed by LG - make hints override (not just complement)
# existing values.
# $hr->{$i} ||= $hints->{$i} ;
$hr->{$i} = $hints->{$i} ;
}
}
$hr->{subject} ||= $subject ;
# Changed by LG - removed prefixing.
# $hr->{subject} = "[mail2lj] " . $hr->{subject} ;
# Changed by LG - added options to add the 'From' field to the
# posted message.
if ( $opt_addfrom ) {
$hr->{event} = "From: $from" . "\n\n" . $hr->{event} ;
} elsif ( $opt_addfromh ) {
my $html_from = "<nobr><i><b>From:</b> $from</i></nobr>" ;
$html_from =~ s/\@/[_\@_]/g ;
$hr->{event} = $html_from . "\n\n" . $hr->{event} ;
}
# Changed by LG - added an option to preserve (html-ize) multiple
# spaces and tabs (convert '\t' to eight ' ' and convert
# multiple continuous spaces into sequence of ' ').
# Lines with tabs are additionally wrapped in <nobr>...</nobr> tags.
if ( $opt_keepspaces ) {
$hr->{event} =~ s/^(.*\t.*)$/<nobr>$1<\/nobr>/gm ;
$hr->{event} =~ s/\t/\ \ \ \ \ \ \ \ /g ;
$hr->{event} =~ s/ / \ /g ;
}
#
# Change by BV - added the option to put lj-cut after '--cut XX' lines
#
# Tweaked by LG - only adding lj-cut if more than $ljcut_delta lines
# is left in the posting.
#
if ($opt_ljcut>0) {
my $nlines = scalar( my @junk=split( /\n/, $hr->{event}, -1) ) - 1;
my $start=0;
for (my $i=0; $i<$opt_ljcut; $i++) {
$start=index($hr->{event},"\n",$start)+1;
if ($start == 0) {
last;
}
}
# And insert the lj-cut if not too close to the end of the post.
if ($start>0 ) {
if ( $nlines >= $opt_ljcut+$ljcut_delta ) {
my $ljcut = ( $opt_ljcut_text =~ /^\s*$/ ) ?
'<lj-cut>' :
'<lj-cut text="' . $opt_ljcut_text . '">' ;
substr($hr->{event}, $start,0) = $ljcut ;
} else {
print STDERR "'--cut $opt_ljcut' requested, which is " .
"within $ljcut_delta of the total $nlines " .
"lines. Skipping lj-cut.\n" ;
}
}
}
$req->content_type('application/x-www-form-urlencoded');
$req->content(href2string $hr) ;
print STDERR "working on request from $hr->{user}\n",
"From: $from\n", # Changed by LG
"Date: ", scalar localtime, "\n" ;
return $req ;
}
sub submit_request {
my ($req, $proto) = @_ ;
my $ljres = {} ;
my $ua = new LWP::UserAgent or
die "new LWP::UserAgent: $!\n" ;
# Changed by LG - modified user-agent
# $ua->agent("Mail2LJ/0.9");
$ua->agent("Mail2LJ/${Version}${LGmod}");
$ua->timeout(100);
my $res = $ua->request($req);
if ($proto && $proto eq "comment") {
if ($res->is_success) {
$ljres->{'success'} = "OK";
} else {
$ljres->{'success'} = "FAIL";
$ljres->{'errmsg'} = "Client error: Error contacting server.";
}
return $ljres ;
}
if ($res->is_success) {
%$ljres = split(/\n/, $res->content);
} else {
$ljres->{'success'} = "FAIL";
$ljres->{'errmsg'} = "Client error: Error contacting server.";
}
return $ljres ;
}
sub ljcomment_form2string {
my $s = shift ;
my $h = {} ;
my $p = new HTML::TokeParser(\$s) or
die "new HTML::TokeParser(): $!\n" ;
my $token = $p->get_tag("form");
die "get_inputs(): Wrong form.\n"
if ($token->[1]{action} ne $ljcomment_action) ;
while ($token = $p->get_tag("input") ) {
$h->{$token->[1]{name}} =
$token->[1]{value} || '' if ($token->[1]{name});
}
die "get_inputs(): Incomplete form data\n"
unless $h->{userpost} && $h->{journal} && $h->{parenttalkid} &&
$h->{itemid} && $h->{ecphash} ;
$h->{ecphash} =~ s/^ecph-// ;
return "$h->{userpost}-$h->{journal}-$h->{parenttalkid}-$h->{itemid}-$h->{ecphash}" ;
}
sub ljcomment_string2form {
my $s = shift ;
my $hr = {} ;
my $i ;
my @l = split /\-/, $s ;
foreach $i (qw/userpost journal parenttalkid itemid ecphash/) {
$hr->{$i} = shift @l ;
}
die "badly formed formdata '$s'\n" unless $hr->{ecphash} ;
$hr->{ecphash} = "ecph-" . $hr->{ecphash} ;
return $hr ;
}
sub normalize_header {
my ($s, $e) = @_ ;
my $d = decode_mimewords($s) ;
chomp $d ;
return encode_mimeword($d, 'B', $e) ;
}
sub smstrip_data {
my $data = shift ;
my ($hdr, $ftr) ;
my ($who, $journal) ;
$data =~ /^(.+)Their reply was:(.+)You can view the discussion(.+)$/si
or return $data ;
$hdr = $1 ;
$data = $2 ;
$ftr = $3 ;
$hdr =~ /\((\w+)\) replied to .* ((post)|(comment))/ and $who = $1 ;
$ftr =~ m,http://www\.livejournal\.com/talkpost.bml\?journal=(\w+),
and $journal = $1 ;
if ($who) {
$data = "user [$who] in [$journal]:\n" . $data ;
}
$data =~ s/^\s+Subject:\s*$//m ;
$data =~ s/^\s+Subject:\s(\S.*)\s*$/[$1]/m ;
$data =~ s/\s+/ /gs ;
$data =~ s/(.)/$tr{$1} || $1/ge ;
return $data ;
}
sub send_bounce {
my ($errmsg, $orig, $charset) = @_ ;
# Changed by LG - use KOI-8 instead of Win-1251.
# $charset ||= "windows-1251" ;
$charset ||= "$MailCharset" ;
my $bmsg = build MIME::Entity(
'From' => "MAILER-DAEMON\@$host",
# Changed by LG - allow use of alternative addres for notifications.
# 'To' => $orig->get('From'),
'To' => $opt_bounces || $orig->get('From'),
'Subject' => (
"mail2lj failure (was: " . $orig->get('Subject') . ")"
),
'Content-Type' => "text/plain; charset=$charset" ,
'Data' => <<EOF
Dear Mail2Lj User,
Mail2Lj gateway at $host was trying hard to submit your request,
but, unfortunately, to no avail: a silly, but fatal error has occured.
Mail2Lj(tm) proudly presents the extremely informative error message:
'$errmsg'
Thank you for understanding,
good luck next time,
take care,
sincerely, completely and, in general, very truly yours,
-Mail2Lj.
EOF
);
$bmsg->send("sendmail") ;
$bmsg->purge() ;
}
sub print_usage {
# ----------------------------------------------------------------------- #
# print_usage( $Long );
#
# Prints help message. If defined $Long, the message is more detailed
# as opposed to default brief description.
# ----------------------------------------------------------------------- #
my ( $long ) = @_; # Were we called with a parameter?
my $spacer = ' ' x length($shortname); # bunch of spaces
# ---------------------------------------------------------------------
# Short usage will always be printed when called.
# Indentation messed up because of the HERE-document.
# ---------------------------------------------------------------------
print <<___END_SHORT;
$shortname v. ${Version} by jason\@nichego.net (http://jsn.livejournal.com).
Tweaked to v. ${Version}${LGmod} by Lev Gorenstein \<lev\@ledorub.poxod.com\>, 2007.
Usage:
$shortname ACTION [options] < InputFile
cat MailMessage | $shortname ACTION [options]
A script to post incoming mail messages to Livejournal.com journals.
Reads STDIN and connects to Livejournal's HTTP posting interface.
This is a modification of mail2lj.pl script by Jason
(http://jsn.livejournal.com) described at http://mail2lj.nichego.net/.
I added command line processing and couple more tweaks.
Distributed freely under GNU Public License with absolutely no warranty.
___END_SHORT
# ---------------------------------------------------------------------
# When called in a long format, usage should be followed by some more info.
# Indentation messed up because of the HERE-document.
# ---------------------------------------------------------------------
if ( defined $long && $long !~ /^\s*short\s*$/i ) {
print <<______END_HELP;
ACTIONS:
post Original script used this to handle messages that had keywords
inside (see http://mail2lj.nichego.net/userguide.html) and
used 'post-...' and 'hpost-...' to post keywordless messages
directly. This version doesn't require keywords (i.e. 'post'
can handle keywordless messages and everything can be set via
command line), but if you DO use keywords, then use this action.
post-(user)-(password)
A direct post of mail message (without looking for keywords in
the body) using whatever settings supplied on the command line.
With proper command line parameters, username and password can
be completely bogus (i.e. 'post-aa-bb -u RealUser -p RealPass').
hpost-(user)-(MD5Hash_of_password)
A direct post of mail message (without looking for keywords in
the body) using whatever settings supplied on the command line,
Same as 'post-...', but uses a password hash instead of
clear-text password.
With proper command line parameters, username and hash can be
completely bogus (i.e. 'hpost-aa-bb -u RealUser --hp RealHash').
Options:
-u USER, --user USER
Use this LiveJournal user name to login.
-p PASS, --password PASS
Use this LiveJournal password to login. Use of this option
is deprecated because of clear-text password.
-hp MD5Hash, --hpassword MD5Hash
Use this MD5 hash of the password to login. To generate a hash,
do this:
perl -MDigest::MD5 \
-e 'print Digest::MD5::md5_hex("PASSWORD")."\\n"'
-j JOURNAL, --usejournal JOURNAL
When posting to the community (or the journal that's different
from the one you've specified via '--user'), use this option
to specify that community's name. E.g. if the user
'gusarskie_vesti' wants to post to community 'gusary', it can
be done with options like this:
post -u gusarskie_vesti -p PASS --usejournal gusary
-s SUBJECT, --subject SUBJECT
Use this subject for the posting. If absent, defaults to
e-mail's Subject:.
-t TAGLIST, --tags TAGLIST
Use tags from TAGLIST for posted message. Within a tag list,
tags should be separated by commas. If your tags contain
special characters or spaces, make sure to enclose TAGLIST in
single or double quotes to protect from the shell. Multiple
'-t' options are allowed and taglists will be combined.
--notaglist, --notags
Unsets all previously defined tags. Thus, a call to
$shortname ... --tags X --tags Y ... --notags --tags Z
will yield a taglist consisting of just "Z". This option is
rarely needed and added only for the sake of completeness.
-d DATE, --date DATE
Label posting with this date. Date should be in LiveJournal's
format: DD.MM.YYYY HH:mm. If absent, current date/time is used.
--backdated
If set, tells LiveJournal to make this message back-dated
(i.e. to set 'Date out-of-order' flag to prevent this item
from showing in people's friends lists). Note that currently
Livejournal only allows back-dated entries in individual
journals (not in communities), so use with caution. The option
can be negated ('--nobackdated'). Default is '--nobackdated'.
--security public|protected|private
Post security mode. Default is "public".
-f, --formatted
If set, tells LiveJournal to assume our message to be already
formatted (i.e. '--formatted' turns OFF LJ's autoformat
feature). The option can be negated ('--noformatted').
Default is '--noformatted' (i.e. *use* LJ's autoformat).
--mood MOOD Current Mood for Livejournal. TEXT ONLY (images not supported).
Defaults to nothing.
--music MUSIC Current Music for Livejournal. Defaults to nothing.
--picture KEYWORD, --userpic KEYWORD
Keyword for the Livejournal userpic to use. Default one is
used when not specified.
-c on|yes|default|off|no|noemail, --comments on|yes|default|off|no|noemail
Controls permissions to leave comments for this post.
"on" ("yes", "default") will use the journal's default settings.
"off" or "no" prohibit comments. "noemail" allows comments,
but tells Livejournal not to email them to you.
--from, --addfrom
Insert the From: field from the e-mail as the first line of
the posted message. The field is added in plain text (without
any HTML-formatting - see '--fromh' for that). For slight
antispam protection, '\@' is replaced by '[_\@_]'. The option
can be negated ('--nofrom'). Default is '--nofrom'.
--fromh, --addfromh
Same as '--from', but uses HTML-markup to highlight inserted
field (<nobr><i><b>From:</b> Address</i></nobr>). This is
nice for mailing list -> Livejournal crossposting. The option
can be negated ('--nofromh'). Default is '--nofromh'.
--spaces, --keepspaces
Normally the script does not change original message text,
and all of it is preserved in the body of resulting LJ post.
Which means that all tabs and multiple consecutive spaces
(while valid in e-mail and preserved in the post), will not
be properly *shown* in the browser (browser will display them
as single space). With '--spaces', however, all tabs will
be converted to 8 '\ ' instances, and each pair of
consecutive spaces will be converted to a ' \ ' sequence.
Additionally, lines with tabs will be wrapped in <nobr> tag.
This way the formatting of original e-mail will be much
better preserved in the journal. The option can be negated
('--nospaces'). Default is '--nospaces'.
--ljcut NUM, --cut NUM, -l NUM
Inserts '<lj-cut>' after NUM lines of the post content.
If the resulting lj-cut happens to be within $ljcut_delta lines from
the end of the post, the cut will not be added.
--ljcut-text TEXT, --cut-text TEXT, --cuttext TEXT
Text to use as lj-cut text parameter (in <lj-cut text="TEXT">).
If the text contains nothing but whitespace, it is ignored.
Remember to quote spaces and special characters from the shell.
--charset CHARSET
This option tells the script that all COMMAND LINE options are
given in this charset. Default is "$SystemCharset".
Remember, THIS HAS NOTHING TO DO with the __posting's charset__
(which is determined from email headers and then converted to
utf8). It also has absolutely no effect on the in-the-body
keywords (they are also governed by email's charset). This
option is meaningful ONLY for the text that you supply VIA
COMMAND LINE (e.g. '-s Subject' or '--cuttext TEXT').
-b xxx\@yyy, --bounces xxx\@yyy
Normally, if errors occur during posting (e.g. wrong password),
the script sends an error notification to the _original poster_
(i.e., the address in the original From: field). This makes
perfect sense for multi-user installations. But occasionally
there is a need to send all errors to a single _maintainer_
(e.g., if you use the script as a mailing list --> LiveJournal
gateway). This option allows exactly that. Default is unset
(i.e. errors go to original poster).
-h, --help: This help.
If you decide to use keywords in the body of the message (as opposed to
command line options), they should look like this:
From: .... \\
To: .... + # Regular e-mail headers
Subject: ... /
# Normal blank line after headers
User: gusarskie_vesti
Password: password # (or Hpassword: MD5Hash)
Date: 22.01.2007 5:04
Security: private
Subject: Rzhevskij zhiv!
Tags: Junk, Viva Rzhevskij!
Notags: yes # Clears all preceding tags
Formatted: on # Or equivalent "Autoformat: off"
Usejournal: gusary
Mood: okay
Music: silence
Backdated: yes
Comments: no
# Blank line
Oh well. some text # Text of your message.
And the text would be posted.
Almost all keyword fields (as well as their command line counterparts)
are optional and have reasonable defaults. The only mandatory parameter
is the user name (well, doh!). See more on keywords in the original
script's user guide: http://mail2lj.nichego.net/userguide.html
______END_HELP
print "\n";
} # End of "if $long" test
# ---------------------------------------------------------------------
# All done
# ---------------------------------------------------------------------
return;
}
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>