Finance::Bank::Postbank_de - Check your Postbank.de bank account from Perl


Finance-Bank-Postbank_de documentation Contained in the Finance-Bank-Postbank_de distribution.

Index


Code Index:

NAME

Top

Finance::Bank::Postbank_de - Check your Postbank.de bank account from Perl

SYNOPSIS

Top

  use strict;
  require Crypt::SSLeay; # It's a prerequisite
  use Finance::Bank::Postbank_de;
  my $account = Finance::Bank::Postbank_de->new(
                login => '9999999999',
                password => '11111',
                status => sub { shift;
                                print join(" ", @_),"\n"
                                  if ($_[0] eq "HTTP Code")
                                      and ($_[1] != 200)
                                  or ($_[0] ne "HTTP Code");

                              },
              );
  # Retrieve account data :
  my $retrieved_statement = $account->get_account_statement();
  print "Statement date : ",$retrieved_statement->balance->[0],"\n";
  print "Balance : ",$retrieved_statement->balance->[1]," EUR\n";

  # Output CSV for the transactions
  for my $row ($retrieved_statement->transactions) {
    print join( ";", map { $row->{$_} } (qw( tradedate valuedate type comment receiver sender amount ))),"\n";
  };

  $account->close_session;
  # See Finance::Bank::Postbank_de::Account for
  # a simpler example

DESCRIPTION

Top

This module provides a rudimentary interface to the Postbank online banking system at https://banking.postbank.de/. You will need either Crypt::SSLeay or IO::Socket::SSL installed for HTTPS support to work with LWP.

The interface was cooked up by me without taking a look at the other Finance::Bank modules. If you have any proposals for a change, they are welcome !

WARNING

Top

This is code for online banking, and that means your money, and that means BE CAREFUL. You are encouraged, nay, expected, to audit the source of this module yourself to reassure yourself that I am not doing anything untoward with your banking data. This software is useful to me, but is provided under NO GUARANTEE, explicit or implied.

WARNUNG

Top

Dieser Code beschaeftigt sich mit Online Banking, das heisst, hier geht es um Dein Geld und das bedeutet SEI VORSICHTIG ! Ich gehe davon aus, dass Du den Quellcode persoenlich anschaust, um Dich zu vergewissern, dass ich nichts unrechtes mit Deinen Bankdaten anfange. Diese Software finde ich persoenlich nuetzlich, aber ich stelle sie OHNE JEDE GARANTIE zur Verfuegung, weder eine ausdrueckliche noch eine implizierte Garantie.

METHODS

Top

new

Creates a new object. It takes three named parameters :

login => '9999999999'

This is your account number.

password => '11111'

This is your PIN.

status => sub {}

This is an optional parameter where you can specify a callback that will receive the messages the object Finance::Bank::Postbank produces per session.

$account->new_session

Closes the current session and logs in to the website using the credentials given at construction time.

$account->close_session

Closes the session and invalidates it on the server.

$account->agent

Returns the WWW::Mechanize object. You can retrieve the content of the current page from there.

$session->account_numbers

Returns the account numbers. Only numeric account numbers are returned - the credit card account numbers are not returned.

$account->select_function STRING

Selects a function. The currently supported functions are

	accountstatement
	quit

$account->get_account_statement

Navigates to the print version of the account statement. The content can currently be retrieved from the agent, but this will most likely change, as the print version of the account statement is not a navigable page. The result of the function is either undef or a Finance::Bank::Postbank_de::Account object.

session_timed_out

Returns true if our banking session timed out.

maintenance

Returns true if the banking interface is currently unavailable due to maintenance.

TODO:

Top

  * Add even more runtime tests to validate the HTML
  * Streamline the site access to use even less bandwidth

AUTHOR

Top

Max Maischein, <corion@cpan.org>

SEE ALSO

Top

perl, WWW::Mechanize.


Finance-Bank-Postbank_de documentation Contained in the Finance-Bank-Postbank_de distribution.

package Finance::Bank::Postbank_de;

use strict;
use warnings;
use Carp;
use base 'Class::Accessor';
#use LWP::Debug qw(+);

use WWW::Mechanize;
use Finance::Bank::Postbank_de::Account;

use vars qw[ $VERSION ];

$VERSION = '0.26';

BEGIN {
  Finance::Bank::Postbank_de->mk_accessors(qw( agent login password urls ));
};

use constant LOGIN => 'https://banking.postbank.de/app/welcome.do';

use vars qw(%functions);
BEGIN {
  %functions = (
    quit		=> qr'^Banking beenden$',
    accountstatement	=> qr'^Kontoums.*?tze$|^Konto<.*?>u</.*?>ms.*?tze$',
  );
};

