File Coverage

blib/lib/Net/DAVTalk.pm
Criterion Covered Total %
statement 39 219 17.8
branch 0 88 0.0
condition 0 37 0.0
subroutine 13 29 44.8
pod 16 16 100.0
total 68 389 17.4


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