3345b3f06257e17a25bf416d72d3b6264c4bf588
[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   return $self->api_error || 'Error logging in'
145     unless $self->api_login;
146   my $params = $self->_mail_user_params($svc_acct);
147   my $remoteid = $self->api_call('mail_user_add',$self->option('client_id'),$params);
148   return $self->api_error_logout if $self->api_error;
149   my $error = $self->set_remoteid($svc_acct,$remoteid);
150   $error = "Remote system updated, but error setting remoteid ($remoteid): $error"
151     if $error;
152   $self->api_logout;
153   return $error;
154 }
155
156 sub _export_replace {
157   my ($self, $svc_acct, $svc_acct_old) = @_;
158   return $self->api_error || 'Error logging in'
159     unless $self->api_login;
160   my $remoteid = $self->get_remoteid($svc_acct_old);
161   return "Could not load remoteid for old service" unless $remoteid;
162   my $params = $self->_mail_user_params($svc_acct);
163   #API docs claim "Returns the number of affected rows"
164   my $success = $self->api_call('mail_user_update',$self->option('client_id'),$remoteid,$params);
165   return $self->api_error_logout if $self->api_error;
166   return "Server returned no rows updated, but no other error message" unless $success;
167   my $error = '';
168   unless ($svc_acct->svcnum eq $svc_acct_old->svcnum) { # are these ever not equal?
169     $error = $self->set_remoteid($svc_acct,$remoteid);
170     $error = "Remote system updated, but error setting remoteid ($remoteid): $error"
171       if $error;
172   }
173   $self->api_logout;
174   return $error;
175 }
176
177 sub _export_delete {
178   my ($self, $svc_acct) = @_;
179   return $self->api_error || 'Error logging in'
180     unless $self->api_login;
181   my $remoteid = $self->get_remoteid($svc_acct);
182   #don't abort deletion--
183   #  might have been provisioned before export was implemented,
184   #  still need to be able to delete from freeside
185   unless ($remoteid) {
186     warn "Could not load remoteid for svcnum ".$svc_acct->svcnum.", unprovisioning anyway";
187     return '';
188   }
189   #API docs claim "Returns the number of deleted records"
190   my $success = $self->api_call('mail_user_delete',$remoteid);
191   return $self->api_error_logout if $self->api_error;
192   #don't abort deletion--
193   #  if it's already been deleted remotely,
194   #  still need to be able to delete from freeside
195   warn "Server returned no records deleted for svcnum ".$svc_acct->svcnum.
196        " remoteid $remoteid, unprovisioning anyway"
197     unless $success;
198   $self->api_logout;
199   return '';
200 }
201
202 sub _export_suspend {
203   my ($self, $svc_acct) = @_;
204   return '';
205 }
206
207 sub _export_unsuspend {
208   my ($self, $svc_acct) = @_;
209   return '';
210 }
211
212 =head1 ISPConfig3 API
213
214 These methods allow access to the ISPConfig3 API using the credentials
215 set in the export options.
216
217 =cut
218
219 =head2 api_call
220
221 Accepts I<$method> and I<@params>.  Places an api call to the specified
222 method with the specified params.  Returns the result of that call
223 (empty on failure.)  Retrieve error messages using L</api_error>.
224
225 Do not include session id in list of params;  it will be included
226 automatically.  Must run L</api_login> first.
227
228 =cut
229
230 sub api_call {
231   my ($self,$method,@params) = @_;
232   # This does get used by api_login,
233   # to retrieve the session id after it sets the client,
234   # so we only check for existence of client,
235   # and we only include session id if we have one
236   my $client = $self->{'__ispconfig_client'};
237   unless ($client) {
238     $self->{'__ispconfig_error'} = 'Not logged in';
239     return;
240   }
241   if ($self->{'__ispconfig_session'}) {
242     unshift(@params,$self->{'__ispconfig_session'});
243   }
244   # Contact server in eval, to trap connection errors
245   warn "Calling SOAP method $method with params:\n".Dumper(\@params)."\n"
246     if $self->option('debug');
247   my $response = eval { $client->$method(@params) };
248   unless ($response) {
249     $self->{'__ispconfig_error'} = "Error contacting server: $@";
250     return;
251   }
252   # Set results and return
253   $self->{'__ispconfig_error'} = $response->fault
254                                ? "Error from server: " . $response->faultstring
255                                : '';
256   return $response->result;
257 }
258
259 =head2 api_error
260
261 Returns the error string set by L</ISPConfig3 API> methods,
262 or a blank string if most recent call produced no errors.
263
264 =cut
265
266 sub api_error {
267   my $self = shift;
268   return $self->{'__ispconfig_error'} || '';
269 }
270
271 =head2 api_error_logout
272
273 Attempts L</api_logout>, but returns L</api_error> message from
274 before logout was attempted.  Useful for logging out
275 properly after an error.
276
277 =cut
278
279 sub api_error_logout {
280   my $self = shift;
281   my $error = $self->api_error;
282   $self->api_logout;
283   return $error;
284 }
285
286 =head2 api_login
287
288 Initializes an api session using the credentials for this export.
289 Returns true on success, false on failure.
290 Retrieve error messages using L</api_error>.
291
292 =cut
293
294 sub api_login {
295   my $self = shift;
296   if ($self->{'__ispconfig_session'} || $self->{'__ispconfig_client'}) {
297     $self->{'__ispconfig_error'} = 'Already logged in';
298     return;
299   }
300   $self->{'__ispconfig_session'} = undef;
301   $self->{'__ispconfig_client'} =
302     SOAP::Lite->proxy($self->option('soap_location'), ssl_opts => [ verify_hostname => 0 ] )
303     || undef;
304   unless ($self->{'__ispconfig_client'}) {
305     $self->{'__ispconfig_error'} = 'Error creating SOAP client';
306     return;
307   }
308   $self->{'__ispconfig_session'} = 
309     $self->api_call('login',$self->option('username'),$self->option('password'))
310     || undef;
311   return unless $self->{'__ispconfig_session'};
312   return 1;
313 }
314
315 =head2 api_logout
316
317 Ends the current api session established by L</api_login>.
318 Returns true on success, false on failure.
319
320 =cut
321
322 sub api_logout {
323   my $self = shift;
324   unless ($self->{'__ispconfig_session'}) {
325     $self->{'__ispconfig_error'} = 'Not logged in';
326     return;
327   }
328   my $result = $self->api_call('logout');
329   # clear these even if there was a failure to logout
330   $self->{'__ispconfig_client'} = undef;
331   $self->{'__ispconfig_session'} = undef;
332   return if $self->api_error;
333   return 1;
334 }
335
336 # false laziness with portaone export
337 sub _substitute {
338   my ($self, $string, @objects) = @_;
339   return '' unless $string;
340   foreach my $object (@objects) {
341     next unless $object;
342     my @fields = $object->fields;
343     push(@fields,'domain') if $object->table eq 'svc_acct';
344     foreach my $field (@fields) {
345       next unless $field;
346       my $value = $object->$field;
347       $string =~ s/\$$field/$value/g;
348     }
349   }
350   # strip leading/trailing whitespace
351   $string =~ s/^\s//g;
352   $string =~ s/\s$//g;
353   return $string;
354 }
355
356 =head1 SEE ALSO
357
358 L<FS::part_export>
359
360 =head1 AUTHOR
361
362 Jonathan Prykop 
363 jonathan@freeside.biz
364
365 =cut
366
367 1;
368
369