package Mail::Postini; use 5.008001; use strict; use warnings; our $VERSION = '0.17'; our $CVSID = '$Id: Postini.pm,v 1.12 2008/02/25 18:26:49 scott Exp $'; our $Debug = 0; our $Trace = 0; use LWP::UserAgent (); use URI::Escape 'uri_escape'; use Digest::SHA1 'sha1_base64'; use HTML::Form (); use Carp 'carp'; use Data::Dumper 'Dumper'; ## NOTE: This is an inside-out object; remove members in ## NOTE: the DESTROY() sub if you add additional members. my %postini = (); my %app_serv = (); my %username = (); my %password = (); my %secret = (); my %orgid = (); my %orgname = (); my %ua = (); my %agent = (); my %errors = (); my %err_pages = (); sub new { my $class = shift; my %args = @_; my $self = bless \(my $ref), $class; $args{postini} ||= ''; $args{username} ||= ''; $args{password} ||= ''; $args{secret} ||= ''; $args{orgid} ||= ''; $args{orgname} ||= ''; $args{agent} ||= 'perl/Mail-Postini $VERSION'; $postini {$self} = $args{postini}; $app_serv {$self} = ''; $username {$self} = $args{username}; $password {$self} = $args{password}; $secret {$self} = $args{secret}; $orgid {$self} = $args{orgid}; $orgname {$self} = $args{orgname}; $agent {$self} = $args{agent}; $errors {$self} = []; $err_pages {$self} = []; return $self; } ## this gets us a cookie and gives us some state when we need it sub connect { my $self = shift; my %args = @_; exists $args{postini} and $postini {$self} = $args{postini}; exists $args{username} and $username {$self} = $args{username}; exists $args{password} and $password {$self} = $args{password}; exists $args{secret} and $secret {$self} = $args{secret}; exists $args{orgid} and $orgid {$self} = $args{orgid}; exists $args{orgname} and $orgname {$self} = $args{orgname}; exists $args{agent} and $agent {$self} = $args{agent}; $ua{$self} = LWP::UserAgent->new; $ua{$self}->agent($agent{$self}); $ua{$self}->cookie_jar({}); my $uname = uri_escape($username{$self}); my $upass = uri_escape($password{$self}); ## authenticate my $req = HTTP::Request->new( POST => $postini{$self} ); $req->content_type( 'application/x-www-form-urlencoded' ); $req->content( "email=$uname&pword=$upass&action=login" ); my $res = $ua{$self}->request($req); unless( $res->code == 302 ) { $self->errors("Failure: " . $res->code . ': ' . $res->message); $self->err_pages($res); return; } ## get landing page $req = HTTP::Request->new( GET => $res->header('location') ); $res = $ua{$self}->request($req); unless( $res->code == 200 ) { $self->errors("Failure: " . $res->code . ': ' . $res->message); $self->err_pages($res); return; } ## get system admin page my ($sa_page) = $res->content =~ m{System Administration}; $req = HTTP::Request->new( GET => $sa_page ); $res = $ua{$self}->request($req); ($app_serv{$self}) = $sa_page =~ m!^(https?://[^/]+)/!; $orgid{$self} ||= $self->get_orgid( name => $orgname{$self} ); return 1; } sub create_organization { my $self = shift; my %args = @_; my $parentorgid; unless( $parentorgid = $args{parentorgid} ) { $parentorgid = $self->get_orgid( name => $args{parentorg} ) if $args{parentorg}; $parentorgid ||= $orgid{$self}; ## top level org } print STDERR "Parent orgid: $parentorgid\n" if $Debug; my $org = $args{org}; my $req = HTTP::Request->new( GET => qq!$app_serv{$self}/exec/admin_orgs?targetorgid=$parentorgid! ); my $res = $ua{$self}->request($req); print STDERR "call _get_form in create_organization()\n" if $Trace; my $form = $self->_get_form($res, qr(/exec/admin_orgs\?targetorgid=${parentorgid}$), { type => 'submit', value => 'Add', name => '', } ) or do { carp "Form error: " . join(', ', $self->errors()); return; }; print STDERR "setting form variables\n" if $Trace; $form->value( "setconf-neworg" => $org ); $form->value( "setconf-parent" => $parentorgid ); $form->value( "action" => "addOrg" ); $res = $ua{$self}->request( $form->click() ); unless( $res->is_redirect ) { $self->errors("Failure: " . $res->code . ": " . $res->message); $self->err_pages($res); return; } print STDERR "Form submission ok\n" if $Trace; my($new_org) = $res->content =~ /\btargetorgid=(\d+)"/ or return; ## https://ac-s7.postini.com/exec/admin_orgs?targetorgid=100059067&action=display_GeneralSettings $req = HTTP::Request->new( GET => qq!$app_serv{$self}/exec/admin_orgs?targetorgid=${new_org}&action=display_GeneralSettings! ); $res = $ua{$self}->request($req); ## this trick might be useful sometime in the future. It certainly ## makes the redirect more change-tolerant # $res = $self->_do_redirect($app_serv{$self} . $res->header('location')); print STDERR "call _get_form to set org details\n" if $Trace; my $setform = $self->_get_form($res, qr(\badmin_orgs\b), { type => 'text', name => 'setconf-name', }) or do { carp "Form error: " . join(', ', $self->errors()); return; }; $setform->value('setconf-name' => $args{name}) if $args{name}; $setform->value('setconf-is_email_config' => $args{email_config}) if $args{email_config}; $setform->value('setconf-support_contact' => $args{support_contact}) if $args{support_contact}; $setform->value('setconf-api_secret' => $args{api_secret}) if $args{api_secret}; $res = $ua{$self}->request( $setform->click() ); unless( $res->is_redirect ) { $self->errors("Failure: " . $res->code . ": " . $res->message); $self->err_pages($res); return; } print STDERR "Successful organization creation ($new_org)\n" if $Trace; return ( $new_org ? $new_org : undef ); } sub list_organizations { my $self = shift; my %args = @_; my $orgid = $args{orgid} || $self->get_orgid( name => $orgname{$self} ); my $req = HTTP::Request->new( GET => qq!$app_serv{$self}/exec/admin_listorgs_download?sortkeys=orgtag%3Ah&type_of_user=all&childorgs=1&type_of_encrypted_user=ext_encrypt_any&aliases=0&targetorgid=${orgid}&type=orgsets! ); my $res = $ua{$self}->request($req); my @keys = (); my %orgs = (); LINES: for my $line ( split(/\n/, $res->content) ) { ## find the keys if( $line =~ /^\#/ ) { next unless $line =~ /\borgname\b/; next if @keys; $line =~ s/^#\s*//; @keys = split(/,/, $line); next; } next LINES unless @keys; ## FIXME: here skip all orgs except the one we're looking for... ## next unless $line =~ /^$args{orgname}/; ## or something like that. my @fields = (); my $state = 0; CHUNK: for my $chunk ( split(/,/, $line) ) { if( $state == 1 ) { if( $chunk =~ s/"$// ) { $state = 0; } $fields[$#fields] .= ',' . $chunk; next CHUNK; } if( $chunk =~ s/^"// ) { $state = 1; } push @fields, $chunk; } my %acct = (); @acct{@keys} = @fields; $orgs{$acct{$keys[0]}} = \%acct; } return \%orgs; } sub set_org_data { my $self = shift; my %args = @_; unless( $args{orgid} ) { unless( $args{org} ) { $self->errors("Failure: orgid or og parameter required for set_org_data()"); return; } $args{orgid} = $self->get_orgid( name => $args{orgid}) or do { $self->errors("Failure: Could not get orgid for '$args{org}' (misspelled org name, or Postini down?)"); return; }; } my $orgid = $args{orgid}; if( $args{section} eq 'GeneralSettings' ) { my $req = HTTP::Request->new( GET => qq!$app_serv{$self}/exec/admin_orgs?targetorgid=${orgid}&action=display_GeneralSettings! ); my $res = $ua{$self}->request($req); my $form = $self->_get_form($res, qr(admin_orgs\?targetorgid=${orgid}), { type => 'text', name => 'setconf-name' } ) or do { carp "Form error: " . join(", ", $self->errors); return; }; $form->value( action => "modifyGeneralSettings" ); $form->value( "setconf-name" => $args{name} ) if $args{name}; $form->value( "setconf-orgtag" => $args{orgtag} ) if $args{orgtag}; $form->value( "setconf-parent" => $args{parent} ) if $args{parent}; $form->value( "setconf-support_contact" => $args{support_contact} ) if $args{support_contact}; $form->value( "setconf-api_secret" => $args{api_secret} ) if $args{api_secret}; $form->value( "setconf-tight_postini" => $args{tight_postini} ) if $args{tight_postini}; $form->value( "setconf-default_user" => $args{default_user} ) if $args{default_user}; $form->value( "setconf-smartcreate" => $args{smartcreate} ) if $args{smartcreate}; $form->value( "setconf-quar_links" => $args{quar_links} ) if $args{quar_links}; $form->value( "setconf-timezone" => $args{timezone} ) if $args{timezone}; $form->value( "lang" => $args{lang} ) if $args{lang}; $form->value( "encoding" => $args{encoding} ) if $args{encoding}; $form->value( "cascade" => $args{cascade} ) if $args{cascade}; $res = $ua{$self}->request( $form->click('save') ); unless( $res->is_redirect ) { $self->errors("Failure: " . $res->code . ": " . $res->message); $self->err_pages($res); return; } } return 1; } sub set_org_mail_server { my $self = shift; my %args = @_; unless( $args{orgid} ) { unless( $args{org} ) { $self->errors("Failure: orgid or org parameter required for set_org_mail_server()"); return; } $args{orgid} = $self->get_orgid( name => $args{org} ) or do { $self->errors("Failure: Could not get orgid for '$args{org}' (misspelled org name, or Postini down?)"); return; }; } my $orgid = $args{orgid}; ## param check unless( $args{server1} ) { $self->errors("Failure: server1 parameter required for set_org_mail_server()"); return; } $args{weight1} ||= 100; $args{maxcon1} ||= ''; my $req = HTTP::Request->new( GET => qq!$app_serv{$self}/exec/delivmgr?targetorgid=${orgid}&action=display_Edit!); my $res = $ua{$self}->request($req); my $form = $self->_get_form($res, qr(\b/exec/delivmgr\b)) or do { carp "Form error: " . join(", ", $self->errors()); return; }; $form->value("action" => 'modifyDeliv'); $form->value("targetorgid" => $orgid); $form->value("mailhost-0|0" => $args{server1}); $form->value("weight-0|0" => $args{weight1}); $form->value("maxcon-0|0" => $args{maxcon1}); if( $args{server2} ) { $args{weight2} ||= 50; $args{maxcon2} ||= ''; $form->value("mailhost-0|1" => $args{server2}); $form->value("weight-0|1" => $args{weight2}); $form->value("maxcon-0|1" => $args{maxcon2}); } if( $args{failover1} ) { $args{fo_weight1} ||= 50; $args{fo_maxcon1} ||= ''; $form->value("mailhost-1|0" => $args{failover1}); $form->value("weight-1|0" => $args{fo_weight1}); $form->value("maxcon-1|0" => $args{fo_maxcon1}); } if( $args{failover2} ) { $args{fo_weight2} ||= 25; $args{fo_maxcon2} ||= ''; $form->value("mailhost-1|1" => $args{failover2}); $form->value("weight-1|1" => $args{fo_weight2}); $form->value("maxcon-1|1" => $args{fo_maxcon2}); } if( $args{failover3} ) { $args{fo_weight3} ||= 25; $args{fo_maxcon3} ||= ''; $form->value("mailhost-1|2" => $args{failover3}); $form->value("weight-1|2" => $args{fo_weight3}); $form->value("maxcon-1|2" => $args{fo_maxcon3}); } $form->value("overflow" => 1) if $args{overflow}; $res = $ua{$self}->request( $form->click() ); unless( $res->is_redirect ) { $self->errors("Failure: " . $res->code . ": " . $res->message); $self->err_pages($res); return; } return 1; } sub get_org_mail_server { my $self = shift; my %args = @_; unless( $args{orgid} ) { unless( $args{org} ) { $self->errors("Failure: orgid or org parameter required for get_org_mail_server()"); return; } $args{orgid} = $self->get_orgid( name => $args{org} ) or do { $self->errors("Failure: Could not get orgid for '$args{org}' (misspelled org name or Postini down?)"); return; }; } my $orgid = $args{orgid}; my $req = HTTP::Request->new( GET => qq!$app_serv{$self}/exec/delivmgr?targetorgid=$orgid&action=display_Edit! ); my $res = $ua{$self}->request($req); my $form = $self->_get_form($res, qr(\bdelivmgr\b), { type => 'hidden', name => 'action', value => 'modifyDeliv' }) or do { warn "Could not find form.\n"; $self->err_pages($res); return; }; my %data = (); $data{server1} = $form->value('mailhost-0|0'); $data{weight1} = $form->value('weight-0|0'); $data{maxcon1} = $form->value('maxcon-0|0'); $data{server2} = $form->value('mailhost-0|1'); $data{weight2} = $form->value('weight-0|1'); $data{maxcon2} = $form->value('maxcon-0|1'); $data{failover1} = $form->value('mailhost-1;0'); $data{fo_weight1} = $form->value('weight-1;0'); $data{fo_maxcon1} = $form->value('maxcon-1;0'); $data{failover2} = $form->value('mailhost-1;1'); $data{fo_weight2} = $form->value('weight-1;1'); $data{fo_maxcon2} = $form->value('maxcon-1;1'); $data{failover3} = $form->value('mailhost-1;2'); $data{fo_weight3} = $form->value('weight-1;2'); $data{fo_maxcon3} = $form->value('maxcon-1;2'); return %data; } sub delete_organization { my $self = shift; my %args = @_; my $req; my $res; unless( $args{orgid} ) { unless( $args{org} ) { $self->errors("orgid or org parameter required for delete_organization()"); return; } $args{orgid} = $self->get_orgid( name => $args{org} ) or do { $self->errors("Could not fetch orgid from '$args{org}'"); return; }; } $req = HTTP::Request->new( POST => qq!$app_serv{$self}/exec/admin_orgs?targetorgid=$args{orgid}! ); $req->content_type( 'application/x-www-form-urlencoded' ); $req->content( "confirm=Confirm&action=deleteOrg" ); $res = $ua{$self}->request($req); unless( $res->is_redirect ) { $self->errors("Failure: " . $res->code . ": " . $res->message); $self->err_pages($res); return; } return 1; } sub get_user_data { my $self = shift; my $user = shift; my($domain) = $user =~ /\@(.+)$/ if $user; my %args = (); $args{user} = $user if $user; $args{domain} = $domain if $domain; my $users = $self->list_users( %args ); return ($users ? %$users : ()); } sub get_orgid { my $self = shift; my %args = @_; if( $args{name} eq $orgname{$self} ) { return $orgid{$self} if $orgid{$self}; ## for bootstrapping } my $req = HTTP::Request->new( GET => qq!$app_serv{$self}/exec/admin_list?type=orgs&orgtagqs=$args{name}! ); my $res = $ua{$self}->request($req); my ($chunk) = $res->content =~ m#\n(.+)\n#s; return unless $chunk; my ($orgid) = $chunk =~ m!$args{name}!; return( $orgid ? $orgid : undef ); } sub add_domain { my $self = shift; my %args = @_; unless( $args{orgid} ) { unless( $args{org} ) { $self->errors("orgid or org parameter required for delete_organization()"); return; } $args{orgid} = $self->get_orgid( name => $args{org} ) or do { $self->errors("Could not fetch orgid from '$args{org}'"); return; }; } my $orgname = $args{org}; my $domain = $args{domain}; my $req = HTTP::Request->new( GET => qq!$app_serv{$self}/exec/admin_domains?action=display_Add&targetorgid=$args{orgid}! ); my $res = $ua{$self}->request($req); my $form = $self->_get_form($res, qr(/exec/admin_domains)) or do { warn "Could not find form.\n"; return; }; $form->value( "setconf-targetorg" => $orgname ); $form->value( "setconf-domainname" => $domain ); $form->value( "action" => "addDomain" ); $res = $ua{$self}->request( $form->click('save') ); unless( $res->code == 302 ) { $self->errors("Failure: " . $res->code . ": " . $res->message); $self->err_pages($res); return; } return 1; } sub delete_domain { my $self = shift; my %args = @_; my $req = HTTP::Request->new( POST => qq!$app_serv{$self}/exec/admin_domains! ); $req->content_type( 'application/x-www-form-urlencoded' ); $req->content( "setconf-domainname=$args{domain}&action=deleteDomain&delete=Delete" ); my $res = $ua{$self}->request($req); unless( $res->code == 302 ) { $self->errors("Failure: " . $res->code . ": " . $res->message); $self->err_pages($res); return; } return 1; } sub org_from_domain { my $self = shift; my $domain = shift; my $qs = qq!$app_serv{$self}/exec/admin_list?type=domains&childorgs=0&domainqs=${domain}&childorgs=1&Search=Search!; my $req = HTTP::Request->new( GET => $qs ); my $res = $ua{$self}->request($req); my ($chunk) = $res->content =~ m#\n(.+)\n#s; return unless $chunk; my ($orgid) = $chunk =~ m!$domain.+?\n