sub new {
  my ($class,%args) = @_;

  croak "Login/Account number must be specified"
    unless $args{login};
  croak "Password/PIN must be specified"
    unless $args{password};
  my $logger = $args{status} || sub {};

  my $self = {
    agent => undef,
    login => $args{login},
    password => $args{password},
    logger => $logger,
    urls => {},
  };
  bless $self, $class;

  $self->log("New $class created");
  $self;
};

sub log { $_[0]->{logger}->(@_); };
sub log_httpresult { $_[0]->log("HTTP Code",$_[0]->agent->status,$_[0]->agent->res->as_string) };

sub new_session {
  my ($self) = @_;

  # Reset our user agent
  $self->close_session()
    if ($self->agent);

  my $result = $self->get_login_page(LOGIN);
  if($result == 200) {
    if ($self->maintenance) {
      $self->log("Status","Banking is unavailable due to maintenance");
      die "Banking unavailable due to maintenance";
    };
    my $agent = $self->agent();
    $agent->form_name("loginForm");
    eval {
      $agent->current_form->value( accountNumber => $self->login );
      $agent->current_form->value( pinNumber => $self->password );
    };
    if ($@) {
      warn $agent->content;
      croak $@;
    };
    $agent->submit;
    $self->log_httpresult();
    $result = $agent->status;

    if ($self->is_security_advice) {
      $self->skip_security_advice;
    };

    $self->init_session_urls()
        if not $self->access_denied();
  };
  $result;
};

sub get_login_page {
  my ($self,$url) = @_;
  $self->log("Connecting to $url");
  $self->agent(WWW::Mechanize->new( autocheck => 1, keep_alive => 1 ));

  my $agent = $self->agent();
  $agent->add_header("If-SSL-Cert-Subject" => qr{\Q/1.3.6.1.4.1.311.60.2.1.3=DE/2.5.4.15=V1.0, Clause 5.(b)/serialNumber=HRB6793/C=DE/postalCode=53113/ST=NRW/L=Bonn/streetAddress=Friedrich Ebert Allee 114 126/O=Deutsche Postbank AG/OU=Systems AG/CN=banking.postbank.de});

  $agent->get(LOGIN);
  $self->log_httpresult();
  #warn $agent->res->header('Client-SSL-Cert-Subject');
  $agent->status;
};

sub is_security_advice {
  my ($self) = @_;
  $self->agent->content() =~ /\bZum\s+Finanzstatus\b/;
};

sub skip_security_advice {
  my ($self) = @_;
  $self->log('Skipping security advice page');
  $self->agent->follow(qr/\bZum\s+Finanzstatus\b/);
  # $self->agent->content() =~ /Sicherheitshinweis/;
};

sub error_page {
  # Check if an error page is shown (a page with much red on it)
  my ($self) = @_;
  return unless $self->agent;
  $self->agent->content =~ m!<h3 class="h3Error">Es ist ein Fehler aufgetreten</h3>!sm
      or $self->maintenance;
};

sub error_message {
  my ($self) = @_;
  return unless $self->agent;
  die "No error condition detected in:\n" . $self->agent->content
    unless $self->error_page;
  $self->agent->content =~ m!<p class="errorText">(.*?)</p>!sm
    or die "No error message found in:\n" . $self->agent->content;
  $1
};

sub maintenance {
  my ($self) = @_;
  return unless $self->agent;
  #$self->error_page and
  $self->agent->content =~ m!Sehr geehrter <span lang="en">Online-Banking</span>\s+Nutzer,\s+wegen einer hohen Auslastung kommt es derzeit im Online-Banking zu\s*l&auml;ngeren Wartezeiten.!sm
  or $self->agent->content =~ m!&nbsp;Wartung\b!;
};

sub access_denied {
  my ($self) = @_;
  if ($self->error_page) {
    my $message = $self->error_message;

    return (
         $message =~ m!^Die Kontonummer ist nicht für das Internet Online-Banking freigeschaltet. Bitte verwenden Sie zur Freischaltung den Link "Online-Banking freischalten"\.<br />\s*$!sm
      or $message =~ m!^Sie haben zu viele Zeichen in das Feld eingegeben.<br />\s*$!sm
      or $message =~ m!^Die Anmeldung ist fehlgeschlagen. Bitte vergewissern Sie sich über die Richtigkeit Ihrer Eingaben und führen Sie den Anmeldevorgang erneut durch.<br />\s*$!sm
     #   $message =~ m!^\s*.*?\(anmeldung.login.accountNumber.ktonr-n-vorh.error\)<br />\s*$!sm
     #or $message =~ m!^\s*.*?\(anmeldung.login.accountNumber.checkMaxLen.error\)<br />\s*$!sm
    )
  } else {
    return;
  };
};

