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