File Coverage

blib/lib/Net/DAVTalk.pm
Criterion Covered Total %
statement 39 222 17.5
branch 0 90 0.0
condition 0 37 0.0
subroutine 13 29 44.8
pod 16 16 100.0
total 68 394 17.2


line stmt bran cond sub pod time code
1             package Net::DAVTalk;
2              
3 1     1   105749 use strict;
  1         3  
  1         30  
4              
5 1     1   6 use Carp;
  1         2  
  1         50  
6 1     1   785 use DateTime::Format::ISO8601;
  1         652599  
  1         52  
7 1     1   13 use DateTime::TimeZone;
  1         3  
  1         27  
8 1     1   941 use HTTP::Tiny;
  1         50993  
  1         54  
9 1     1   883 use JSON;
  1         11702  
  1         10  
10 1     1   776 use Tie::DataUUID qw{$uuid};
  1         3659  
  1         10  
11 1     1   647 use XML::Spice;
  1         1548  
  1         7  
12 1     1   642 use Net::DAVTalk::XMLParser;
  1         5  
  1         66  
13 1     1   659 use MIME::Base64 qw(encode_base64);
  1         700  
  1         75  
14 1     1   8 use Encode qw(encode_utf8 decode_utf8);
  1         2  
  1         86  
15 1     1   621 use URI::Escape qw(uri_escape uri_unescape);
  1         1541  
  1         77  
16 1     1   816 use URI;
  1         2919  
  1         2919  