sub session_timed_out {
  my ($self) = @_;
  $self->agent->content =~ /Die Sitzungsdaten sind ung&uuml;ltig, bitte f&uuml;hren Sie einen erneuten Login durch.\s+\(27000\)/;
};

sub init_session_urls {
    my ($self) = @_;
    my $agent = $self->agent;

    for (keys %functions) {
        $self->log( "init_functions: $_ : " . $agent->find_link(text_regex => $functions{ $_ })->url_abs );
        $self->urls->{$_} = $agent->find_link(text_regex => $functions{ $_ })->url_abs;
    };
};

sub select_function {
    my ($self,$function) = @_;
    if (! $self->agent) {
        $self->new_session;
    };
    carp "Unknown account function '$function'"
        unless exists $self->urls->{$function};
    $self->agent->get( $self->urls->{$function} )
        or die "Couldn't get ".$self->urls->{$function};
    $self->agent->status
};

sub close_session {
  my ($self) = @_;
  my $result;
  if (not ($self->access_denied or $self->maintenance)) {
    $self->log("Closing session");
    $self->select_function('quit');
    $result = $self->agent->res->as_string =~ m!<p class="pHeadlineLeft"><span lang="en">Online-Banking</span> beendet</p>!sm
      #or warn $self->agent->content;
  } else {
    $result = 'Never logged in';
  };
  $self->agent(undef);
  $result;
};

sub account_numbers {
  my ($self,%args) = @_;
  $self->{account_numbers} ||= do {
    my @numbers;

    $self->log("Getting related account numbers");
    $self->select_function("accountstatement");

    my $giro_input;
    my $f = $self->agent->form_name("kontoumsatzUmsatzForm");
    if ($f) {
      $giro_input = $f->find_input('konto');
    };

    if (defined $giro_input) {
      if ($giro_input->type eq 'hidden') {
        @numbers = $giro_input->value();
        $self->log("Only one related account number found: @numbers");
      } else {
        @numbers = $giro_input->possible_values();
        $self->log( scalar(@numbers) . " related account numbers found: @numbers");
      }
    } else {
      # Find the single account number
      my $c = $self->agent->content;
      @numbers = ($c =~ /\?konto=(\d+)/g);
      if (! @numbers) {
        warn "No account number found!";
        warn $_ for ($c =~ /(konto)/imsg);
        $self->log("No related account numbers found");
      };
    };

    # Discard credit card numbers:
    @numbers = grep { /^\d{9,10}$/ } @numbers;
    \@numbers
  };
  @{ $self->{account_numbers} };
};

sub get_account_statement {
  my ($self,%args) = @_;

  #warn "*** Entry: $args{account_number}";
  #for my $l ($self->agent->links) {
      #next unless $l->text =~ /konto/i;
      #warn "ACCT: " . $l->text. "\t" . $l->url;
  #};
  #Umsatzauskunft aktualisieren
  if (! $self->select_function("accountstatement")) {
      $self->log("Error selecting accountstatement");
      $self->log_httpresult();
      #die;
      return;
  };

  my $agent = $self->agent();

  my $f;
  if (! ($f = $self->agent->form_name("kontoumsatzUmsatzForm"))) {
      $self->log_httpresult();
      return;
  };
  if (exists $args{account_number}) {
    $self->log("Getting account statement for $args{account_number}");
    $agent->current_form->param( konto => [ delete $args{account_number}]);
  } else {
    my @accounts = $agent->current_form->value('konto');
    $self->log("Getting account statement via default (@accounts)");
  };

  $f->value('zeitraum','tage');
  $f->param('tage',['90']);

  $self->log("Downloading text version");
  $agent->click('action');

  my $l = $agent->find_link(text_regex => qr'Download Kontoums.*?tze');
  #my $u = $l->url_abs;
  #$u =~ s/cache=true\&//i;
  #$l->[0] = $u;
  #warn $l->url_abs;
  if ($l) {
    $agent->get($l);
    $self->log_httpresult();
  } else {
    # keine Umsaetze
    $self->log("No transactions found");
    return ();
  };

  if ($args{file}) {
    $self->log("Saving to $args{file}");
    local *F;
    open F, "> $args{file}"
      or croak "Couldn't create '$args{file}' : $!";
    print F $agent->content
      or croak "Couldn't write to '$args{file}' : $!";
    close F
      or croak "Couldn't close '$args{file}' : $!";;
  };

  if ($agent->status == 200) {
    my $result = $agent->content;
    return Finance::Bank::Postbank_de::Account->parse_statement(content => $result);
  } else {
    $self->log("Got status ".$agent->status);
    #warn $agent->status;
    return wantarray ? () : undef;
  };
};

1;
__END__