1 package FS::part_export::acct_google;
4 use vars qw(%info %SIG $CACHE);
7 use base 'FS::part_export';
9 tie my %options, 'Tie::IxHash',
10 'domain' => { label => 'Domain name' },
11 'username' => { label => 'Admin username' },
12 'password' => { label => 'Admin password' },
14 # To handle multiple domains, use separate instances of
15 # the export. We assume that they all have different
20 'desc' => 'Google hosted mail',
21 'options' => \%options,
24 'default_svc_class' => 'Email',
26 Export accounts to the Google Provisioning API. Requires
27 REST::Google::Apps::Provisioning from CPAN.
31 sub rebless { shift; }
34 my($self, $svc_acct) = (shift, shift);
35 $svc_acct->finger =~ /^(.*)\s+(\S+)$/;
36 my ($first, $last) = ($1, $2);
37 $self->google_request('createUser',
38 'username' => $svc_acct->username,
39 'password' => $svc_acct->_password,
40 'givenName' => $first,
41 'familyName' => $last,
46 my( $self, $new, $old ) = (shift, shift, shift);
47 # We have to do this in two steps, so do the renameUser last so that
48 # if it fails partway through the username is still coherent.
49 if ( $new->_password ne $old->_password
50 or $new->finger ne $old->finger ) {
51 $new->finger =~ /^(.*)\s+(\S+)$/;
52 my ($first, $last) = ($1, $2);
53 my $error = $self->google_request('updateUser',
54 'username' => $old->username,
55 'password' => $new->_password,
56 'givenName' => $first,
57 'familyName' => $last,
59 return $error if $error;
61 if ( $new->username ne $old->username ) {
62 my $error = $self->google_request('renameUser',
63 'username' => $old->username,
64 'newname' => $new->username
66 return $error if $error;
72 my( $self, $svc_acct ) = (shift, shift);
73 $self->google_request('deleteUser',
74 'username' => $svc_acct->username
79 my( $self, $svc_acct ) = (shift, shift);
80 $self->google_request('updateUser',
81 'username' => $svc_acct->username,
82 'suspended' => 'true',
86 sub _export_unsuspend {
87 my( $self, $svc_acct ) = (shift, shift);
88 $self->google_request('updateUser',
89 'username' => $svc_acct->username,
90 'suspended' => 'false',
96 my $google = $self->google_handle;
97 if ( $google->{'error'} ) {
98 my $url = $google->{'captcha_url'} || '';
99 $url = "http://www.google.com/accounts/$url" if $url;
100 return { 'captcha_url' => $url,
102 'Unable to connect to the Google API: '.$google->{'error'}.'.',
105 return; #nothing on success
110 my $response = shift;
111 my $google = $self->google_handle('captcha_response' => $response);
112 return (defined($google->{'token'}));
116 1000 => 'unknown error',
117 1001 => 'server busy',
118 1100 => 'username belongs to a recently deleted account',
119 1101 => 'user suspended',
120 1200 => 'domain user limit exceeded',
121 1201 => 'domain alias limit exceeded',
122 1202 => 'domain suspended',
123 1203 => 'feature not available on this domain',
124 1300 => 'username in use',
125 1301 => 'user not found',
126 1302 => 'reserved username',
127 1400 => 'illegal character in first name',
128 1401 => 'illegal character in last name',
129 1402 => 'invalid password',
130 1403 => 'illegal character in username',
131 # should be everything we need
134 # Runs the request and returns nothing if it succeeds, or an
138 my ($self, $method, %opt) = @_;
139 my $google = $self->google_handle(
140 'captcha_response' => delete $opt{'captcha_response'}
142 return $google->{'error'} if $google->{'error'};
144 # Throw away the result from this; we don't use it yet.
145 eval { $google->$method(%opt) };
147 return $google_error{ $@->{'error'}->{'errorCode'} } || $@->{'error'};
152 # Returns a REST::Google::Apps::Provisioning object which is hooked
153 # to die {error => stuff} on API errors. The cached auth token
154 # will be used if possible. If not, try to authenticate. On
155 # authentication error, the R:G:A:P object will still be returned
156 # but with $google->{'error'} set to the error message.
162 'REST::Google::Apps::Provisioning',
164 'LWP::UserAgent 5.815',
168 die "failed to load $_\n" if $@;
170 $CACHE ||= new Cache::FileCache( {
171 'namespace' => __PACKAGE__,
172 'cache_root' => "$FS::UID::cache_dir/cache.$FS::UID::datasrc",
174 my $google = REST::Google::Apps::Provisioning->new(
175 'domain' => $self->option('domain')
178 # REST::Google::Apps::Provisioning lacks error reporting. We deal
179 # with that by hooking HTTP::Response to throw a useful fatal error
181 $google->{'lwp'}->add_handler( 'response_done' =>
183 my $response = shift;
184 return if $response->is_success;
187 if ( $response->content =~ /^</ ) {
189 $error = $google->{'xml'}->parse_string($response->content);
191 elsif ( $response->content =~ /=/ ) {
192 $error = +{ map { if ( /^(\w+)=(.*)$/ ) { lc($1) => $2 } }
193 split("\n", $response->content)
196 else { # have something to say if there is no response...
197 $error = {'error' => $response->status_line};
203 my $cache_token = $self->exportnum . '_token';
204 my $cache_captcha = $self->exportnum . '_captcha_token';
205 $google->{'token'} = $CACHE->get($cache_token);
206 if ( !$google->{'token'} ) {
208 'username' => $self->option('username'),
209 'password' => $self->option('password'),
211 if ( $opt{'captcha_response'} ) {
212 $login{'logincaptcha'} = $opt{'captcha_response'};
213 $login{'logintoken'} = $CACHE->get($cache_captcha);
215 eval { $google->captcha_auth(%login); };
217 $google->{'error'} = $@->{'error'};
218 $google->{'captcha_url'} = $@->{'captchaurl'};
219 $CACHE->set($cache_captcha, $@->{'captchatoken'}, '1 minute');
222 $CACHE->remove($cache_captcha);
223 $CACHE->set($cache_token, $google->{'token'}, '1 hour');
228 # REST::Google::Apps::Provisioning also lacks a way to do this
229 sub REST::Google::Apps::Provisioning::captcha_auth {
232 return( 1 ) if $self->{'token'};
237 map { $arg->{lc($_)} = $arg->{$_} } keys %{$arg};
239 foreach my $param ( qw/ username password / ) {
240 $arg->{$param} || croak( "Missing required '$param' argument" );
244 'accountType' => 'HOSTED',
246 'Email' => $arg->{'username'} . '@' . $self->{'domain'},
247 'Passwd' => $arg->{'password'},
249 if ( $arg->{'logincaptcha'} ) {
251 'logintoken' => $arg->{'logintoken'},
252 'logincaptcha'=> $arg->{'logincaptcha'}
255 my $response = $self->{'lwp'}->post(
256 'https://www.google.com/accounts/ClientLogin',
260 $response->is_success() || return( 0 );
262 foreach ( split( /\n/, $response->content() ) ) {
263 $self->{'token'} = $1 if /^Auth=(.+)$/;
264 last if $self->{'token'};
267 return( 1 ) if $self->{'token'} || return( 0 );