17              
18             =head1 NAME
19              
20             Net::DAVTalk - Interface to talk to DAV servers
21              
22             =head1 VERSION
23              
24             Version 0.20
25              
26             =cut
27              
28             our $VERSION = '0.20';
29              
30             =head1 SYNOPSIS
31              
32             Net::DAVTalk is was originally designed as a service module for Net::CalDAVTalk
33             and Net::DAVTalk, abstracting the process of connecting to a DAV server and
34             parsing the XML responses.
35              
36             Example:
37              
38             use Net::DAVTalk;
39             use XML::Spice;
40              
41             my $davtalk = Net::DAVTalk->new(
42             url => "https://dav.example.com/",
43             user => "foo\@example.com",
44             password => "letmein",
45             headers => { Cookie => "123", Referer => "456" },
46             );
47              
48             $davtalk->Request(
49             'MKCALENDAR',
50             "$calendarId/",
51             x('C:mkcalendar', $Self->NS(),
52             x('D:set',
53             x('D:prop', @Properties),
54             ),
55             ),
56             );
57              
58             $davtalk->Request(
59             'DELETE',
60             "$calendarId/",
61             );
62              
63             =head1 SUBROUTINES/METHODS
64              
65             =head2 $class->new(%Options)
66              
67             Options:
68              
69             url: either full https?:// url, or relative base path on the
70             server to the DAV endpoint
71              
72             host, scheme and port: alternative to using full URL.
73             If URL doesn't start with https?:// then these will be used to
74             construct the endpoint URI.
75              
76             expandurl and wellknown: if these are set, then the wellknown
77             name (caldav and carddav are both defined) will be used to
78             resolve /.well-known/$wellknown to find the current-user-principal
79             URI, and then THAT will be resovlved to find the $wellknown-home-set
80             URI, which will be used as the URL for all further actions on
81             this object.
82              
83             user and password: if these are set, perform basic authentication.
84             user and access_token: if these are set, perform Bearer (OAUTH2)
85             authentication.
86              
87             headers: a hashref of additional headers to add to every request
88              
89             =cut
90              
91             # General methods
92              
93             sub new {
94 0     0 1   my ($Class, %Params) = @_;
95              
96 0 0         unless ($Params{url}) {
97 0           confess "URL not supplied";
98             }
99              
100             # Assume url points to xyz-home-set, otherwise expand the url
101 0 0         if (delete $Params{expandurl}) {
102             # Locating Services for CalDAV and CardDAV (RFC6764)
103 0           my $PrincipalURL = $Class->GetCurrentUserPrincipal(%Params);
104 0           $Params{principal} = $PrincipalURL;
105              
106 0           my $HomeSet = $Class->GetHomeSet(
107             %Params,
108             url => $PrincipalURL,
109             );
110              
111 0           $Params{url} = $HomeSet;
112             }
113              
114 0   0       my $Self = bless \%Params, ref($Class) || $Class;
115 0           $Self->SetURL($Params{url});
116 0           $Self->SetPrincipalURL($Params{principal});
117 0           $Self->ns(D => 'DAV:');
118              
119 0           return $Self;
120             }
121              
122             =head2 my $ua = $Self->ua();
123             =head2 $Self->ua($setua);
124              
125             Get or set the useragent (HTTP::Tiny or compatible) that will be used to make
126             the requests:
127              
128             e.g.
129              
130             my $ua = $Self->ua();
131              
132             $Self->ua(HTTP::Tiny->new(agent => "MyAgent/1.0", timeout => 5));
133              
134             =cut
135              
136             sub ua {
137 0     0 1   my $Self = shift;
138 0 0         if (@_) {
139 0           $Self->{ua} = shift;
140             }
141             else {
142 0   0       $Self->{ua} ||= HTTP::Tiny->new(
143             agent => "Net-DAVTalk/0.01",
144             );
145             }
146 0           return $Self->{ua};
147             }
148              
149             =head2 $Self->SetURL($url)
150              
151             Change the endpoint URL for an existing connection.
152              
153             =cut
154              
155             sub SetURL {
156 0     0 1   my ($Self, $URL) = @_;
157              
158 0           $URL =~ s{/$}{}; # remove any trailing slash
159              
160 0 0         if ($URL =~ m{^https?://}) {
161 0           my ($HTTPS, $Hostname, $Port, $BasePath)
162             = $URL =~ m{^http(s)?://([^/:]+)(?::(\d+))?(.*)?};
163              
164 0 0         unless ($Hostname) {
165 0           confess "Invalid hostname in '$URL'";
166             }
167              
168 0 0         $Self->{scheme} = $HTTPS ? 'https' : 'http';
169 0           $Self->{host} = $Hostname;
170 0   0       $Self->{port} = ($Port || ($HTTPS ? 443 : 80));
171 0           $Self->{basepath} = $BasePath;
172             }
173             else {
174 0           $Self->{basepath} = $URL;
175             }
176              
177 0           $Self->{url} = "$Self->{scheme}://$Self->{host}:$Self->{port}$Self->{basepath}";
178              
179 0           return $Self->{url};
180             }
181              
182             =head2 $Self->SetPrincipalURL($url)
183              
184             Set the URL to the DAV Principal
185              
186             =cut
187              
188             sub SetPrincipalURL {
189 0     0 1   my ($Self, $PrincipalURL) = @_;
190              
191 0           return $Self->{principal} = $PrincipalURL;
192             }
193              
194             =head2 $Self->fullpath($shortpath)
195              
196             Convert from a relative path to a full path:
197              
198             e.g
199             my $path = $Dav->fullpath('Default');
200             ## /dav/calendars/user/foo/Default
201              
202             NOTE: a you can pass a non-relative full path (leading /)
203             to this function and it will be returned unchanged.
204              
205             =cut
206              
207             sub fullpath {
208 0     0 1   my $Self = shift;
209 0           my $path = shift;
210 0           my $basepath = $Self->{basepath};
211 0 0         return $path if $path =~ m{^/};
212 0           return "$basepath/$path";
213             }
214              
215             =head2 $Self->shortpath($fullpath)
216              
217             Convert from a full path to a relative path
218              
219             e.g
220             my $path = $Dav->fullpath('/dav/calendars/user/foo/Default');
221             ## Default
222              
223             NOTE: if the full path is outside the basepath of the object, it
224             will be unchanged.
225              
226             my $path = $Dav->fullpath('/dav/calendars/user/bar/Default');
227             ## /dav/calendars/user/bar/Default
228              
229             =cut
230              
231             sub shortpath {
232 0     0 1   my $Self = shift;
233 0           my $origpath = shift;
234 0           my $basepath = $Self->{basepath};
235 0           my $path = $origpath;
236 0           $path =~ s{^$basepath/?}{};
237 0 0         return ($path eq '' ? $origpath : $path);
238             }
239              
240             =head2 $Self->Request($method, $path, $content, %headers)
241              
242             The whole point of the module! Perform a DAV request against the
243             endpoint, returning the response as a parsed hash.
244              
245             method: http method, i.e. GET, PROPFIND, MKCOL, DELETE, etc
246              
247             path: relative to base url. With a leading slash, relative to
248             server root, i.e. "Default/", "/dav/calendars/user/foo/Default".
249              
250             content: if the method takes a body, raw bytes to send
251              
252             headers: additional headers to add to request, i.e (Depth => 1)
253              
254             =cut
255              
256             sub Request {
257 0     0 1   my ($Self, $Method, $Path, $Content, %Headers) = @_;
258              
259             # setup request {{{
260              
261 0 0         $Content = '' unless defined $Content;
262 0           my $Bytes = encode_utf8($Content);
263              
264 0           my $ua = $Self->ua();
265              
266 0   0       $Headers{'Content-Type'} //= 'application/xml; charset=utf-8';
267              
268 0 0         if ($Self->{user}) {
269 0           $Headers{'Authorization'} = $Self->auth_header();
270             }
271              
272 0 0         if ($Self->{headers}) {
273 0           $Headers{$_} = $Self->{headers}->{$_} for ( keys %{ $Self->{headers} } );
  0            
274             }
275              
276             # XXX - Accept-Encoding for gzip, etc?
277              
278             # }}}
279              
280             # send request {{{
281              
282 0           my $URI = $Self->request_url($Path);
283              
284 0           my $Response = $ua->request($Method, $URI, {
285             headers => \%Headers,
286             content => $Bytes,
287             });
288              
289 0 0 0       if ($Response->{status} == '599' and $Response->{content} =~ m/timed out/i) {
290 0           confess "Error with $Method for $URI (504, Gateway Timeout)";
291             }
292              
293 0           my $count = 0;
294 0   0       while ($Response->{status} =~ m{^30[1278]} and (++$count < 10)) {
295 0           my $location = URI->new_abs($Response->{headers}{location}, $URI);
296 0 0         if ($ENV{DEBUGDAV}) {
297 0           warn "******** REDIRECT ($count) $Response->{status} to $location\n";
298             }
299              
300 0           $Response = $ua->request($Method, $location, {
301             headers => \%Headers,
302             content => $Bytes,
303             });
304              
305 0 0 0       if ($Response->{status} == '599' and $Response->{content} =~ m/timed out/i) {
306 0           confess "Error with $Method for $location (504, Gateway Timeout)";
307             }
308             }
309              
310             # one is enough
311              
312 0   0       my $ResponseContent = $Response->{content} || '';
313              
314 0 0         if ($ENV{DEBUGDAV}) {
315 0           warn "<<<<<<<< $Method $URI HTTP/1.1\n$Bytes\n" .
316             ">>>>>>>> $Response->{protocol} $Response->{status} $Response->{reason}\n$ResponseContent\n" .
317             "========\n\n";
318             }
319              
320 0 0 0       if ($Method eq 'REPORT' && $Response->{status} == 403) {
321             # maybe invalid sync token, need to return that fact
322 0           my $Xml = xmlToHash($ResponseContent);
323 0 0         if (exists $Xml->{"{DAV:}valid-sync-token"}) {
324             return {
325 0           error => "valid-sync-token",
326             };
327             }
328             }
329              
330 0 0         unless ($Response->{success}) {
331 0           confess("ERROR WITH REQUEST\n" .
332             "<<<<<<<< $Method $URI HTTP/1.1\n$Bytes\n" .
333             ">>>>>>>> $Response->{protocol} $Response->{status} $Response->{reason}\n$ResponseContent\n" .
334             "========\n\n");
335             }
336              
337 0 0 0       if ((grep { $Method eq $_ } qw{GET DELETE}) or ($Response->{status} != 207) or (not $ResponseContent)) {
  0   0        
338 0           return { content => $ResponseContent };
339             }
340              
341             # }}}
342              
343             # parse XML response {{{
344 0           my $Xml = xmlToHash($ResponseContent);
345              
346             # Normalise XML
347              
348 0 0         if (exists($Xml->{"{DAV:}response"})) {
349 0 0         if (ref($Xml->{"{DAV:}response"}) ne 'ARRAY') {
350 0           $Xml->{"{DAV:}response"} = [ $Xml->{"{DAV:}response"} ];
351             }
352              
353 0           foreach my $Response (@{$Xml->{"{DAV:}response"}}) {
  0            
354 0 0         if (exists($Response->{"{DAV:}propstat"})) {
355 0 0         unless (ref($Response->{"{DAV:}propstat"}) eq 'ARRAY') {
356 0           $Response->{"{DAV:}propstat"} = [$Response->{"{DAV:}propstat"}];
357             }
358             }
359             }
360             }
361              
362 0           return $Xml;
363              
364             # }}}
365             }
366              
367             =head2 $Self->GetProps($Path, @Props)
368              
369             perform a propfind on a particular path and get the properties back
370              
371             =cut
372              
373             sub GetProps {
374 0     0 1   my ($Self, $Path, @Props) = @_;
375 0           my @res = $Self->GetPropsArray($Path, @Props);
376 0 0         return wantarray ? map { $_->[0] } @res : $res[0][0];
  0            
377             }
378              
379             =head2 $Self->GetPropsArray($Path, @Props)
380              
381             perform a propfind on a particular path and get the properties back
382             as an array of one or more items
383              
384             =cut
385              
386             sub GetPropsArray {
387 0     0 1   my ($Self, $Path, @Props) = @_;
388              
389             # Fetch one or more properties.
390             # Use [ 'prop', 'sub', 'item' ] to dig into result structure
391              
392 0           my $NS_D = $Self->ns('D');
393              
394             my $Response = $Self->Request(
395             'PROPFIND',
396             $Path,
397             x('D:propfind', $Self->NS(),
398             x('D:prop',
399 0 0         map { ref $_ ? x($_->[0]): x($_) } @Props,
  0            
400             ),
401             ),
402             Depth => 0,
403             );
404              
405 0           my @Results;
406 0 0         foreach my $Response (@{$Response->{"{$NS_D}response"} || []}) {
  0            
407 0 0         foreach my $Propstat (@{$Response->{"{$NS_D}propstat"} || []}) {
  0            
408 0   0       my $PropData = $Propstat->{"{$NS_D}prop"} || next;
409 0           for my $Prop (@Props) {
410 0           my @Values = ($PropData);
411              
412             # Array ref means we need to git through structure
413 0 0         foreach my $Key (ref $Prop ? @$Prop : $Prop) {
414 0           my @New;
415 0           foreach my $Result (@Values) {
416 0 0         if ($Key =~ m/:/) {
417 0           my ($N, $P) = split /:/, $Key;
418 0           my $NS = $Self->ns($N);
419 0           $Result = $Result->{"{$NS}$P"};
420             } else {
421 0           $Result = $Result->{$Key};
422             }
423 0 0         if (ref($Result) eq 'ARRAY') {
    0          
424 0           push @New, @$Result;
425             }
426             elsif (defined $Result) {
427 0           push @New, $Result;
428             }
429             }
430 0           @Values = @New;
431             }
432              
433 0           push @Results, [ map { $_->{content} } @Values ];
  0            
434             }
435             }
436             }
437              
438 0 0         return wantarray ? @Results : $Results[0];
439             }
440              
441             =head2 $Self->GetCurrentUserPrincipal()
442             =head2 $class->GetCurrentUserPrincipal(%Args)
443              
444             Can be called with the same args as new() as a class method, or
445             on an existing object. Either way it will use the .well-known
446             URI to find the path to the current-user-principal.
447              
448             Returns a string with the path.
449              
450             =cut
451              
452             sub GetCurrentUserPrincipal {
453 0     0 1   my ($Class, %Args) = @_;
454              
455 0 0         if (ref $Class) {
456 0           %Args = %{$Class};
  0            
457 0           $Class = ref $Class;
458             }
459              
460 0   0       my $OriginalURL = $Args{url} || '';
461 0           my $Self = $Class->new(%Args);
462 0           my $NS_D = $Self->ns('D');
463 0           my @BasePath = split '/', $Self->{basepath};
464              
465 0 0         @BasePath = ('', ".well-known/$Args{wellknown}") unless @BasePath;
466              
467 0           PRINCIPAL: while(1) {
468 0           $Self->SetURL(join '/', @BasePath);
469              
470 0 0         if (my $Principal = $Self->GetProps('', [ 'D:current-user-principal', 'D:href' ])) {
471 0           $Self->SetURL(uri_unescape($Principal));
472 0           return $Self->{url};
473             }
474              
475 0           pop @BasePath;
476 0 0         last unless @BasePath;
477             }
478              
479 0           croak "Error finding current user principal at '$OriginalURL'";
480             }
481              
482             =head2 $Self->GetHomeSet
483             =head2 $class->GetHomeSet(%Args)
484              
485             Can be called with the same args as new() as a class method, or
486             on an existing object. Either way it assumes that the created
487             object has a 'url' parameter pointing at the current user principal
488             URL (see GetCurrentUserPrincipal above)
489              
490             Returns a string with the path to the home set.
491              
492             =cut
493              
494             sub GetHomeSet {
495 0     0 1   my ($Class, %Args) = @_;
496              
497 0 0         if (ref $Class) {
498 0           %Args = %{$Class};
  0            
499 0           $Class = ref $Class;
500             }
501              
502 0   0       my $OriginalURL = $Args{url} || '';
503 0           my $Self = $Class->new(%Args);
504 0           my $NS_D = $Self->ns('D');
505 0           my $NS_HS = $Self->ns($Args{homesetns});
506 0           my $HomeSet = $Args{homeset};
507              
508 0 0         if (my $Homeset = $Self->GetProps('', [ "$Args{homesetns}:$HomeSet", 'D:href' ])) {
509 0           $Self->SetURL($Homeset);
510 0           return $Self->{url};
511             }
512              
513 0           croak "Error finding $HomeSet home set at '$OriginalURL'";
514             }
515              
516             =head2 $Self->genuuid()
517              
518             Helper to generate a uuid string. Returns a UUID, e.g.
519              
520             my $uuid = $DAVTalk->genuuid(); # 9b9d68af-ad13-46b8-b7ab-30ab70da14ac
521              
522             =cut
523              
524             sub genuuid {
525 0     0 1   my $Self = shift;
526 0           return "$uuid";
527             }
528              
529             =head2 $Self->auth_header()
530              
531             Generate the authentication header to use on requests:
532              
533             e.g:
534              
535             $Headers{'Authorization'} = $Self->auth_header();
536              
537             =cut
538              
539             sub auth_header {
540 0     0 1   my $Self = shift;
541              
542 0 0         if ($Self->{password}) {
543 0           return 'Basic ' . encode_base64("$Self->{user}:$Self->{password}", '');
544             }
545              
546 0 0         if ($Self->{access_token}) {
547 0           return "Bearer $Self->{access_token}";
548             }
549              
550 0           croak "Need a method to authenticate user (password or access_token)";
551             }
552              
553             =head2 $Self->request_url()
554              
555             Generate the authentication header to use on requests:
556              
557             e.g:
558              
559             $Headers{'Authorization'} = $Self->auth_header();
560              
561             =cut
562              
563             sub request_url {
564 0     0 1   my $Self = shift;
565 0           my $Path = shift;
566              
567 0           my $URL = $Self->{url};
568              
569             # If a reference, assume absolute
570 0 0         if (ref $Path) {
571 0           ($URL, $Path) = $$Path =~ m{(^https?://[^/]+)(.*)$};
572             }
573              
574 0 0         if ($Path) {
575 0           $Path = join "/", map { uri_escape $_ } split m{/}, $Path, -1;
  0            
576 0 0         if ($Path =~ m{^/}) {
577 0           $URL =~ s{(^https?://[^/]+)(.*)}{$1$Path};
578             }
579             else {
580 0           $URL =~ s{/$}{};
581 0           $URL .= "/$Path";
582             }
583             }
584              
585 0           return $URL;
586             }
587              
588             =head2 $Self->NS()
589              
590             Returns a hashref of the 'xmlns:shortname' => 'full namespace' items for use in XML::Spice body generation, e.g.
591              
592             $DAVTalk->Request(
593             'MKCALENDAR',
594             "$calendarId/",
595             x('C:mkcalendar', $Self->NS(),
596             x('D:set',
597             x('D:prop', @Properties),
598             ),
599             ),
600             );
601              
602             # { 'xmlns:C' => 'urn:ietf:params:xml:ns:caldav', 'xmlns:D' => 'DAV:' }
603              
604             =cut
605              
606             sub NS {
607 0     0 1   my $Self = shift;
608              
609             return {
610 0           map { ( "xmlns:$_" => $Self->ns($_) ) }
  0            
611             $Self->ns(),
612             };
613             }
614              
615              
616             =head2 $Self->ns($key, $value)
617              
618             Get or set namespace aliases, e.g
619              
620             $Self->ns(C => 'urn:ietf:params:xml:ns:caldav');
621             my $NS_C = $Self->ns('C'); # urn:ietf:params:xml:ns:caldav
622              
623             =cut
624              
625             sub ns {
626 0     0 1   my $Self = shift;
627              
628             # case: keys
629 0 0         return keys %{$Self->{ns}} unless @_;
  0            
630              
631 0           my $key = shift;
632             # case read one
633 0 0         return $Self->{ns}{$key} unless @_;
634              
635             # case write
636 0           my $prev = $Self->{ns}{$key};
637 0           $Self->{ns}{$key} = shift;
638 0           return $prev;
639             }
640              
641             =head2 function2
642              
643             =cut
644              
645             =head1 AUTHOR
646              
647             Bron Gondwana, C<< <brong at cpan.org> >>
648              
649             =head1 BUGS
650              
651             Please report any bugs or feature requests to C<bug-net-davtalk at rt.cpan.org>, or through
652             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Net-DAVTalk>. I will be notified, and then you'll
653             automatically be notified of progress on your bug as I make changes.
654              
655              
656              
657              
658             =head1 SUPPORT
659              
660             You can find documentation for this module with the perldoc command.
661              
662             perldoc Net::DAVTalk
663              
664              
665             You can also look for information at:
666              
667             =over 4
668              
669             =item * RT: CPAN's request tracker (report bugs here)
670              
671             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Net-DAVTalk>
672              
673             =item * AnnoCPAN: Annotated CPAN documentation
674              
675             L<http://annocpan.org/dist/Net-DAVTalk>
676              
677             =item * CPAN Ratings
678              
679             L<http://cpanratings.perl.org/d/Net-DAVTalk>
680              
681             =item * Search CPAN
682              
683             L<http://search.cpan.org/dist/Net-DAVTalk/>
684              
685             =back
686              
687              
688             =head1 ACKNOWLEDGEMENTS
689              
690              
691             =head1 LICENSE AND COPYRIGHT
692              
693             Copyright 2015 FastMail Pty. Ltd.
694              
695             This program is free software; you can redistribute it and/or modify it
696             under the terms of the the Artistic License (2.0). You may obtain a
697             copy of the full license at:
698              
699             L<http://www.perlfoundation.org/artistic_license_2_0>
700              
701             Any use, modification, and distribution of the Standard or Modified
702             Versions is governed by this Artistic License. By using, modifying or
703             distributing the Package, you accept this license. Do not use, modify,
704             or distribute the Package, if you do not accept this license.
705              
706             If your Modified Version has been derived from a Modified Version made
707             by someone other than you, you are nevertheless required to ensure that
708             your Modified Version complies with the requirements of this license.
709              
710             This license does not grant you the right to use any trademark, service
711             mark, tradename, or logo of the Copyright Holder.
712              
713             This license includes the non-exclusive, worldwide, free-of-charge
714             patent license to make, have made, use, offer to sell, sell, import and
715             otherwise transfer the Package with respect to any patent claims
716             licensable by the Copyright Holder that are necessarily infringed by the
717             Package. If you institute patent litigation (including a cross-claim or
718             counterclaim) against any party alleging that the Package constitutes
719             direct or contributory patent infringement, then this Artistic License
720             to you shall terminate on the date that such litigation is filed.
721              
722             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
723             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
724             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
725             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
726             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
727             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
728             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
729             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
730              
731              
732             =cut
733              
734             1; # End of Net::DAVTalk