File Coverage

blib/lib/XMail/Ctrl.pm
Criterion Covered Total %
statement 15 155 9.6
branch 0 102 0.0
condition 0 46 0.0
subroutine 5 21 23.8
pod 0 13 0.0
total 20 337 5.9


line stmt bran cond sub pod time code
1             package XMail::Ctrl;
2              
3 1     1   18326 use strict;
  1         2  
  1         58  
4 1     1   7 use warnings;
  1         1  
  1         42  
5 1     1   4 use vars qw($VERSION $AUTOLOAD);
  1         5  
  1         63  
6 1     1   3 use Digest::MD5();
  1         2  
  1         11  
7 1     1   524 use IO::Socket;
  1         19347  
  1         4  
8              
9              
10              
11             # ABSTRACT: Crtl access to XMail server
12              
13             $VERSION = 2.4;
14              
15              
16             # Perl interface to crtl for XMail
17             # Written by Aaron Johnson solution@gina.net
18              
19             # Once you create a new xmail connection don't
20             # let it sit around too long or it will time out!
21              
22             sub new {
23              
24 0     0 0   my ( $class, %args ) = @_;
25              
26 0   0       my $self = bless {
      0        
      0        
      0        
      0        
27             _helo => {},
28             _last_error => {},
29             _last_success => {},
30             _command_ok => 0,
31             _io => undef,
32             _ctrlid => $args{ctrlid} || "",
33             _ctrlpass => $args{ctrlpass} || "",
34             _host => $args{host} || "127.0.0.1",
35             _port => $args{port} || 6017,
36             debug => $args{debug} || 0,
37             }, $class;
38              
39             # no point of connecting unless we got a password
40 0 0         return $self unless $args{ctrlpass};
41              
42             # Skip connection with argument no_connect
43 0 0         $self->connect unless $args{no_connect};
44 0           return $self;
45             }
46              
47             # connect
48             # returns a 0/1 value indicating the result
49             # errors are retrieved by last_error as such.
50             #
51             # print $ctrl->last_error->{description}
52             # unless $ctrl->connect;
53             #
54             # errors could be one of:
55             # * Failed connecting to server ([socket_info])
56             # * Authentication failed
57              
58             sub connect {
59 0     0 0   my $self = shift;
60              
61             # return ok if a connection is already present
62 0 0         $self->connected and return 1;
63              
64 0           my ( $host, $port ) = ( $self->{_host}, $self->{_port} );
65              
66 0           $self->{_io} =
67             IO::Socket::INET->new( PeerAddr => $host, PeerPort => $port );
68              
69 0 0 0       $self->last_error("Connection failed [$host:$port] ($@)")
70             and return 0
71             unless defined $self->{_io};
72              
73 0 0         print STDOUT "\n" if $self->debug > 1;
74              
75             # get the helo string or return failure
76 0 0         defined( my $buf = $self->_recv ) or return 0;
77              
78             # gather some useful stuff from the helo string
79            
80             # version 1.19 and above no longer return OS removed
81 0           $buf =~ /^\+\d+ (<[\d\.@]+>)\D+([\d\.]+)/; # \(([^\)]+)\).+/;
82 0           $self->{_helo} = { timestamp => $1,
83             version => $2,
84             # os => $3
85             };
86              
87             # create and send MD5 auth string
88 0 0         $self->_send(
89             $self->{_ctrlid} . "\t#"
90             . Digest::MD5::md5_hex(
91             $self->{_helo}{timestamp} . $self->{_ctrlpass}
92             )
93             )
94             or return 0; # shouldn't happen.
95              
96             # receive auth results
97 0           $buf = $self->_recv;
98              
99             # auth not accepted ?
100 0 0 0       unless ( defined $buf && $buf =~ /^\+/ ) {
101              
102             # upon a xmail MD5 auth failure, xmail returns a
103             # "-00171 Resource lock entry not found". don't think this status
104             # fits very well and there actually is a ERR_MD5_AUTH_FAILED (-152)
105             # defined in the xmail errorcode table. Reporting that instead
106             # since that more accurately describes what just happened.
107              
108 0           $self->last_error( "00152",
109             "MD5 authentication failed [$self->{_ctrlid}\@$host:$port]" );
110              
111             # the server will cut the connection here, so we'd better get rid of
112             # the socket object accordingly
113 0           undef $self->{_io};
114 0           return 0;
115             }
116              
117 0 0         $buf =~ /^.(\d+)\s?(.*)/ and $self->last_success( $1, $2 );
118 0           return 1;
119             }
120              
121             # helo,
122             # returns a 3-key hash (timestamp,version,os)
123             # Calling this method before a connection is made
124             # obviously will return an empty hash. Helo information
125             # will be unset when a call to quit is made.
126             sub helo {
127 0     0 0   return (shift)->{_helo};
128             }
129              
130             # connected,
131             # returns the connection state.
132             sub connected {
133 0     0 0   my $self = shift;
134 0 0 0       return ( defined $self->{_io} && $self->{_io}->connected ) ? 1 : 0;
135             }
136              
137             # last_error,
138             # returns a two-key hash (code/description) exposing the last
139             # error encountered. method quit will undefine. on no errors
140             # an emtpy hash is returned. If running in debug mode, errors
141             # are additionally printed out to the console as they appear.
142             sub last_error {
143 0     0 0   my ( $self, $code, $desc ) = @_;
144 0 0         if ($code) {
145              
146             # if there the code is not a xmail code and
147             # the desc has no value then we shift
148             # the description to be the code and
149             # assign our custom error code (-99999)
150 0 0 0       if ( $code !~ /^\d+/ || !$desc ) {
151 0           $desc = $code;
152 0           $code = "99999";
153             }
154 0           $desc =~ s/\r?\n$//;
155 0 0         print STDOUT "error: code:$code description:$desc\n"
156             if $self->{debug};
157 0           $self->{_last_error} = { code => $code, description => $desc };
158             }
159 0           return $self->{_last_error};
160            
161             }
162              
163             # last_success,
164             # returns a two-key hash (code/description) exposing the last
165             # successfull xcommand. method quit will undefine.
166             sub last_success {
167 0     0 0   my ( $self, $code, $desc ) = @_;
168 0 0         if ( defined($code) ) {
169 0 0         return $self->{_last_success} = {} if $code eq '0'; #reset
170 0 0         $desc =~ s/\r?\n$// if $desc;
171 0 0         print STDOUT "ok : code:$code description:$desc\n"
172             if $self->{debug} > 2;
173 0           $self->{_last_success} = { code => $code, description => $desc };
174             }
175 0           return $self->{_last_success};
176             }
177              
178             # debug,
179             # sets debug mode (0-2)
180             sub debug {
181 0     0 0   my ( $self, $set ) = @_;
182 0 0         $self->{debug} = $set if defined $set;
183 0           return $self->{debug};
184             }
185              
186             # _send, wraps socket recv + does dbg output. returns 0 or 1
187             sub _send {
188 0     0     my ( $self, $data ) = @_;
189 0 0         $data .= "\r\n" unless $data =~ /\r?\n$/;
190              
191             # if the socket has been shutdown by the server, send returns
192             # a defined value,(perlfunc says otherwise) but it will atleast
193             # reset the connected state to false, so by additionally check
194             # connection state after send, we can detect a dead peer and
195             # perform a transparent reconnect and retransmit of the last command...
196 0 0 0       unless(defined $self->{_io}->send($data) && $self->connected){
197              
198             # socket is down, reconnect and retransmit
199 0 0         print STDOUT "info : reconnecting [$self->{_host}:$self->{_port}]\n"
200             if $self->debug > 2;
201             # still failing ? then report a permanent error...
202 0 0 0       $self->last_error("socket::send failed, no connection")
      0        
203             && return 0
204             unless $self->connect && defined $self->{_io}->send($data);
205             }
206              
207 0 0         print STDOUT "debug:<< $data" if $self->debug > 1;
208 0           return 1;
209             }
210              
211             # _recv, wraps socket recv + does dbg output. returns indata or undef
212             sub _recv {
213 0     0     my ( $self, $bufsz ) = @_;
214 0           my $buf;
215 0 0         return unless $self->connected;
216              
217 0 0 0       $self->last_error("socket::recv failed, no connection")
      0        
      0        
218             && return
219             unless $self->connected && defined $self->{_io}->recv( $buf, $bufsz || 128 );
220            
221 0 0         print STDOUT "debug:>> $buf" if $self->debug > 1;
222 0           return $buf;
223             }
224              
225             # xcommand, invoked by the autoloaded method
226             #
227             # * on a getter command, x data is returned if the command
228             # was successful. otherwise undef is returned.
229             # my $data = $ctrl->userlist(...);
230             # print $ctrl->last_error->{code} unless defined $data;
231             #
232             # * on a setter command, undef/1 is returned indicating the result
233             # print $ctrl->last_error->{description}
234             # unless $ctrl->useradd(...) [ ==1 ]
235             #
236             # An eventual error occuring during the transaction is
237             # retrieved by the last_error method
238             #
239             sub xcommand {
240 0     0 0   my ( $self, $args ) = @_;
241 0           $self->command_ok(0);
242              
243             # $self->last_success(0);
244              
245 0           my @build_command = qw(
246             domain
247             alias
248             account
249             mlusername
250             username
251             password
252             mailaddress
253             perms
254             usertype
255             loc-domain
256             loc-username
257             extrn-domain
258             extrn-username
259             extrn-password
260             authtype
261             relative-file-path
262             vars
263             lev0
264             lev1
265             msgfile
266             );
267            
268 0           my $command = delete $args->{command};
269 0           foreach my $step (@build_command) {
270 0 0         if ( ref $args->{$step} ne "HASH" ) {
271 0 0         $command .= "\t$args->{$step}" if $args->{$step};
272             }
273             else {
274 0           foreach my $varname ( keys %{ $args->{$step} } ) {
  0            
275 0           $command .= "\t$varname\t$args->{$step}{$varname}";
276             }
277             }
278 0           delete $args->{$step};
279             }
280              
281             # no connection, try bring one up, return on failure
282 0 0         $self->connect or return;
283              
284             # make debug output reader friendly
285 0 0         print STDOUT "\n" if $self->debug > 1;
286              
287             # issue the command, return if send failure
288 0 0         $self->_send($command) or return;
289              
290 0           local ($_);
291 0           my $sck = $self->{_io};
292 0           my ( $charge, $mode, $desc, $line, @data );
293 0           while ( defined( $line = <$sck> ) ) {
294 0 0         print STDOUT "debug:>> $line" if $self->debug > 1;
295 0 0         if ( defined $mode ) {
296              
297             # weed out newlines
298 0           $line =~ s/\r?\n$//;
299              
300             # end of input, break outta here
301 0 0         last if $line =~ /^\.$/;
302              
303             # pile up input
304 0           push ( @data, $line );
305             }
306             else {
307 0 0         if ( $line =~ /^(.)(\d+)\s?(.*)/ ) {
308 0           ( $charge, $mode, $desc ) = ( $1, $2, $3 );
309             }
310              
311             # report '-' unless regexp matched
312 0   0       $self->command_ok( $charge || '-' );
313              
314 0 0         if ( $charge eq '+' ) {
315 0           $self->last_success( $mode, $desc );
316 0 0         return 1 if $mode eq '00000';
317 0 0         last if $mode ne '00100';
318              
319             }
320             else {
321 0           $self->last_error( $mode, $desc );
322 0           return;
323             }
324             }
325             }
326              
327 0 0 0       $self->last_error("Unknown recv error")
328             and return
329             if not defined $mode; # cannot happen ?! :~/
330              
331             # got a +00101 code, xmail expects a list
332 0 0         if ( $mode eq '00101' ) {
333 0           @data =
334             ( ref( $args->{output_to_file} ) eq 'ARRAY' )
335 0 0         ? @{ $args->{output_to_file} }
336             : split ( /\r?\n/, $args->{output_to_file} );
337              
338 0           for (@data) {
339              
340             # From Xmail docs section "Setting mailproc.tab file":
341             # if line begins with a period... take care of that.
342 0 0         $_ = ".$_" if /^\./;
343 0 0         $self->_send($_) or last; # end if error
344             }
345 0           $self->_send(".");
346 0           $line = $self->_recv;
347              
348             # determine whether the list was accepted..
349 0 0 0       $line =~ /^(.)(\d+)\s?(.*)/
      0        
350             or $self->last_error( $line || "Unknown recv error" )
351             and return;
352              
353 0           ( $charge, $mode, $desc ) = ( $1, $2, $3 );
354              
355             # set error and return unless good return status
356 0 0 0       $self->last_error( $mode, $desc )
357             and return
358             unless $charge eq '+';
359              
360             # command_ok should be updated here aswell
361 0           $self->command_ok($charge);
362              
363             # update last_success
364 0           $self->last_success( $mode, $desc );
365 0           return 1;
366             }
367              
368             # got a +00100, a list as indata
369             # return as-is unless told otherwise, the rare case I'd presume
370 0 0         return ( join ( "\r\n", @data ) . "\r\n" ) if $self->raw_list;
371              
372             # ...otherwise, build up an array ref
373 0           my $array_ref;
374 0           my $count = 0;
375              
376             # attempting to save some memory on large lists
377 0           while ( defined( $_ = shift @data ) ) {
378 0           tr/"//d;
379 0           $array_ref->[ $count++ ] = [ split /\t/ ];
380             }
381 0           return $array_ref;
382             }
383              
384             sub error {
385 0     0 0   my ($self) = @_;
386 0           return $self->last_error->{code};
387             }
388              
389             sub mode {
390 0     0 0   my ($self) = @_;
391 0           return $self->last_success->{code};
392             }
393              
394             sub command_ok {
395 0     0 0   my ( $self, $value ) = @_;
396 0 0         return $self->{_command_ok} if ( !defined($value) );
397 0 0         $self->{_command_ok} = ( $value eq '+' ) ? 1 : 0;
398 0           return $self->{_command_ok};
399             }
400              
401             sub raw_list {
402 0     0 0   my ( $self, $value ) = @_;
403 0 0         if ($value) {
404 0           $self->{raw_list} = $value;
405 0           return;
406             }
407             else {
408 0           return $self->{raw_list};
409             }
410            
411             }
412              
413             sub quit {
414 0     0 0   my $self = shift;
415 0           $self->{_helo} = {};
416 0           $self->{_last_error} = {};
417 0           $self->{_last_success} = {};
418 0 0         if ( $self->connected ) {
419 0           $self->_send("quit");
420 0           $self->{_io}->close;
421 0           undef $self->{_io};
422             }
423 0           return;
424             }
425              
426             sub AUTOLOAD {
427 0     0     my ( $self, $args ) = @_;
428              
429 0           $AUTOLOAD =~ /.*::(\w+)/;
430 0           my $command = $1;
431 0 0         if ( $command =~ /[A-Z]/ ) { exit }
  0            
432 0           $args->{command} = $command;
433 0           return $self->xcommand($args);
434            
435             }
436              
437             1;
438              
439             __END__