RT# 82087 - updated saisei documentation
[freeside.git] / FS / FS / part_export / ispconfig3.pm
1 package FS::part_export::ispconfig3;
2
3 use strict;
4
5 use base qw( FS::part_export );
6
7 use Data::Dumper;
8 use SOAP::Lite;
9 use IO::Socket::SSL;
10
11 =pod
12
13 =head1 NAME
14
15 FS::part_export::ispconfig3
16
17 =head1 SYNOPSIS
18
19 ISPConfig 3 integration for Freeside
20
21 =head1 DESCRIPTION
22
23 This export offers basic svc_acct provisioning for ISPConfig 3.
24 All email accounts will be assigned to a single specified client.
25
26 This module also provides generic methods for working through the L</ISPConfig3 API>.
27
28 =cut
29
30 use vars qw( %info );
31
32 my @yesno = (
33   options => ['y','n'],
34   option_labels => { 'y' => 'yes', 'n' => 'no' },
35 );
36
37 tie my %options, 'Tie::IxHash',
38   'soap_location'      => { label   => 'SOAP Location' },
39   'username'           => { label   => 'User Name',
40                             default => '' },
41   'password'           => { label   => 'Password',
42                             default => '' },
43   'debug'              => { type    => 'checkbox',
44                             label   => 'Enable debug warnings' },
45   'subheading'         => { type    => 'title',
46                             label   => 'Account defaults' },
47   'client_id'          => { label   => 'Client ID' },
48   'server_id'          => { label   => 'Server ID' },
49   'maildir'            => { label   => 'Maildir (substitutions from svc_acct, e.g. /mail/$domain/$username)', },
50   'cc'                 => { label   => 'Cc' },
51   'autoresponder_text' => { label   => 'Autoresponder text', 
52                             default => 'Out of Office Reply' },
53   'move_junk'          => { type    => 'select',
54                             options => ['y','n'],
55                             option_labels => { 'y' => 'yes', 'n' => 'no' },
56                             label   => 'Move junk' },
57   'postfix'            => { type    => 'select',
58                             @yesno,
59                             label   => 'Postfix' },
60   'access'             => { type    => 'select',
61                             @yesno,
62                             label   => 'Access' },
63   'disableimap'        => { type    => 'select',
64                             @yesno,
65                             label   => 'Disable IMAP' },
66   'disablepop3'        => { type    => 'select',
67                             @yesno,
68                             label   => 'Disable POP3' },
69   'disabledeliver'     => { type    => 'select',
70                             @yesno,
71                             label   => 'Disable deliver' },
72   'disablesmtp'        => { type    => 'select',
73                             @yesno,
74                             label   => 'Disable SMTP' },
75 ;
76
77 %info = (
78   'svc'             => 'svc_acct',
79   'desc'            => 'Export email account to ISPConfig 3',
80   'options'         => \%options,
81   'no_machine'      => 1,
82   'notes'           => <<'END',
83 All email accounts will be assigned to a single specified client and server.
84 END
85 );
86
87 sub _mail_user_params {
88   my ($self, $svc_acct) = @_;
89   # all available api fields are in comments below, even if we don't use them
90   return {
91     #server_id  (int(11))
92     'server_id' => $self->option('server_id'),
93     #email  (varchar(255))
94     'email' => $svc_acct->username.'@'.$svc_acct->domain,
95     #login  (varchar(255))
96     'login' => $svc_acct->username.'@'.$svc_acct->domain,
97     #password  (varchar(255))
98     'password' => $svc_acct->_password,
99     #name  (varchar(255))
100     'name' => $svc_acct->finger,
101     #uid  (int(11))
102     'uid' => $svc_acct->uid,
103     #gid  (int(11))
104     'gid' => $svc_acct->gid,
105     #maildir  (varchar(255))
106     'maildir' => $self->_substitute($self->option('maildir'),$svc_acct),
107     #quota  (bigint(20))
108     'quota' => $svc_acct->quota,
109     #cc  (varchar(255))
110     'cc' => $self->option('cc'),
111     #homedir  (varchar(255))
112     'homedir' => $svc_acct->dir,
113
114     ## initializing with autoresponder off, but this could become an export option...
115     #autoresponder  (enum('n','y'))
116     'autoresponder' => 'n',
117     #autoresponder_start_date  (datetime)
118     #autoresponder_end_date  (datetime)
119     #autoresponder_text  (mediumtext)
120     'autoresponder_text' => $self->option('autoresponder_text'),
121
122     #move_junk  (enum('n','y'))
123     'move_junk' => $self->option('move_junk'),
124     #postfix  (enum('n','y'))
125     'postfix' => $self->option('postfix'),
126     #access  (enum('n','y'))
127     'access' => $self->option('access'),
128
129     ## not needed right now, not sure what it is
130         #custom_mailfilter  (mediumtext)
131
132     #disableimap  (enum('n','y'))
133     'disableimap' => $self->option('disableimap'),
134     #disablepop3  (enum('n','y'))
135     'disablepop3' => $self->option('disablepop3'),
136     #disabledeliver  (enum('n','y'))
137     'disabledeliver' => $self->option('disabledeliver'),
138     #disablesmtp  (enum('n','y'))
139     'disablesmtp' => $self->option('disablesmtp'),
140   };
141 }
142
143 sub _export_insert {
144   my ($self, $svc_acct) = @_;
145   return $self->api_error || 'Error logging in'
146     unless $self->api_login;
147   my $params = $self->_mail_user_params($svc_acct);
148   my $remoteid = $self->api_call('mail_user_add',$self->option('client_id'),$params);
149   return $self->api_error_logout if $self->api_error;
150   my $error = $self->set_remoteid($svc_acct,$remoteid);
151   $error = "Remote system updated, but error setting remoteid ($remoteid): $error"
152     if $error;
153   $self->api_logout;
154   return $error;
155 }
156
157 sub _export_replace {
158   my ($self, $svc_acct, $svc_acct_old) = @_;
159   return $self->api_error || 'Error logging in'
160     unless $self->api_login;
161   my $remoteid = $self->get_remoteid($svc_acct_old);
162   return "Could not load remoteid for old service" unless $remoteid;
163   my $params = $self->_mail_user_params($svc_acct);
164   #API docs claim "Returns the number of affected rows"
165   my $success = $self->api_call('mail_user_update',$self->option('client_id'),$remoteid,$params);
166   return $self->api_error_logout if $self->api_error;
167   return "Server returned no rows updated, but no other error message" unless $success;
168   my $error = '';
169   unless ($svc_acct->svcnum eq $svc_acct_old->svcnum) { # are these ever not equal?
170     $error = $self->set_remoteid($svc_acct,$remoteid);
171     $error = "Remote system updated, but error setting remoteid ($remoteid): $error"
172       if $error;
173   }
174   $self->api_logout;
175   return $error;
176 }
177
178 sub _export_delete {
179   my ($self, $svc_acct) = @_;
180   return $self->api_error || 'Error logging in'
181     unless $self->api_login;
182   my $remoteid = $self->get_remoteid($svc_acct);
183   #don't abort deletion--
184   #  might have been provisioned before export was implemented,
185   #  still need to be able to delete from freeside
186   unless ($remoteid) {
187     warn "Could not load remoteid for svcnum ".$svc_acct->svcnum.", unprovisioning anyway";
188     return '';
189   }
190   #API docs claim "Returns the number of deleted records"
191   my $success = $self->api_call('mail_user_delete',$remoteid);
192   return $self->api_error_logout if $self->api_error;
193   #don't abort deletion--
194   #  if it's already been deleted remotely,
195   #  still need to be able to delete from freeside
196   warn "Server returned no records deleted for svcnum ".$svc_acct->svcnum.
197        " remoteid $remoteid, unprovisioning anyway"
198     unless $success;
199   $self->api_logout;
200   return '';
201 }
202
203 sub _export_suspend {
204   my ($self, $svc_acct) = @_;
205   return '';
206 }
207
208 sub _export_unsuspend {
209   my ($self, $svc_acct) = @_;
210   return '';
211 }
212
213 =head1 ISPConfig3 API
214
215 These methods allow access to the ISPConfig3 API using the credentials
216 set in the export options.
217
218 =cut
219
220 =head2 api_call
221
222 Accepts I<$method> and I<@params>.  Places an api call to the specified
223 method with the specified params.  Returns the result of that call
224 (empty on failure.)  Retrieve error messages using L</api_error>.
225
226 Do not include session id in list of params;  it will be included
227 automatically.  Must run L</api_login> first.
228
229 =cut
230
231 sub api_call {
232   my ($self,$method,@params) = @_;
233   # This does get used by api_login,
234   # to retrieve the session id after it sets the client,
235   # so we only check for existence of client,
236   # and we only include session id if we have one
237   my $client = $self->{'__ispconfig_client'};
238   unless ($client) {
239     $self->{'__ispconfig_error'} = 'Not logged in';
240     return;
241   }
242   if ($self->{'__ispconfig_session'}) {
243     unshift(@params,$self->{'__ispconfig_session'});
244   }
245   # Contact server in eval, to trap connection errors
246   warn "Calling SOAP method $method with params:\n".Dumper(\@params)."\n"
247     if $self->option('debug');
248   my $response = eval { $client->$method(@params) };
249   unless ($response) {
250     $self->{'__ispconfig_error'} = "Error contacting server: $@";
251     return;
252   }
253   # Set results and return
254   $self->{'__ispconfig_error'} = $response->fault
255                                ? "Error from server: " . $response->faultstring
256                                : '';
257   return $response->result;
258 }
259
260 =head2 api_error
261
262 Returns the error string set by L</ISPConfig3 API> methods,
263 or a blank string if most recent call produced no errors.
264
265 =cut
266
267 sub api_error {
268   my $self = shift;
269   return $self->{'__ispconfig_error'} || '';
270 }
271
272 =head2 api_error_logout
273
274 Attempts L</api_logout>, but returns L</api_error> message from
275 before logout was attempted.  Useful for logging out
276 properly after an error.
277
278 =cut
279
280 sub api_error_logout {
281   my $self = shift;
282   my $error = $self->api_error;
283   $self->api_logout;
284   return $error;
285 }
286
287 =head2 api_login
288
289 Initializes an api session using the credentials for this export.
290 Returns true on success, false on failure.
291 Retrieve error messages using L</api_error>.
292
293 =cut
294
295 sub api_login {
296   my $self = shift;
297   if ($self->{'__ispconfig_session'} || $self->{'__ispconfig_client'}) {
298     $self->{'__ispconfig_error'} = 'Already logged in';
299     return;
300   }
301   $self->{'__ispconfig_session'} = undef;
302   $self->{'__ispconfig_client'} =
303     SOAP::Lite->proxy( $self->option('soap_location'),
304                          ssl_opts => [
305                            verify_hostname => 0,
306                            SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE,
307                          ]
308                      )
309     || undef;
310   unless ($self->{'__ispconfig_client'}) {
311     $self->{'__ispconfig_error'} = 'Error creating SOAP client';
312     return;
313   }
314   $self->{'__ispconfig_session'} = 
315     $self->api_call('login',$self->option('username'),$self->option('password'))
316     || undef;
317   return unless $self->{'__ispconfig_session'};
318   return 1;
319 }
320
321 =head2 api_logout
322
323 Ends the current api session established by L</api_login>.
324 Returns true on success, false on failure.
325
326 =cut
327
328 sub api_logout {
329   my $self = shift;
330   unless ($self->{'__ispconfig_session'}) {
331     $self->{'__ispconfig_error'} = 'Not logged in';
332     return;
333   }
334   my $result = $self->api_call('logout');
335   # clear these even if there was a failure to logout
336   $self->{'__ispconfig_client'} = undef;
337   $self->{'__ispconfig_session'} = undef;
338   return if $self->api_error;
339   return 1;
340 }
341
342 # false laziness with portaone export
343 sub _substitute {
344   my ($self, $string, @objects) = @_;
345   return '' unless $string;
346   foreach my $object (@objects) {
347     next unless $object;
348     my @fields = $object->fields;
349     push(@fields,'domain') if $object->table eq 'svc_acct';
350     foreach my $field (@fields) {
351       next unless $field;
352       my $value = $object->$field;
353       $string =~ s/\$$field/$value/g;
354     }
355   }
356   # strip leading/trailing whitespace
357   $string =~ s/^\s//g;
358   $string =~ s/\s$//g;
359   return $string;
360 }
361
362 =head1 SEE ALSO
363
364 L<FS::part_export>
365
366 =head1 AUTHOR
367
368 Jonathan Prykop 
369 jonathan@freeside.biz
370
371 =cut
372
373 1;
374
375