#!/usr/bin/perl -w
#
# M2WP - 1.01
#
# This is M2WP - Mail 2 WordPress, an SMTP mail robot, which can
# be used to post blog entries to a wordpress blog.
#
# By  accessing  this  software,  M2WP, you  are  duly  informed
# of and agree to be  bound  by the  conditions  described below
# in this notice:
#
# This  software  product,  M2WP,  is developed by Thomas Linden
# and  copyrighted  (C)  2005-2006  by  Thomas Linden,  with all
# rights reserved.
#
# There  is  no charge for M2WP software.  You  can redistribute
# it and/or modify it under the terms of the GNU  General Public
# License, which is incorporated by reference herein.
#
# M2WP is distributed  WITHOUT ANY WARRANTY, IMPLIED OR EXPRESS,
# OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE or that
# the use of it will not infringe on any third party's intellec-
# tual property rights.
#
# You  should  have received a copy of the  GNU  General  Public
# License  along  with M2WP.   Copies can also be obtained from:
#
#   http://www.gnu.org/licenses/gpl.txt
#
# or by writing to:
#
#   Free Software Foundation, Inc.
#   59 Temple Place, Suite 330
#   Boston, MA 02111-1307
#   USA
#
# Or contact:
#
#  "Thomas Linden" <tom@daemon.de>
#
# Find more details about M2WP on its website:
#
#  http://www.daemon.de/M2wp
#

use Data::Dumper;
use MIME::Parser;
use MIME::QuotedPrint;
use WWW::Mechanize;
use Config::General;
use Getopt::Long;
use strict;

my $me = "m2wp";
my $VERSION = 1.01;

#
# Default config
my $ConfigFile = "/etc/m2wp.conf";

#
# commandline options
my($o_h, $o_c, $o_v);
Getopt::Long::Configure( qw(no_ignore_case));
if (! GetOptions (
		  "config|c=s" => \$o_c,
		  "version|v"  => \$o_v,
                  "help|h"     => \$o_h,
		 )    ) {
  &usage;
}

if ($o_h) {
  &usage;
}

if ($o_v) {
  &finish("m2wp version $VERSION\n", 0);
}

my %conf = ParseConfig(-ConfigFile => $o_c ? $o_c : $ConfigFile);
my %category = %{$conf{Categories}};

if (! -d $conf{ParserDirectory}) {
    mkdir($conf{ParserDirectory}) or die "Could not create $conf{ParserDirectory}: $!\n";
}

chdir($conf{ParserDirectory}) or die "Could not chdir to $conf{ParserDirectory}: $!\n";


#
# end of user customizable part
#
my $parser = new MIME::Parser;
my $entity = $parser->parse(\*STDIN);
$parser->decode_headers(1);
$parser->extract_uuencode(1);
$parser->output_to_core(1);
my @mail_header = split('\n', $entity->head->as_string);
my %head;
my %meta;

foreach my $entry (@mail_header) {
   if ($entry =~ /(From|Subject|Message-ID): \s*(.*)$/) {
       my $field = $1;
       my $value = $2;
       $head{$field} = $value;
   }
   elsif ($entry =~ /X-Meta-([^:]+): \s*(.*)$/i) {
     $meta{$1} = $2;
   }
}

my @FromLine = reverse (split /\s\s*/, $head{From});
my $mail = $FromLine[0];
if (! $mail) {
    # no name part
    $mail = $head{From};
}
$head{From} = $mail;
$head{From} =~ s/[<>]//g;

# check email from for permission
my @allowed = split /\s*,\s*/, $conf{AllowedEmail};
if (! grep { $head{From} =~ /$_/i } @allowed ) {
    &finish("Permission denied for \"$head{From}\"!\n", 1);
}

my ($subject, $cat) = split /\s*\-\s*/, $head{Subject};
$head{Subject} = $subject;
if (! $cat) {
    # we will not post a category in this case, see below
    $head{Category} = undef;
}
else {
    $head{Category} = $category{$cat};
}


