File Coverage

blib/lib/XMail/Ctrl.pm
Criterion Covered Total %
statement 12 149 8.0
branch 0 102 0.0
condition 0 46 0.0
subroutine 4 20 20.0
pod 0 13 0.0
total 16 330 4.8


line stmt bran cond sub pod time code
1             package XMail::Ctrl;
2              
3 1     1   40312 use strict;
  1         4  
  1         60  
4 1     1   7 use vars qw($VERSION $AUTOLOAD);
  1         4  
  1         87  
5 1     1   8 use Digest::MD5();
  1         6  
  1         21  
6 1     1   1108 use IO::Socket;
  1         98380  
  1         6  
7              
8             $VERSION = 2.3;
9              
10             =head1 NAME
11              
12             XMail::Ctrl - Crtl access to XMail server
13              
14             =head1 VERISON
15              
16             version 2.3 of XMail::Ctrl
17              
18             released 07/10/2004
19              
20             =head1 SYNOPSIS
21              
22             use XMail::Ctrl;
23             my $XMail_admin = "aaron.johnson";
24             my $XMail_pass = "mypass";
25             my $XMail_port = "6017";
26             my $XMail_host = "aopen.hank.net";
27             my $test_domain = "aopen.hank.net";
28             my $test_user = "rick";
29              
30             my $xmail = XMail::Ctrl->new(
31             ctrlid => "$XMail_admin",
32             ctrlpass => "$XMail_pass",
33             port => "$XMail_port",
34             host => "$XMail_host"
35             ) or die $!;
36              
37             my $command_ok = $xmail->useradd(
38             {
39             username => "$test_user",
40             password => 'test',
41             domain => "$test_domain",
42             usertype => 'U'
43             }
44             );
45              
46             printf("Failed to add user <%s@%s>\n", $test_user, $test_domain)
47             unless $cmd_ok;
48              
49             # setting the mailproc.tab
50              
51             my $proc = $xmail->usersetmproc(
52             {
53             username => "$test_user",
54             domain => "$test_domain",
55             output_to_file => "command for mailproc.tab",
56              
57             }
58             );
59              
60             $xmail->quit;
61              
62             =head1 DESCRIPTION
63              
64             This module allows for access to the Crtl functions for XMail.
65             It operates over TCP/IP. It can be used to communicate with either
66             Windows or Linux based XMail based servers.
67              
68             The code was written on a Win32 machine and has been tested on
69             Mandrake and Red Hat Linux as well with Perl version 5.6 and 5.8
70              
71             As of version 2.0 all code is written on under a Linux platform
72             using Perl 5.8. It has been tested on:
73             - Mandrake 9.0 with Perl 5.8 by Aaron Johnson
74             - Mandrake 8.2 with Perl 5.6.1 by Aaron Johnson
75             - ActiveState Perl (5.8) on Windows by Thomas Loo
76              
77             Version 2.0 and higher require Digest::MD5, all passwords are
78             now sent as an MD5 value.
79              
80             =head2 Overview
81              
82             All commands take the same arguments as outlined in the XMail
83             (http://www.xmailserver.com) documentation. All commands are
84             processed by name and arguments can be sent in the any order.
85              
86             Example command from manual (is one line):
87             "useradd"[TAB]"domain"[TAB]"username"[TAB]"password"[TAB]"usertype"
88              
89             This turns into:
90              
91             $xmail->useradd( {
92             domain => "domain.com",
93             username => "username",
94             password => "password",
95             usertype => "U"
96             }
97             );
98              
99             You can put the four parts in any order, they are put in the
100             correct order by the modules internals.
101              
102             The command structure for XMail allows a fairly easy interface
103             to the command set. This module has NO hardcoded xmail methods.
104             As long as the current ordering of commands is followed in the
105             XMail core the module should work to any new commands unchanged.
106              
107             Any command that accepts vars can be used
108             by doing the following:
109              
110             To send uservarsset (user.tab) add a vars anonymous hash,
111             such as:
112              
113             $xmail->uservarsset( {
114             domain => 'aopen.hank.net',
115             username => 'rick',
116             vars => {
117             RealName => 'Willey FooFoo',
118             RemoteAddress => '300.000.000.3',
119             VillageGrid => '45678934'
120             }
121             } );
122              
123             The ".|rm" command can used as described in the XMail docs.
124              
125             If you are having problems you might want to turn on debugging
126             (new in 1.5)
127              
128             $xmail->debug(1);
129              
130             to help you track down the cause.
131              
132             Setting the debug level to 4 will provide a very complete
133             output of the communication between the server and your
134             program. A line starting with >> (incoming) indicates what the Ctrl
135             service sent back and << (outgoing) indicates what the XMail::Ctrl
136             sent to the server.
137              
138             All commands return a 1 if successful and undef on failure.
139              
140             =head2 Lists
141              
142             Lists are now (as of 1.3) returned as an array reference unless
143             you set the raw_list method to true.
144              
145             $xmail->raw_list(1);
146              
147             To print the lists you can use a loop like this:
148              
149             my $list = $xmail->userlist( { domain => 'yourdomin.net' } );
150             foreach my $row (@{$list}) {
151             print join("\t",@{$row}) . "\n";
152             }
153              
154             Refer to the XMail documentation for each command for information
155             on which columns will be returned for a particular command.
156              
157             You can send a noop (keeps the connection alive) with:
158              
159             $xmail->noop();
160              
161             As of version 1.5 you can perform any froz command:
162              
163             $froz = $xmail->frozlist();
164              
165             foreach my $frozinfo (@{$froz}) {
166             s/\"//g foreach @{$frozinfo};
167             $res = $xmail->frozdel( {
168             lev0 => "$frozinfo->[1]",
169             lev1 => "$frozinfo->[2]" || '0',
170             msgfile => "$frozinfo->[0]",
171             });
172             print $res , "\n";
173             }
174              
175             =head1 BUGS
176              
177             Possible problems dealing with wild card requests. I have
178             not tested this fully. Please send information on what you
179             are attempting if you feel the module is not providing the
180             correct function.
181              
182             =head1 AUTHOR
183              
184             Aaron Johnson
185             solution@gina.net
186              
187             =head1 THANKS
188              
189             Thanks to Davide Libenzi for a sane mail server with
190             an incredibly consistent interface for external control.
191              
192             Thanks to Mark-Jason Dominus for his wonderful classes at
193             the 2000 Perl University in Atlanta, GA where the power of
194             AUTOLOAD was revealed to me.
195              
196             Thanks to my Dad for buying that TRS-80 in 1981 and getting
197             me addicted to computers.
198              
199             Thanks to my wife for leaving me alone while I write my code
200             :^)
201              
202             Thanks to Oscar Sosa for spotting the lack of support for
203             editing the 'tab' files
204              
205             Thanks to Thomas Loo for making many major refactoring
206             contributions for version 2.0 as well as providing better
207             debugging output.
208              
209             =head1 CHANGES
210              
211             Changes file included in distro
212              
213             =head1 COPYRIGHT
214              
215             Copyright (c) 2000,2001,2002,2003 Aaron Johnson.
216             All rights Reserved. This module is free software.
217             It may be used, redistributed and/or modified under
218             the same terms as Perl itself.
219              
220             =cut
221              
222             # Perl interface to crtl for XMail
223             # Written by Aaron Johnson solution@gina.net
224              
225             # Once you create a new xmail connection don't
226             # let it sit around too long or it will time out!
227              
228             sub new {
229              
230 0     0 0   my ( $class, %args ) = @_;
231              
232 0   0       my $self = bless {
      0        
      0        
      0        
      0        
233             _helo => {},
234             _last_error => {},
235             _last_success => {},
236             _command_ok => 0,
237             _io => undef,
238             _ctrlid => $args{ctrlid} || "",
239             _ctrlpass => $args{ctrlpass} || "",
240             _host => $args{host} || "127.0.0.1",
241             _port => $args{port} || 6017,
242             debug => $args{debug} || 0,
243             }, $class;
244              
245             # no point of connecting unless we got a password
246 0 0         return $self unless $args{ctrlpass};
247              
248             # Skip connection with argument no_connect
249 0 0         $self->connect unless $args{no_connect};
250 0           $self;
251             }
252              
253             # connect
254             # returns a 0/1 value indicating the result
255             # errors are retrieved by last_error as such.
256             #
257             # print $ctrl->last_error->{description}
258             # unless $ctrl->connect;
259             #
260             # errors could be one of:
261             # * Failed connecting to server ([socket_info])
262             # * Authentication failed
263              
264             sub connect {
265 0     0 0   my $self = shift;
266              
267             # return ok if a connection is already present
268 0 0         $self->connected and return 1;
269              
270 0           my ( $host, $port ) = ( $self->{_host}, $self->{_port} );
271              
272 0           $self->{_io} =
273             IO::Socket::INET->new( PeerAddr => $host, PeerPort => $port );
274              
275 0 0 0       $self->last_error("Connection failed [$host:$port] ($@)")
276             and return 0
277             unless defined $self->{_io};
278              
279 0 0         print STDOUT "\n" if $self->debug > 1;
280              
281             # get the helo string or return failure
282 0 0         defined( my $buf = $self->_recv ) or return 0;
283              
284             # gather some useful stuff from the helo string
285            
286             # version 1.19 and above no longer return OS removed
287 0           $buf =~ /^\+\d+ (<[\d\.@]+>)\D+([\d\.]+)/; # \(([^\)]+)\).+/;
288 0           $self->{_helo} = { timestamp => $1,
289             version => $2,
290             # os => $3
291             };
292              
293             # create and send MD5 auth string
294 0 0         $self->_send(
295             $self->{_ctrlid} . "\t#"
296             . Digest::MD5::md5_hex(
297             $self->{_helo}{timestamp} . $self->{_ctrlpass}
298             )
299             )
300             or return 0; # shouldn't happen.
301              
302             # receive auth results
303 0           $buf = $self->_recv;
304              
305             # auth not accepted ?
306 0 0 0       unless ( defined $buf && $buf =~ /^\+/ ) {
307              
308             # upon a xmail MD5 auth failure, xmail returns a
309             # "-00171 Resource lock entry not found". don't think this status
310             # fits very well and there actually is a ERR_MD5_AUTH_FAILED (-152)
311             # defined in the xmail errorcode table. Reporting that instead
312             # since that more accurately describes what just happened.
313              
314 0           $self->last_error( "00152",
315             "MD5 authentication failed [$self->{_ctrlid}\@$host:$port]" );
316              
317             # the server will cut the connection here, so we'd better get rid of
318             # the socket object accordingly
319 0           undef $self->{_io};
320 0           return 0;
321             }
322              
323 0 0         $buf =~ /^.(\d+)\s?(.*)/ and $self->last_success( $1, $2 );
324 0           1;
325             }
326              
327             # helo,
328             # returns a 3-key hash (timestamp,version,os)
329             # Calling this method before a connection is made
330             # obviously will return an empty hash. Helo information
331             # will be unset when a call to quit is made.
332             sub helo {
333 0     0 0   (shift)->{_helo};
334             }
335              
336             # connected,
337             # returns the connection state.
338             sub connected {
339 0     0 0   my $self = shift;
340 0 0 0       return ( defined $self->{_io} && $self->{_io}->connected ) ? 1 : 0;
341             }
342              
343             # last_error,
344             # returns a two-key hash (code/description) exposing the last
345             # error encountered. method quit will undefine. on no errors
346             # an emtpy hash is returned. If running in debug mode, errors
347             # are additionally printed out to the console as they appear.
348             sub last_error {
349 0     0 0   my ( $self, $code, $desc ) = @_;
350 0 0         if ($code) {
351              
352             # if there the code is not a xmail code and
353             # the desc has no value then we shift
354             # the description to be the code and
355             # assign our custom error code (-99999)
356 0 0 0       if ( $code !~ /^\d+/ || !$desc ) {
357 0           $desc = $code;
358 0           $code = "99999";
359             }
360 0           $desc =~ s/\r?\n$//;
361 0 0         print STDOUT "error: code:$code description:$desc\n"
362             if $self->{debug};
363 0           $self->{_last_error} = { code => $code, description => $desc };
364             }
365 0           $self->{_last_error};
366             }
367              
368             # last_success,
369             # returns a two-key hash (code/description) exposing the last
370             # successfull xcommand. method quit will undefine.
371             sub last_success {
372 0     0 0   my ( $self, $code, $desc ) = @_;
373 0 0         if ( defined($code) ) {
374 0 0         return $self->{_last_success} = {} if $code eq '0'; #reset
375 0 0         $desc =~ s/\r?\n$// if $desc;
376 0 0         print STDOUT "ok : code:$code description:$desc\n"
377             if $self->{debug} > 2;
378 0           $self->{_last_success} = { code => $code, description => $desc };
379             }
380 0           $self->{_last_success};
381             }
382              
383             # debug,
384             # sets debug mode (0-2)
385             sub debug {
386 0     0 0   my ( $self, $set ) = @_;
387 0 0         $self->{debug} = $set if defined $set;
388 0           $self->{debug};
389             }
390              
391             # _send, wraps socket recv + does dbg output. returns 0 or 1
392             sub _send {
393 0     0     my ( $self, $data ) = @_;
394 0 0         $data .= "\r\n" unless $data =~ /\r?\n$/;
395              
396             # if the socket has been shutdown by the server, send returns
397             # a defined value,(perlfunc says otherwise) but it will atleast
398             # reset the connected state to false, so by additionally check
399             # connection state after send, we can detect a dead peer and
400             # perform a transparent reconnect and retransmit of the last command...
401 0 0 0       unless(defined $self->{_io}->send($data) && $self->connected){
402              
403             # socket is down, reconnect and retransmit
404 0 0         print STDOUT "info : reconnecting [$self->{_host}:$self->{_port}]\n"
405             if $self->debug > 2;
406             # still failing ? then report a permanent error...
407 0 0 0       $self->last_error("socket::send failed, no connection")
      0        
408             and return 0
409             unless $self->connect && defined $self->{_io}->send($data);
410             }
411              
412 0 0         print STDOUT "debug:<< $data" if $self->debug > 1;
413 0           1;
414             }
415              
416             # _recv, wraps socket recv + does dbg output. returns indata or undef
417             sub _recv {
418 0     0     my ( $self, $bufsz ) = @_;
419 0           my $buf;
420 0 0         return undef unless $self->connected;
421              
422 0 0 0       $self->last_error("socket::recv failed, no connection")
      0        
      0        
423             and return undef
424             unless $self->connected && defined $self->{_io}->recv( $buf, $bufsz || 128 );
425            
426 0 0         print STDOUT "debug:>> $buf" if $self->debug > 1;
427 0           $buf;
428             }
429              
430             # xcommand, invoked by the autoloaded method
431             #
432             # * on a getter command, x data is returned if the command
433             # was successful. otherwise undef is returned.
434             # my $data = $ctrl->userlist(...);
435             # print $ctrl->last_error->{code} unless defined $data;
436             #
437             # * on a setter command, undef/1 is returned indicating the result
438             # print $ctrl->last_error->{description}
439             # unless $ctrl->useradd(...) [ ==1 ]
440             #
441             # An eventual error occuring during the transaction is
442             # retrieved by the last_error method
443             #
444             sub xcommand {
445 0     0 0   my ( $self, $args ) = @_;
446 0           $self->command_ok(0);
447              
448             # $self->last_success(0);
449              
450 0           my @build_command = qw(
451             domain
452             alias
453             account
454             mlusername
455             username
456             password
457             mailaddress
458             perms
459             usertype
460             loc-domain
461             loc-username
462             extrn-domain
463             extrn-username
464             extrn-password
465             authtype
466             relative-file-path
467             vars
468             lev0
469             lev1
470             msgfile
471             );
472            
473 0           my $command = delete $args->{command};
474 0           foreach my $step (@build_command) {
475 0 0         if ( ref $args->{$step} ne "HASH" ) {
476 0 0         $command .= "\t$args->{$step}" if $args->{$step};
477             }
478             else {
479 0           foreach my $varname ( keys %{ $args->{$step} } ) {
  0            
480 0           $command .= "\t$varname\t$args->{$step}{$varname}";
481             }
482             }
483 0           delete $args->{$step};
484             }
485              
486             # no connection, try bring one up, return on failure
487 0 0         $self->connect or return undef;
488              
489             # make debug output reader friendly
490 0 0         print STDOUT "\n" if $self->debug > 1;
491              
492             # issue the command, return if send failure
493 0 0         $self->_send($command) or return undef;
494              
495 0           local ($_);
496 0           my $sck = $self->{_io};
497 0           my ( $charge, $mode, $desc, $line, @data );
498 0           while ( defined( $line = <$sck> ) ) {
499 0 0         print STDOUT "debug:>> $line" if $self->debug > 1;
500 0 0         if ( defined $mode ) {
501              
502             # weed out newlines
503 0           $line =~ s/\r?\n$//;
504              
505             # end of input, break outta here
506 0 0         last if $line =~ /^\.$/;
507              
508             # pile up input
509 0           push ( @data, $line );
510             }
511             else {
512 0 0         if ( $line =~ /^(.)(\d+)\s?(.*)/ ) {
513 0           ( $charge, $mode, $desc ) = ( $1, $2, $3 );
514             }
515              
516             # report '-' unless regexp matched
517 0   0       $self->command_ok( $charge || '-' );
518              
519 0 0         if ( $charge eq '+' ) {
520 0           $self->last_success( $mode, $desc );
521 0 0         return 1 if $mode eq '00000';
522 0 0         last if $mode ne '00100';
523              
524             }
525             else {
526 0           $self->last_error( $mode, $desc );
527 0           return undef;
528             }
529             }
530             }
531              
532 0 0 0       $self->last_error("Unknown recv error")
533             and return undef
534             if not defined $mode; # cannot happen ?! :~/
535              
536             # got a +00101 code, xmail expects a list
537 0 0         if ( $mode eq '00101' ) {
538 0           @data =
539             ( ref( $args->{output_to_file} ) eq 'ARRAY' )
540 0 0         ? @{ $args->{output_to_file} }
541             : split ( /\r?\n/, $args->{output_to_file} );
542              
543 0           for (@data) {
544              
545             # From Xmail docs section "Setting mailproc.tab file":
546             # if line begins with a period... take care of that.
547 0 0         $_ = ".$_" if /^\./;
548 0 0         $self->_send($_) or last; # end if error
549             }
550 0           $self->_send(".");
551 0           $line = $self->_recv;
552              
553             # determine whether the list was accepted..
554 0 0 0       $line =~ /^(.)(\d+)\s?(.*)/
      0        
555             or $self->last_error( $line || "Unknown recv error" )
556             and return undef;
557              
558 0           ( $charge, $mode, $desc ) = ( $1, $2, $3 );
559              
560             # set error and return unless good return status
561 0 0 0       $self->last_error( $mode, $desc )
562             and return undef
563             unless $charge eq '+';
564              
565             # command_ok should be updated here aswell
566 0           $self->command_ok($charge);
567              
568             # update last_success
569 0           $self->last_success( $mode, $desc );
570 0           return 1;
571             }
572              
573             # got a +00100, a list as indata
574             # return as-is unless told otherwise, the rare case I'd presume
575 0 0         return ( join ( "\r\n", @data ) . "\r\n" ) if $self->raw_list;
576              
577             # ...otherwise, build up an array ref
578 0           my $array_ref;
579 0           my $count = 0;
580              
581             # attempting to save some memory on large lists
582 0           while ( defined( $_ = shift @data ) ) {
583 0           tr/"//d;
584 0           $array_ref->[ $count++ ] = [ split /\t/ ];
585             }
586 0           return $array_ref;
587             }
588              
589             sub error {
590 0     0 0   my ($self) = @_;
591 0           return $self->last_error->{code};
592             }
593              
594             sub mode {
595 0     0 0   my ($self) = @_;
596 0           return $self->last_success->{code};
597             }
598              
599             sub command_ok {
600 0     0 0   my ( $self, $value ) = @_;
601 0 0         return $self->{_command_ok} if ( !defined($value) );
602 0 0         $self->{_command_ok} = ( $value eq '+' ) ? 1 : 0;
603             }
604              
605             sub raw_list {
606 0     0 0   my ( $self, $value ) = @_;
607 0 0         if ($value) {
608 0           $self->{raw_list} = $value;
609             }
610             else {
611 0           return $self->{raw_list};
612             }
613             }
614              
615             sub quit {
616 0     0 0   my $self = shift;
617 0           $self->{_helo} = {};
618 0           $self->{_last_error} = {};
619 0           $self->{_last_success} = {};
620 0 0         if ( $self->connected ) {
621 0           $self->_send("quit");
622 0           $self->{_io}->close;
623 0           undef $self->{_io};
624             }
625             }
626              
627             sub AUTOLOAD {
628 0     0     my ( $self, $args ) = @_;
629              
630 0           $AUTOLOAD =~ /.*::(\w+)/;
631 0           my $command = $1;
632 0 0         if ( $command =~ /[A-Z]/ ) { exit }
  0            
633 0           $args->{command} = $command;
634 0           $self->xcommand($args);
635             }
636              
637             1;