File Coverage

blib/lib/ZCS/Admin.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package ZCS::Admin;
2              
3 1     1   20355 use strict;
  1         3  
  1         38  
4 1     1   5 use warnings;
  1         2  
  1         33  
5 1     1   966 use LWP::UserAgent qw();
  1         51030  
  1         24  
6 1     1   9 use URI qw();
  1         2  
  1         17  
7 1     1   969 use ZCS::Admin::Interfaces::Admin::AdminSoap12 ();
  0            
  0            
8              
9             #OFF use SOAP::Lite ( +trace => "debug" );
10             our $VERSION = '0.07';
11              
12             =head1 NAME
13              
14             ZCS::Admin - module for the Zimbra Collaboration Suite (ZCS) Admin web services
15              
16             =head1 SYNOPSIS
17              
18             use ZCS::Admin;
19              
20             my $zimbra = ZCS::Admin->new;
21             my $resp = $zimbra->auth( name => 'admin', password => 'mypass' );
22             die ZCS::Admin->faultinfo($resp) if !$resp;
23             ...
24              
25             =head1 DESCRIPTION
26              
27             The ZCS::Admin Perl module uses SOAP to interface with the Zimbra
28             Collaboration Suite Admin web services (primarily SOAP but also REST).
29              
30             =head1 METHODS
31              
32             =head2 new
33              
34             my $z = ZCS::Admin->new(
35             name => 'zimbra',
36             password => $pass,
37             );
38             die ZCS::Admin->faultinfo($z) if !$z;
39              
40             Create a new instance of ZCS::Admin. On errors a SOAP fault object is
41             returned. See SOAP::WSDL documentation for details of the SOAP fault
42             object.
43              
44             During object instantiation the L method is called to ensure
45             communcation with the server is possible.
46              
47             The default the Admin SOAP service URL is typically:
48              
49             https://127.0.0.1:7071/service/admin/soap
50              
51             Use the 'proxy' argument to specify a different URL than the default:
52              
53             ...->new( proxy => 'https://my.svr.loc:7071/service/admin/soap', ... )
54              
55             =cut
56              
57             sub new {
58             my ( $class, %args ) = @_;
59              
60             my $info = "name => , password => , [proxy => ]";
61             Carp::confess("usage: new($info)\n")
62             unless ( exists $args{name} and $args{password} );
63              
64             my $self = {%args};
65             bless( $self, $class );
66              
67             my $r = $self->cl;
68             return $r ? $self->auth : $r;
69             }
70              
71             =pod
72              
73             A ZCS::Admin has the following object attributes:
74              
75             =over 4
76              
77             =item name
78              
79             The user name for authentication.
80              
81             =item password
82              
83             The password for authentication.
84              
85             =item proxy
86              
87             The URL of the ZCS Admin SOAP service
88              
89             =back
90              
91             =cut
92              
93             sub name { @_ > 1 ? $_[0]->{name} = $_[1] : $_[0]->{name}; }
94             sub password { @_ > 1 ? $_[0]->{password} = $_[1] : $_[0]->{password}; }
95             sub proxy { @_ > 1 ? $_[0]->{proxy} = $_[1] : $_[0]->{proxy}; }
96              
97             =head2 new_element
98              
99             $z->new_element($element);
100              
101             Get the instance of a "ZCS::Admin::Elements::$element" object.
102              
103             Note: This can be used as a class or object method.
104              
105             =cut
106              
107             sub new_element {
108             my ( $self, $elem, @args ) = @_;
109              
110             # default to ZCS::Admin::Elements::...
111             $elem = __PACKAGE__ . "::Elements::" . $elem
112             if ( $elem and $elem !~ /::/ );
113             eval "require $elem" || die $@; ## no critic (ProhibitStringyEval)
114              
115             return $elem->new(@args);
116             }
117              
118             =head2 new_type
119              
120             $z->new_type($element);
121              
122             Get the instance of a "ZCS::Admin::Types::$type" object.
123              
124             Note: This can be used as a class or object method.
125              
126             =cut
127              
128             sub new_type {
129             my ( $self, $type, @args ) = @_;
130              
131             # default to ZCS::Admin::Types::...
132             $type = __PACKAGE__ . "::Types::" . $type
133             if ( $type and $type !~ /::/ );
134             eval "require $type" || die $@; ## no critic (ProhibitStringyEval)
135              
136             return $type->new(@args);
137             }
138              
139             =head2 new_fault
140              
141             $z->new_fault( \%args );
142              
143             Get the instance of a SOAP::WSDL::SOAP::Typelib::Fault11.
144              
145             Note: This can be used as a class or object method.
146              
147             Warning: the object type is likely to change in a future release but
148             the object will still likely behave in a similar manner to the current
149             object.
150              
151             =cut
152              
153             # faultcode => ..., faultstring => ...
154             sub new_fault {
155             my ( $self, %args ) = @_;
156             require SOAP::WSDL::SOAP::Typelib::Fault11;
157             return SOAP::WSDL::SOAP::Typelib::Fault11->new( \%args );
158             }
159              
160             =head2 faultinfo
161              
162             $z->faultinfo($fault);
163              
164             Note: This can be used as a class or object method.
165              
166             Returns a string containing the concatenation of "Code" from the ZCS
167             fault detail (if available), the "faultstring", and the "Trace" from
168             the ZCS fault detail (if available).
169              
170             =cut
171              
172             sub faultinfo {
173             my ( $class, $fault ) = @_;
174             return "" unless ref($fault);
175              
176             my ( $code, $trace ) = ( [], [] );
177             my $error = $fault->get_detail ? $fault->get_detail->get_Error : undef;
178             ( $code, $trace ) = ( [ $error->get_Code ], [ $error->get_Trace ] )
179             if ($error);
180              
181             return join( "; ", @$code, $fault->get_faultstring, @$trace );
182             }
183              
184             =head2 client
185              
186             Creates and returns a new instance of
187             ZCS::Admin::Interfaces::Admin::AdminSoap12, which is the underlying
188             object being used to communicate with the ZCS Admin SOAP service. On
189             errors a SOAP fault object is returned. See SOAP::WSDL documentation
190             for details of the SOAP fault object.
191              
192             =cut
193              
194             sub client {
195             my ($self) = @_;
196              
197             my $r = $self->{_client};
198             unless ($r) {
199             my @proxy = $self->proxy ? ( proxy => $self->proxy ) : ();
200             $r = $self->{_client} =
201             ZCS::Admin::Interfaces::Admin::AdminSoap12->new( {@proxy} );
202             }
203             return $r; # a client or fault object
204             }
205              
206             =head2 cl
207              
208             Gets a ZCS::Admin::Interfaces::Admin::AdminSoap12 object via client()
209             and calls auth() if the current session authentication information has
210             expired or no session information is already stored.
211              
212             =cut
213              
214             sub cl {
215             my ($self) = @_;
216              
217             my $r = $self->client;
218             if ($r) {
219             my $exp = $self->{_auth}->{expires};
220             if ( !$exp or time() > $exp ) {
221             my $cl = $r;
222             $r = $self->auth;
223             $r = $cl if $r;
224             }
225             }
226              
227             return $r; # a client or fault object
228             }
229              
230             =head1 REST and SOAP Interface Calls
231              
232             =head2 auth
233              
234             Calls Auth on the underlying ZCS Admin object, removes stale context()
235             information and caches new authentication information on success.
236             Returns itself (for call chaining if desired) or a SOAP Fault object
237             on failures.
238              
239             =cut
240              
241             sub auth {
242             my ($self) = @_;
243              
244             my %auth = ( map { $_ => $self->$_ } qw(name password) );
245             my $r = $self->client->Auth( \%auth );
246             if ($r) {
247             delete $self->{_context};
248             $self->{_auth} = {
249             expires => $r->get_lifetime / 1000 + time(),
250             sessionId => $r->get_sessionId,
251             authToken => $r->get_authToken,
252             };
253             }
254              
255             return $r ? $self : $r;
256             }
257              
258             =head2 delegateauth
259              
260             $z->delegateauth( name => $acct );
261              
262             Calls DelegateAuth on the underlying ZCS Admin object. And returns results.
263              
264             Probably need to do more but this is a start. Probably need some sort of
265             context updating in most cases. But not sure how we should manage multiple
266             contexts and push/pop them as we need.
267              
268             Returns DelegateAuth response for now.
269              
270             =cut
271              
272             sub delegateauth {
273             my ( $self, $by, $acct ) = @_;
274              
275             my $s = $self->new_type( "GetAccountSpecifier", { value => $acct } );
276             $s->attr( { "by" => $by } );
277              
278             my $e = $self->new_element( "DelegateAuthRequest", { account => $s } );
279             return $self->cl->DelegateAuth( $e, $self->context );
280             }
281              
282             =head2 context
283              
284             Returns a context element object using cached information if it exists
285             or by calling cl() if no cached data is available (auth() clears a
286             cached context object if re-authentication has taken place).
287              
288             =cut
289              
290             sub context {
291             my ($self) = @_;
292              
293             my $r = $self->{_context};
294             return $r if $r;
295              
296             $r = $self->cl;
297             if ($r) {
298             $r =
299             $self->new_element( "context",
300             { map { $_ => $self->{_auth}->{$_} } qw(sessionId authToken) } );
301             $self->{_context} = $r if $r;
302             }
303              
304             return $r;
305             }
306              
307             =head2 createaccount
308              
309             $z->createaccount( name => $name, password => $password, a => \@attr );
310              
311             =cut
312              
313             sub createaccount {
314             my ( $self, %args ) = @_;
315              
316             my @attr = @{ delete $args{a} || [] };
317             my @item = $self->item_from_attr(@attr);
318             $args{a} = \@item if @item;
319              
320             my $e = $self->new_element( "CreateAccountRequest", {%args} );
321             return $self->cl->CreateAccount( $e, $self->context );
322             }
323              
324             =head2 getaccount
325              
326             =head2 getaccountinfo
327              
328             $z->getaccount( name => $acct );
329             $z->getaccountinfo( name => $acct );
330              
331             Arguments:
332              
333             =over 4
334              
335             =item {id|name} => $acct
336              
337             =back
338              
339             =cut
340              
341             sub getaccount {
342             my ( $self, $by, $acct ) = @_;
343              
344             my $s = $self->new_type( "GetAccountSpecifier", { value => $acct } );
345             $s->attr( { "by" => $by } );
346              
347             my $e = $self->new_element( "GetAccountRequest", { account => $s } );
348             return $self->cl->GetAccount( $e, $self->context );
349             }
350              
351             sub getaccountinfo {
352             my ( $self, $by, $acct ) = @_;
353              
354             my $s = $self->new_type( "GetAccountSpecifier", { value => $acct } );
355             $s->attr( { "by" => $by } );
356              
357             my $e = $self->new_element( "GetAccountInfoRequest", { account => $s } );
358             return $self->cl->GetAccountInfo( $e, $self->context );
359             }
360              
361             =head2 getaccountid
362              
363             my $id = $z->getaccountid($acct);
364              
365             =cut
366              
367             # BUG?: return fault if no $id is found? (shouldn't happen but...)
368             sub getaccountid {
369             my ( $self, $acct ) = @_;
370              
371             my $r = $self->getaccountinfo( "name" => $acct );
372             return $r if !$r;
373              
374             # use list context: there should be only one element/id in the list!
375             my ($id) = $self->get_from_a( $r->get_a, "zimbraid" );
376             return $id;
377             }
378              
379             =head2 addaccountalias
380              
381             $z->addaccountalias( name => $acct, alias => $alias );
382              
383             Arguments:
384              
385             =over 4
386              
387             =item {id|name} => $acct
388              
389             =back
390              
391             =cut
392              
393             sub addaccountalias {
394             my ( $self, $by, $acct, %args ) = @_;
395              
396             my $id = $acct;
397             if ( $by ne "id" ) {
398             $id = $self->getaccountid($acct);
399             return $id if ( !$id );
400             }
401              
402             my $e =
403             $self->new_element( "AddAccountAliasRequest", { id => $id, %args } );
404             return $self->cl->AddAccountAlias( $e, $self->context );
405             }
406              
407             =head2 removeaccountalias
408              
409             $z->removeaccountalias( name => $acct, alias => $alias );
410              
411             Arguments:
412              
413             =over 4
414              
415             =item {id|name} => $acct
416              
417             =back
418              
419             =cut
420              
421             sub removeaccountalias {
422             my ( $self, $by, $acct, %args ) = @_;
423              
424             my $id = $acct;
425             if ( $by ne "id" ) {
426             $id = $self->getaccountid($acct);
427             return $id if ( !$id );
428             }
429              
430             my $e =
431             $self->new_element( "RemoveAccountAliasRequest", { id => $id, %args } );
432             return $self->cl->RemoveAccountAlias( $e, $self->context );
433             }
434              
435             =head2 modifyaccount
436              
437             $z->modifyaccount( name => $acct, attr1 => val1, attr2 => val2, ... );
438              
439             Arguments:
440              
441             =over 4
442              
443             =item {id|name} => $acct
444              
445             =back
446              
447             =cut
448              
449             sub modifyaccount {
450             my ( $self, $by, $acct, @attr ) = @_;
451              
452             my $id = $acct;
453             if ( $by ne "id" ) {
454             $id = $self->getaccountid($acct);
455             return $id if ( !$id );
456             }
457              
458             my %args;
459             my @item = $self->item_from_attr(@attr);
460             $args{a} = \@item if @item;
461              
462             my $e = $self->new_element( "ModifyAccountRequest", { id => $id, %args } );
463             return $self->cl->ModifyAccount( $e, $self->context );
464             }
465              
466             =head2 renameaccount
467              
468             $z->renameaccount( name => $acct, $newname );
469              
470             Arguments:
471              
472             =over 4
473              
474             =item {id|name} => $acct
475              
476             =item $newname
477              
478             =back
479              
480             =cut
481              
482             sub renameaccount {
483             my ( $self, $by, $acct, $newname ) = @_;
484              
485             my $id = $acct;
486             if ( $by ne "id" ) {
487             $id = $self->getaccountid($acct);
488             return $id if ( !$id );
489             }
490              
491             return $self->cl->RenameAccount( { id => $id, newName => $newname },
492             $self->context );
493             }
494              
495             =head2 deleteaccount
496              
497             $z->deleteaccount( name => $acct );
498              
499             Arguments:
500              
501             =over 4
502              
503             =item {id|name} => $acct
504              
505             =back
506              
507             =cut
508              
509             sub deleteaccount {
510             my ( $self, $by, $acct ) = @_;
511              
512             my $id = $acct;
513             if ( $by ne "id" ) {
514             $id = $self->getaccountid($acct);
515             return $id if ( !$id );
516             }
517              
518             return $self->cl->DeleteAccount( { id => $id }, $self->context );
519             }
520              
521             =head2 getcos
522              
523             $z->getcos( name => $cos, @attrs );
524              
525             Arguments:
526              
527             =over 4
528              
529             =item {id|name} => $cos
530              
531             =item @attrs (optional)
532              
533             =back
534              
535             =cut
536              
537             sub getcos {
538             my ( $self, $by, $cos, @attr ) = @_;
539              
540             my $gcs = $self->new_type( "GetCosSpecifier", { value => $cos } );
541             $gcs->attr( { "by" => $by } );
542              
543             my $gce = $self->new_element( "GetCosRequest", { cos => $gcs } );
544             $gce->attr( { "attrs" => join( ",", @attr ) } ) if @attr;
545              
546             return $self->cl->GetCos( $gce, $self->context );
547             }
548              
549             =head2 getcosid
550              
551             $z->getcosid($cos);
552              
553             =cut
554              
555             sub getcosid {
556             my ( $self, $cos ) = @_;
557              
558             my $r = $self->getcos( "name" => $cos );
559             return $r if !$r;
560             return $r->get_cos->attr->get_id;
561             }
562              
563             =head2 getserver
564              
565             $z->getserver( name => $svr, %args);
566              
567             Arguments:
568              
569             =over 4
570              
571             =item {id|name} => $svr
572              
573             =item applyConfig => 0|1
574              
575             =item attrs => "attr1,attr2" || [qw(attr1 attr2)]
576              
577             =back
578              
579             =cut
580              
581             sub getserver {
582             my ( $self, $by, $svr, %args ) = @_;
583              
584             my $s = $self->new_type( "GetServerSpecifier", { value => $svr } );
585             $s->attr( { "by" => $by } );
586              
587             my $attrs = delete $args{attrs};
588             my @attr = $attrs ? ( ref($attrs) ? @$attrs : ($attrs) ) : ();
589             my $e = $self->new_element( "GetServerRequest", { server => $s } );
590             $e->attr( { ( @attr ? ( "attrs" => join( ",", @attr ) ) : () ), %args, } );
591             return $self->cl->GetServer( $e, $self->context );
592             }
593              
594             =head2 searchdirectory
595              
596             $z->searchdirectory( query => query, %args );
597              
598             Arguments:
599              
600             =over 4
601              
602             =item query => $query
603              
604             =item limit => $limit
605              
606             =item types => "type1,type2,..."
607              
608             =item ...
609              
610             =back
611              
612             =cut
613              
614             sub searchdirectory {
615             my ( $self, %args ) = @_;
616              
617             my $q = delete $args{query};
618             my $e = $self->new_element( "SearchDirectoryRequest", { query => $q } );
619             $e->attr( \%args ) if %args;
620              
621             return $self->cl->SearchDirectory( $e, $self->context );
622             }
623              
624             =head2 enablearchive
625              
626             $z->enablearchive( {id|name} => $acct, %args );
627              
628             Arguments:
629              
630             =over 4
631              
632             =item {id|name} => $acct # acct-for-which-archiving-is-being-enabled
633              
634             =item create => 0|1
635              
636             =item name => $name
637              
638             =item password => $pass
639              
640             =item cos => $cosname # BUG: only allow name at the moment
641              
642             =item a => \@attr
643              
644             =back
645              
646             =cut
647              
648             # BUG: for performance allow Cos to be an ID or other object?
649             sub enablearchive {
650             my ( $self, $by, $acct, %elem ) = @_;
651              
652             my $gas = $self->new_type( "GetAccountSpecifier", { value => $acct } );
653             $gas->attr( { "by" => $by } );
654              
655             my @item = $self->item_from_attr( @{ delete $elem{a} || [] } );
656             my $create = delete $elem{create};
657             my $acos = delete $elem{cos};
658             if ($acos) {
659             $acos = $self->new_type( "GetCosSpecifier", { value => $acos } );
660             $acos->attr( { "by" => "name" } );
661             }
662              
663             my $ars = $self->new_type(
664             "ArchiveSpecifier",
665             {
666             %elem,
667             ( $acos ? ( cos => $acos ) : () ),
668             ( @item ? ( a => \@item ) : () ),
669             }
670             );
671             $ars->attr( { create => $create } ) if defined($create);
672              
673             my $e = $self->new_element(
674             "EnableArchiveRequest",
675             {
676             account => $gas,
677             archive => $ars,
678             }
679             );
680             return $self->cl->EnableArchive( $e, $self->context );
681             }
682              
683             =head2 disablearchive
684              
685             $z->disablearchive( {id|name} => $acct );
686              
687             Arguments:
688              
689             =over 4
690              
691             =item {id|name} => $acct # acct-for-which-archiving-is-already-enabled
692              
693             =back
694              
695             =cut
696              
697             sub disablearchive {
698             my ( $self, $by, $acct ) = @_;
699              
700             my $s = $self->new_type( "GetAccountSpecifier", { value => $acct } );
701             $s->attr( { "by" => $by } );
702              
703             my $e = $self->new_element( "DisableArchiveRequest", { account => $s } );
704             return $self->cl->DisableArchive( $e, $self->context );
705             }
706              
707             ###
708             # Backup
709              
710             =head2 exportmailbox
711              
712             $z->exportmailbox( name => 'user@dom', dest => 'my.svr.loc', ... );
713              
714             Arguments:
715              
716             =over 4
717              
718             =item name => $acct # account email address
719              
720             =item dest => $server # hostname of target server
721              
722             =item destPort => $port # target port for mailbox import
723              
724             =item switchover => 1|0 # update ldap on/off
725              
726             =item overwrite => 1|0 # replace target mailbox if it exists
727              
728             =back
729              
730             Notes: when switchover is 1, ldap is updated to use the target server as the
731             mailhost for the account; the original host is not longer in use.
732             When switchover is 0, no ldap setting is updated after the move.
733              
734             if overwrite = 1, the target mailbox will be replaced if it exists
735              
736             =cut
737              
738             sub exportmailbox {
739             my ( $self, @attrs ) = @_;
740              
741             my $s = $self->new_type("ExportMailboxSpecifier");
742             $s->attr( {@attrs} );
743              
744             my $e = $self->new_element( "ExportMailboxRequest", { account => $s } );
745             return $self->cl->ExportMailbox( $e, $self->context );
746             }
747              
748             =head2 purgemovedmailbox
749              
750             $z->purgemovedmailbox( 'user@dom' );
751              
752            
753            
754            
755              
756             Following a successful mailbox move to a new server, the mailbox on the old
757             server remains. This allows manually checking the new mailbox to confirm
758             the move worked. Afterwards, PurgeMovedMailboxRequest should be used to remove
759             the old mailbox and reclaim the space.
760              
761             =cut
762              
763             sub purgemovedmailbox {
764             my ( $self, $name ) = @_;
765              
766             my $s = $self->new_type("PurgeMovedMailboxSpecifier");
767             $s->attr( { name => $name } );
768              
769             my $e = $self->new_element( "PurgeMovedMailboxRequest", { mbox => $s } );
770             return $self->cl->PurgeMovedMailbox( $e, $self->context );
771             }
772              
773             =head2 addmessage
774              
775             $z->addmessage( name => $name, folder => $folder, file => $file )
776              
777             POST a message from file $file to /home/$name/$folder using the ZCS
778             REST interface.
779              
780             =cut
781              
782             sub addmessage {
783             my ( $self, %args ) = @_;
784              
785             my $uri = URI->new( $self->cl->get_endpoint );
786              
787             my $name = delete $args{name};
788             my $file = delete $args{file};
789             my $folder = delete $args{folder};
790              
791             # ensure a folder to put the message in is specified
792             $folder = "Inbox" unless ( defined $folder );
793             $folder =~ s,^\s*/,,;
794              
795             $uri->path("/home/$name/$folder");
796             $uri->query( "auth=qp&zauthtoken=" . $self->{_auth}->{authToken} );
797              
798             my $lwp = LWP::UserAgent->new;
799             $lwp->agent( __PACKAGE__ . " $VERSION" );
800              
801             #$lwp->add_handler("request_send", sub { shift->dump; return });
802             #$lwp->add_handler("response_done", sub { shift->dump; return });
803              
804             my $r = eval {
805             $lwp->post(
806             $uri->as_string,
807             Content_Type => 'multipart/form-data',
808             Content => [ file => [$file] ],
809             );
810             };
811             chomp( my $e = $@ );
812              
813             if ( $r and $r->is_success ) {
814             return $r;
815             }
816             else {
817             my $info = "addmessage '$name' file '$file' to '/$folder'";
818             my $err = join( "; ", $r ? $r->status_line : (), $e ? $e : () );
819             return $self->new_fault(
820             faultcode => 'soap:Client',
821             faultstring => "$info: $err",
822             );
823             }
824             }
825              
826             =head2 getdistributionlist
827              
828             $z->getdistributionlist( name => $list );
829              
830             Arguments:
831              
832             =over 4
833              
834             =item {id|name} => $list
835              
836             =back
837              
838             =cut
839              
840             sub getdistributionlist {
841             my ( $self, $by, $acct ) = @_;
842              
843             my $s = $self->new_type( "GetDlSpecifier", { value => $acct } );
844             $s->attr( { "by" => $by } );
845              
846             my $e = $self->new_element( "GetDistributionListRequest", { dl => $s } );
847             return $self->cl->GetDistributionList( $e, $self->context );
848             }
849              
850             =head2 createdistributionlist
851              
852             $z->createtdistributionlist( name => $list );
853              
854             =cut
855              
856             sub createdistributionlist {
857             my ( $self, %args ) = @_;
858              
859             my $e = $self->new_element( "CreateDistributionListRequest", {%args} );
860             return $self->cl->CreateDistributionList( $e, $self->context );
861             }
862              
863             =head2 deletedistributionlist
864              
865             $z->deletedistributionlist( name => $list );
866              
867             Arguments:
868              
869             =over 4
870              
871             =item {id|name} => $list
872              
873             =back
874              
875             =cut
876              
877             sub deletedistributionlist {
878             my ( $self, $by, $list ) = @_;
879              
880             my $id = $list;
881             if ( $by ne "id" ) {
882             my $r = $self->getdistributionlist( $by => $list );
883             return $r if !$r;
884             $id = $r->get_dl->attr->get_id;
885             }
886              
887             return $self->cl->DeleteDistributionList( { id => $id }, $self->context );
888             }
889              
890             =head1 Helper Methods
891              
892             =head2 get_from_a
893              
894             my @vals = $z->get_from_a( $result->get_a, @attrs );
895              
896             Returns an array (arrayref in SCALAR context) of values for attributes
897             (case-insensitively) matched from the list of attribute name(s)
898             specified in @attrs.
899              
900             Returns undef on error.
901              
902             =cut
903              
904             sub get_from_a {
905             my ( $self, $ra, @item ) = @_;
906              
907             return undef unless $ra;
908              
909             my %want = map { lc($_) => $_ } @item;
910             my %data;
911              
912             foreach my $at ( @{ $ra || [] } ) {
913             my $name = lc( $at->attr->get_n );
914             my $want = defined $want{$name} ? $want{$name} : undef;
915             push( @{ $data{$name} }, $at ) if ( !@item || defined($want) );
916             }
917              
918             # got more than 1 item or %data has multiple keys?
919             if ( !@item or @item > 1 or keys(%data) > 1 ) {
920             return wantarray ? %data : \%data;
921             }
922             else {
923             my $key = ( keys %data )[0];
924             my $val = $data{$key};
925             return wantarray ? @$val : $val;
926             }
927             }
928              
929             =head2 item_from_attr
930              
931             my @item = $z->item_from_attr(@attr_name_val_pairs);
932              
933             Returns an array (arrayref in SCALAR context) of ItemAttribute types
934             populated with the name/value pairs specified in @attr_name_val_pairs.
935              
936             Returns undef on error.
937              
938             =cut
939              
940             sub item_from_attr {
941             my ( $self, @attr ) = @_;
942              
943             return undef unless @attr;
944              
945             my @item;
946              
947             while (@attr) {
948             my ( $n, $v ) = ( shift(@attr), shift(@attr) );
949             my $i = $self->new_type( "ItemAttribute", { value => $v } );
950             $i->attr( { "n" => $n } );
951             push( @item, $i );
952             }
953             return wantarray ? @item : \@item;
954             }
955              
956             1;
957              
958             __END__