my ($body, $data, @images);
if ($body = $entity->bodyhandle) {
  # simple mail, no multipart mime-types here
  $data = decode_qp( $entity->bodyhandle->as_string );
}
else {
    # multipart mail
    my @parts = $entity->parts();
    foreach my $part (@parts) {
	if ( $part->effective_type() eq "text/html" ) {
            # prefer HTML
            $data = $part->stringify();
            $data =~ s/<(font|body|meta|html)[^>]*>//isg;
	    $data =~ s/<\!doctype[^>]*>//isg;
	    $data =~ s/<\/(font|meta|body|html)>//isg;
	    $data =~ s/<head[^>]*>.+<\/head>//isg;
	    last;
	}
	elsif ( $part->effective_type() eq "text/plain" ) {
	    # store it anyway, will be overwritten if html is available
	    $data = decode_qp( $part->stringify() ); # FIXME is this legal?
	}
	else {
            # images are stored automatically, just fetch the names
	    if ( $part->effective_type() =~ /image/ ) {
                my $data = $part->stringify();
		if ($data =~ /Content-Disposition: attachment; filename="(.+\.(jpg|gif|png|bmp|jpeg))"/i) {
		    push @images, $1;
		}
	    }
	}
    }
}

$data =~ s/content-(type|disposition|Transfer-Encoding): .+//ig;



my $mech = WWW::Mechanize->new();

# fetch login page and login
$mech->get($conf{BlogAdminURI});

if (! $mech->success ) {
  &finish("Connect failed: ". $mech->response->status_line, 1);
}

$mech->field('log', $conf{LoginUser});
$mech->field('pwd', $conf{LoginPasswd});
$mech->submit(); # to get login cookie

if (! $mech->success ) {
  &finish("Login page submit failed: ". $mech->response->status_line, 1);
}

# are there any image attachments?
if (@images) {
  my @errors;
  $mech->get("$conf{BlogAdminURI}/upload.php");

  if (! $mech->success ) {
    &finish("Fetch upload form failed: ". $mech->response->status_line, 1);
  }

  my $maxsize = $mech->value("MAX_FILE_SIZE");
  foreach my $image (@images) {
    my $imagesize = -s $image;
    if ($data =~ /\Q$image\E/gs && $imagesize <= $maxsize) {
      # referenced in the posting and within size border, so upload it
      $mech->field("img1" => $image);
      $mech->field("thumbsize", "none");
      $mech->field("submit", "upload");
      my $result = $mech->click();

      if (! $mech->success ) {
	&finish("Upload failed: ". $mech->response->status_line, 1);
      }

      if ($result->content() !~ /href="upload.php"/is) {
	# it seems, that the only indication of a successful upload
	# is the occurence of the "upload another one" link, am I right?
	if ($result->content() =~ /<em>(.+)<\/em>/is) {
	  # page contains a parsable error
	  push @errors, "failed to upload $image: $1!\n";
	}
	else {
	  # leave the user unillumined [sic]
	  push @errors, "failed to upload $image!\n";
	}
      }

      # we reached this point so upload worked, prepend the image path
      # in the posting with the upload path
      $data =~ s/\Q$image\E/$conf{ImageBaseURI}${image}/g;
    }
  }
  if (@errors) {
    &finish("@errors", 1);
  }
}


# fetch blog posting page and form
$mech->get("$conf{BlogAdminURI}/post.php");

if (! $mech->success ) {
  &finish("Posting page fetch failed: ". $mech->response->status_line, 1);
}

# which form to use
$mech->form('post');

# fetch the user_ID value
my $user_ID = $mech->value("user_ID");
if (! $user_ID ) {
    &finish("Unable to fetch user_ID, could not login?\n", 1);
}

# fill in all required fields
if ( $head{Category} ) {
   # if not specified, don't post it, thus use WP's default
   $mech->tick("post_category[]", "$head{Category}");
   $mech->tick("post_category[]", "$category{$conf{DefaultCategory}}", undef);
}

$mech->field("post_title", $head{Subject});
$mech->field("content", $data);
$mech->field("publish", "Ver&ouml;ffentlichen");
$mech->field("comment_status", "open");
$mech->field("post_status", "publish");

foreach my $key(keys %meta) {
  # FIXME how to submit multiple userdefined variables in WP?
  $mech->field("metakeyselect", $key);
  $mech->field("metavalue", $meta{$key});
}

# post the crab
my $result = $mech->submit();

if (! $mech->success ) {
  &finish("Posting failed: ". $mech->response->status_line, 1);
}

if ($result->{_content} !~ /<div class="updated">/is) {
  # FIXME fetch error message from $result, if any (?)
  &finish("Failed to post to blog!\n", 1);
}



sub usage {
  &finish("${me} [-vh] [-c <config>]\n", 0);
}

sub finish {
  my ($message, $exit) = @_;
  system("rm -f $conf{ParserDirectory}/msg-$$.*");
  print STDERR $message;
  exit $exit || 0;
}


1;
