#!/usr/bin/perl -w # iCal to WCAP Gateway # HTTP server that implements GET and PUT for both subscribing to and # publishing calendars using Apple iCal and Sun ONE (Java System, iPlanet) # Calendar Server. # # Copyright (C) 2004-2006, John "Rowan" Littell ######### # modules use strict; use Getopt::Std; use HTTP::Daemon; use HTTP::Status; use MIME::Base64; use LWP::UserAgent; use HTTP::Request; use HTTP::Request::Common; use HTTP::Response; use HTTP::Date; use POSIX qw(strftime); use Sys::Syslog qw(:DEFAULT setlogsock); ######### # globals my $CALSERVER = ""; my $REALM = "icald wcap gateway"; my $LOCALADDR = ""; my $LOCALPORT = 7080; my $SSL_PORT = 7443; my $DEBUG = 0; my $SERVER_ID = "icald"; my $SERVER_VERSION = "1.5"; my $PIDFILE = "/var/run/icald.pid"; my $LOGFILE = "/var/log/icald/access.log"; ############ # prototypes sub sighup_handler ($); sub access_log ($$$$$); sub handle_put ($$); sub handle_get ($$); sub handle_unknown ($$); sub wcap_command ($;@); sub wcap_command_post ($$;@); sub wcap_login ($$); sub wcap_logout ($); sub wcap_get_calprops ($$); sub wcap_deletecomponents_by_range ($$); sub wcap_import ($$$); sub wcap_export ($$); sub wcap_fetchcomponents_by_range ($$); ############## # main program MAIN: { my (%opts); getopts ('de:r:a:u:s:p:l:', \%opts); ($opts{'d'}) && ($DEBUG = 1); ($opts{'s'}) && ($CALSERVER = $opts{'s'}); ($opts{'a'}) && ($LOCALADDR = $opts{'a'}); ($opts{'p'}) && ($LOCALPORT = $opts{'p'}); ($opts{'r'}) && ($REALM = $opts{'r'}); ($opts{'l'}) && ($LOGFILE = $opts{'l'}); ($opts{'e'}) && ($SSL_PORT = $opts{'e'}); $0 = $SERVER_ID; if ($CALSERVER eq "") { die "Please specify a calendar server.\n"; } if (!$DEBUG) { my $pid = fork(); if (!defined $pid) { # fork error die "fork: $!\n"; } elsif ($pid) { # parent, record PID and then close open (P, ">$PIDFILE"); print P $pid; close (P); exit; } else { # close open file descriptors close STDIN; close STDOUT; close STDERR; } # tell system we don't care about child procs $SIG{'CHLD'} = 'IGNORE'; } # open syslog setlogsock ('unix'); openlog ($SERVER_ID, 'pid', 'user'); # create the HTTP daemon my $daemon = HTTP::Daemon->new ( LocalAddr => $LOCALADDR, LocalPort => $LOCALPORT, Listen => 10, Reuse => 1 ); if (!defined $daemon) { syslog('err', "could not bind to address $LOCALADDR:$LOCALPORT: $!"); closelog(); exit; } # setuid after binding to the port, if requested if ($opts{'u'}) { if ($< == 0) { my ($uid, $gid) = (getpwnam $opts{'u'})[2,3]; $< = $> = $uid; $( = $) = $gid; } } # open access logfile if (!open (LOG, ">>$LOGFILE")) { syslog('err', "could not open access log file $LOGFILE"); closelog(); exit; } else { select LOG; $| = 1; } # set signal handler for SIGHUP (reopen log file) $SIG{'HUP'} = 'sighup_handler'; # enter main accept loop while (1) { my $conn = $daemon->accept; if (!$conn) { next; } # in normal mode, we spawn off a child process to handle the request if (!$DEBUG && fork()) { # parent $conn->close; undef ($conn); } else { # child # handle requests while (my $req = $conn->get_request) { if (!defined $req) { next; } $conn->autoflush; # currently we only deal with GET and PUT # GET = iCal subscription # PUT = iCal publish my $method = $req->method; if ($method eq 'PUT') { handle_put ($conn, $req); } elsif ($method eq 'GET') { handle_get ($conn, $req); } else { # unknown method, send 501 not implemented # iCal will send a DELETE for a published # calendar to its old location if you change # the publish location. It doesn't care what # the return is, though, so a 501 is perfectly fine. handle_unknown ($conn, $req); } } # shutdown the connection $conn->close; undef($conn); # in normal mode, we've fork()ed, so exit when we're done if (!$DEBUG) { exit; } } } } ################## # signal handler for SIGHUP # close and re-open log file sub sighup_handler ($) { my ($sig) = @_; syslog('info', "SIG$sig - reopening access log $LOGFILE"); close (LOG); if (!open (LOG, ">>$LOGFILE")) { syslog('err', "could not open access log file $LOGFILE"); closelog(); exit; } else { select LOG; $| = 1; } } sub access_log ($$$$$) { my ($client, $logname, $username, $request, $resp) = @_; ($username eq "") ? $username = "-" : $username = $username; my $date = strftime ("%d/%b/%Y:%T %z", localtime(time())); my $code = $resp->code(); my $size = length ($resp->as_string()); my $log = "$client $logname $username [$date] \"$request\" $code $size"; print LOG "$log\n"; } #################### # handle_put # iCal publish # requires: # $conn -- client connection opject # $req -- client request object sub handle_put ($$) { my ($conn, $req) = @_; my ($url, $resp, $h, $username, $password); # need authorization my $auth = $req->header("Authorization"); if ($auth ne "") { my ($type, $cred) = (split /\s+/, $auth); my $decode = decode_base64($cred); ($username, $password) = (split ':', $decode); } else { $username = $password = ""; } my $id = wcap_login ($username, $password); if (!defined $id || $id eq "0") { $h = HTTP::Headers->new; $h->header('Connection' => 'close'); $h->header('WWW-Authenticate' => "Basic realm=\"$REALM\""); my $content = ' 401 Authorization Required

Authorization Required

This server could not verify that you are authorized to access the document requested. Either you supplied the wrong credentials (e.g., bad password), or your browser doesn\'t understand how to supply the credentials required.

'; $resp = HTTP::Response->new ("401", "Authorization Required", $h, $content); } else { # In all cases, $username from the Authenticate header is used # for login -- this may be different from the USERNAME part of # the URI, which would indicate someone modifying another's calendar. # iCal attaches .ics to the calendar name that it publishes; # we strip it off. # URIs # /USERNAME/USERNAME -> calid=USERNAME # /USERNAME/CALENDAR -> calid=USERNAME:CALENDAR # /CALENDAR -> calid=CALENDAR my $calname = $req->uri; $calname =~ s/^https?:\/\/[^\/]+//; $url = $calname; if ($calname =~ /^\/([^\/]+)\/([-_\+\d\w]+)(\.ics)?$/) { my ($tuser, $tname) = ($1, $2); if ($tname eq $tuser) { $calname = "$tuser"; } else { $calname = "$tuser:$tname"; } } elsif ($calname =~ /([^\/]+)(\.ics)?$/) { $calname = $1; } # check for existence my ($errno, $content) = wcap_get_calprops ($id, $calname); if ($errno ne "0") { $h = HTTP::Headers->new; $h->header('Connection' => 'close'); $resp = HTTP::Response->new("404", "Calendar $calname not found", $h); } else { # calendar exists, now delete all entries and upload new one ($errno, $content) = wcap_deletecomponents_by_range ($id, $calname); wcap_import ($id, $calname, $req->content); $h = HTTP::Headers->new; $h->header('Connection' => 'close'); $resp = HTTP::Response->new("200", "Ok", $h); } wcap_logout ($id); } access_log ($conn->peerhost, "-", $username, "PUT ".$url, $resp); $conn->send_response($resp); } #################### # handle_get # iCal subscribe # requires: # $conn -- client connection opject # $req -- client request object sub handle_get ($$) { my ($conn, $req) = @_; my ($url, $username, $password); my ($resp, $h, $need_auth, $need_ssl, $ssl_uri); # if we're given an auth header, use it my $id = "0"; my $auth = $req->header("Authorization"); if ($auth ne "") { my ($type, $cred) = (split /\s+/, $auth); my $decode = decode_base64($cred); ($username, $password) = (split ':', $decode); $id = wcap_login ($username, $password); } else { $username = $password = ""; } # construct the calendar name # URIs: # /CALENDAR -> calid=CALENDAR (including CALENDAR == $username) # /USERNAME/CALENDAR -> calid=USERNAME:CALENDAR # /login/CALENDAR -> calid=CALENDAR, requires AUTH # /login/USERNAME/CALENDAR -> calid=USERNAME:CALENDAR, requires AUTH $need_auth = 0; $need_ssl = 0; my $calname = $req->uri; my $uri = $calname; $calname =~ s/^https?:\/\/[^\/]+//; $url = $calname; if ($calname =~ /^\/login\/([^\/]+)\/([^\/]+)$/) { # /login/USERNAME/CALENDAR $calname = "$1:$2"; $need_auth = 1; } elsif ($calname =~ /^\/login\/([^\/]+)$/) { # /login/CALENDAR $calname = $1; $need_auth = 1; } elsif ($calname =~ /^\/loginssl\/([^\/]+)\/([^\/]+)$/) { # /loginssl/USERNAME/CALENDAR $calname = "$1:$2"; $need_auth = 1; if ($uri !~ /^https/) { $need_ssl = 1; my ($hostname) = (split (/\/+/, $uri))[1]; $hostname =~ s/:\d+$//; $ssl_uri = "https://$hostname:$SSL_PORT$url"; $ssl_uri =~ s/loginssl/login/; } } elsif ($calname =~ /^\/loginssl\/([^\/]+)$/) { # /loginssl/CALENDAR $calname = $1; $need_auth = 1; if ($uri !~ /^https/) { $need_ssl = 1; my ($hostname) = (split (/\/+/, $uri))[1]; $hostname =~ s/:\d+$//; $ssl_uri = "https://$hostname:$SSL_PORT$url"; $ssl_uri =~ s/loginssl/login/; } } elsif ($calname =~ /^\/([^\/]+)\/([^\/]+)$/) { # /USERNAME/CALENDAR $calname = "$1:$2"; $need_auth = 0; } elsif ($calname =~ /([^\/]+)$/) { # /CALENDAR $calname = $1; $need_auth = 0; } # if need ssl, return a redirect if ($need_ssl) { $h = HTTP::Headers->new; $h->header('Connection' => 'close'); $h->header('Content-Type' => 'text/html; charset=iso-8859-1'); $h->header('Location' => $ssl_uri); my $content = ' 301 Moved Permanently

Moved Permanently

The document has moved here.


'; $resp = HTTP::Response->new ("301", "Moved Permanently", $h, $content); access_log ($conn->peerhost, "-", $username, "GET ".$url, $resp); $conn->send_response($resp); return; } # find the calendar my ($errno, $content) = wcap_get_calprops ($id, $calname); if ((!defined $auth || $auth eq "") && ($errno eq "28" || $need_auth)) { # need authorization $h = HTTP::Headers->new; $h->header('Connection' => 'close'); $h->header('Content-Type' => 'text/html; charset=iso-8859-1'); $h->header('WWW-Authenticate' => "Basic realm=\"$REALM\""); my $content = ' 401 Authorization Required

Authorization Required

This server could not verify that you are authorized to access the document requested. Either you supplied the wrong credentials (e.g., bad password), or your browser doesn\'t understand how to supply the credentials required.

'; $resp = HTTP::Response->new ("401", "Authorization Required", $h, $content); } elsif ($errno eq "29") { # nonexistent calendar $h = HTTP::Headers->new; $h->header('Connection' => 'close'); $resp = HTTP::Response->new("404", "Calendar not found", $h); } elsif ($errno eq "0") { # found calendar #($errno, $content) = wcap_fetchcomponents_by_range($id, $calname); ($errno, $content) = wcap_export($id, $calname); # munge content to take out stuff that iCal/iSync doesn't like my $inorg = 0; my $munged_content = ''; foreach my $line (split(/\r\n/, $content)) { if ($inorg && $line !~ /^\s+/) { $inorg = 0; } if ($line =~ /^ORGANIZER/) { $inorg = 1; } if (!$inorg) { if ($line !~ /^X-NSCP-/ && $line !~ /^ ;X-NSCP-/) { $munged_content .= "$line\r\n"; } } } $h = HTTP::Headers->new; $h->header('Connection' => 'close'); $h->header('Content-Type' => 'text/calendar'); $h->header('Content-Disposition' => "attachment; filename=\"$calname.ics\""); $resp = HTTP::Response->new("200", "Ok", $h, $munged_content); } if ($id ne "0") { wcap_logout ($id); } access_log ($conn->peerhost, "-", $username, "GET ".$url, $resp); $conn->send_response($resp); } #################### # handle_unknown # for any unknown HTTP methods # sends a 501 Method Not Implemented to the client # requires: # $conn -- client connection opject # $req -- client request object sub handle_unknown ($$) { my ($conn, $req) = @_; my $h = HTTP::Headers->new; $h->header('Connection' => 'close'); $h->header('Content-Type' => 'text/html; charset=iso-8859-1'); my $content = ' 501 Method Not Implemented

Method Not Implemented

Invalid method request

'; my $resp = HTTP::Response->new("501", "Method Not Implemented", $h, $content); $conn->send_response($resp); } ################################################################ # WCAP INTERFACE ################################################################ ################ # standard wcap commands (GET method) # arguments: # $command -- the command name # @args -- a list of arguments to send to the command (optional) # returns: # errno and content in an array context # content in scalar context sub wcap_command ($;@) { my ($command, @args) = @_; my ($argstring, $url); if (@args) { $argstring = join ('&', @args); } if ($argstring ne "") { $url = "http://$CALSERVER/$command.wcap?$argstring"; } else { $url = "http://$CALSERVER/$command.wcap"; } my $request = HTTP::Request->new (GET => $url); my $browser = LWP::UserAgent->new; $browser->agent("$SERVER_ID/$SERVER_VERSION"); my $response = $browser->simple_request($request); if ($DEBUG) { open (T, ">>/tmp/ical.log"); print T "Request: $url\n"; print T "Response:\n", $response->content, "\n"; close (T); } if (wantarray) { my $errno; $errno = $response->content; $errno =~ /X-NSCP-WCAP-ERRNO:(\d+)/; $errno = $1; return ($errno, $response->content); } else { return ($response->content); } } ################ # POST wcap commands # specifically tuned to the IMPORT command; content is assumed to be # text/calendar and sent as a form submission # arguments: # $command -- the command name # $content -- the data to POST # @args -- a list of arguments to send to the command (optional) # returns: # errno and content in an array context # content in scalar context sub wcap_command_post ($$;@) { my ($command, $content, @args) = @_; my ($argstring, $request, $url); if (@args) { $argstring = join ('&', @args); } if ($argstring ne "") { $url = "http://$CALSERVER/$command.wcap?$argstring"; } else { $url = "http://$CALSERVER/$command.wcap"; } if ($command eq "import") { $request = POST( $url, Content_Type => 'form-data', Content => [ Upload => [ undef, "ical.ics", Content_Type => 'text/calendar', Content => $content ] ] ); } elsif ($command eq "export") { $request = POST( $url, Content_Type => 'form-data', Content => [ Download => [ undef, "export.ics", Accept => 'image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, image/png, */*', Accept_Encoding => 'deflate,gzip', Accept_Language => 'en', Accept_Charset => 'iso-8859-1,*,utf-8' ] ] ); } my $browser = LWP::UserAgent->new; $browser->agent("$SERVER_ID/$SERVER_VERSION"); my $response = $browser->request($request); if ($DEBUG) { open (T, ">>/tmp/ical.log"); print T "Request: $url\n"; print T "Content: $content\n"; print T "Response:\n", $response->content, "\n"; close (T); } if (wantarray) { my $errno; $errno = $response->content; $errno =~ /X-NSCP-WCAP-ERRNO:(\d+)/; $errno = $1; return ($errno, $response->content); } else { return ($response->content); } } ############################################# # Specific WCAP commands ############################################# # login to the calendar server and return an authentication ID sub wcap_login ($$) { my ($user, $pass) = @_; my ($url, $id, $content); $id = "0"; if ($user eq "" || $pass eq "") { return $id; } $content = wcap_command("login", "user=$user", "password=$pass"); my $tid; if ($content =~ /var id='(\w+)'/) { # WCAP pre 3.0 (Calendar 5.x) $tid = $1; } elsif ($content =~ /X-NSCP-WCAP-SESSION-ID:(\w+)/) { # WCAP 3.0 (Calendar 6.x) $tid = $1; } if (defined $tid && $tid ne "") { $id = $tid; } return ($id); } # destroy any logged in session on the server with the authentication ID sub wcap_logout ($) { my ($id) = @_; wcap_command("logout", "id=$id"); } # get the info about a calendar # primarily used to see if the calendar exists and if we have read/write # access. # errno == 0, access granted, no error # errno == 28, read access denied # errno == 29, calendar does not exist sub wcap_get_calprops ($$) { my ($id, $cal) = @_; my ($errno, $content); ($errno, $content) = wcap_command ("get_calprops", "id=$id", "calid=$cal", "fmt-out=text/calendar"); if (wantarray) { return ($errno, $content); } else { return $content; } } # delete the contents of a calendar sub wcap_deletecomponents_by_range ($$) { my ($id, $calid) = @_; my ($errno, $content) = wcap_command ("deletecomponents_by_range", "id=$id", "calid=$calid", "fmt-out=text/calendar"); if (wantarray) { return ($errno, $content); } else { return $content; } } # import a calendar in text/calendar format sub wcap_import ($$$) { my ($id, $calid, $content) = @_; wcap_command_post ("import", $content, "id=$id", "calid=$calid", "content-in=text/calendar") } # export a calendar in text/calendar format sub wcap_export ($$) { my ($id, $calid) = @_; wcap_command_post ("export", "", "id=$id", "calid=$calid", "content-out=text/calendar") } # retrieve the contents of a calendar in text/calendar format (subscriptions) sub wcap_fetchcomponents_by_range ($$) { my ($id, $calid) = @_; my ($errno, $content) = wcap_command ("fetchcomponents_by_range", "id=$id", "calid=$calid", "fmt-out=text/calendar"); if (wantarray) { return ($errno, $content); } else { return $content; } } ############################################################################ # POD DOCUMENTATION =pod =head1 NAME icald - iCal to WCAP calendar publish and subscribe gateway =head1 SYNOPSIS B [-d] [-r authentication realm] [-a listen address] [-p listen port] [-s calendar server address] [-u user to run as] [-e SSL port] =head1 DESCRIPTION B is a simple HTTP server that implements the basic components needed to subscribe to and publish calendars with any of the Sun calendar servers that implement the WCAP protocol (including iPlanet, Sun ONE, and Sun Java). Other servers that implement WCAP may work but have not been tested. =over 8 =item B<-a> listen address The address on which to listen. Default is empty, indicating that the server will listen on all addresses on the host. =item B<-d> Turn on debugging mode. Server will not fork and will write some debugging information to F. =item B<-e> SSL port Specifies the port number that an SSL tunnel for the B daemon listens on. If, for example, B or Apache is configured to proxy SSL to B and a request for a F URL is found, a 301 redirect is sent back to iCal on non-SSL requests. This allows iCal to use SSL transport, even though F URLs are not supported in iCal. =item B<-p> listen port The TCP port to bind to. The default is 7080. =item B<-r> authentication realm The realm used in HTTP Basic auth. The default is B. =item B<-s> calendar server address The name or IP address of the WCAP-capable calendar server. The server is assumed to be listening for HTTP on port 80 and URLs within B are constructed as such: http://CALENDAR-SERVER/command.wcap... =item B<-u> user to run as When started as root, the server will change its UID and GID to that of the user specified after it has bound to the port (in case a port less than 1024 is specified). The default is to run as the user invoking the program. =back =head1 URI SCHEMA B maps subscribe and publish requests to calendar server calendars using the URI of the request. The following mappings are used. =head2 Subscribe =over 8 =item /B If the URI includes only B, then that is taken as the calendar name and anonymous access is attempted to the calendar server. If the calendar server response indicates that read access is not allowed for anonymous users, Basic authentication will be attempted. This method aldo works in the case of users' default calendars, where the calendar name is the same as the user name. =item /B/B If the URI includes one intermdiate slash, the first part is taken as the username of the calendar to view and the second part is taken as the calendar name. As with the previous method, if read access is not permitted, access is attempted again after Basic authentication. =item /login/B =item /login/B/B In the case where one would like to force authentication for subscribing to a calendar, B can be specified as the first path component of the URI. This will force Basic authentication even if anonymous users have read access to the calendar. This is useful in the case where anonymous users may not see as much detail in event information as certain authenticated users. In all other respects, these two methods work as the first two. =item /loginssl/B =item /loginssl/B/B These are identical to the B URLs, except that if they are found in a non-https connection, a 301 redirect is sent back to iCal. The redirect specifies an SSL connection on an alternate port (defaulting to 7443). This gets around an apparent bug in iCal that makes it unable to handle https URLs in subscribing to calendars. If the subscription address redirects through https, however, iCal has no problems. =back =head2 Publish All calendar publish requests require authentication, so there is no concept of an anonymous user. The user to publish as is taken from the HTTP Basic authentication information. In Apple iCal, the calendar is published using a name specified by the user. The URI that is sent to the server includes this name with F<.ics> appended as the final part of the URI. The following mappings are what is seen within B. =over 8 =item /B When only a calendar name is specified by the user, it is taken as the calendar name on the WCAP server. This also works in the case where the calendar name is the same as the user name. In iCal, one would specify this as Publish name: CALENDAR or USERNAME Base URL: http://server:port/ =item /B/B When there are two path components of the URI, the first is taken as the user name for the calendar and the second is taken as the calendar name. The user name may be different than that used in authentication. In iCal, one would specify this as Publish name: CALENDAR Base URL: http://server:port/USERNAME/ =item /B/B If both path components are the same, they are taken as the default calendar of a user that may be different from that used for authentication. As an example for iCal, one would specify Publish name: USERNAME Base URL: http://server:port/USERNAME/ Login: OTHERUSER It is of course possible to make B and B identical, in which case this method is no different from the first when the user is specifying their own default calendar. =back =head1 BUGS & LIMITATIONS The only interface for Sun ONE Calendar that utilizies the full features of the product is the Sun ONE Calendar Express web interface. All other interfaces, including this one, present limitations. In particular, the following limitations are known: =head2 Publish Mode When using B to publish calendars, there is no concept of multiple owners of a calendar. That is to say, Sun ONE will happily allow multiple people to publish to the same calendar, but it will make no attempt to synchronize differences among the calendars it receives. If one person publishes a set of events to a calendar and another person publishes a different set of events to the same calendar, the first set will be lost. It is up to the multiple calendar owners to manually synchronize events, either by hand or by a multi-step subscribe, copy, publish process. In addition, other features that pertain to the calendar's originating system are not transferred -- in particular this includes alarms. It only makes sense for one system to control alarms, and whn the originator of the calendar is iCal, it will retain this control and strip alarm tags from the published calendar. Other group interaction features available within Sun calendars (including invitations and event privacy options) are not present in iCal and thus cannot be published through B. If you have need of these features, the Sun ONE Calendar Express web interface is the only interface that supports them. Be aware than any changes made through the web interface to a calendar published via B will be lost the next time the calendar is published. =head2 Subscribe Mode Subscribed calendars in iCal are read-only; you can not make changes to the events or add new ones. To Do items will be transferred (if so checked on the iCal subscription preferences), however e-mail reminders are not transferred, as they would have no place outside of the server environment (while VALARM tags within the export file are passed to iCal, iCal will strip these from the calendar). =head2 Other B is essentially just a gateway between iCal or something that uses that protocol and the Sun family of WCAP-capabale calendar servers. No attempt is made to translate the calendar data passed between the two, only to present it to each side in a form they understand. Any problems in how the data is interpreted by either side are the responsibility of the end programs. =head1 COPYRIGHT Copyright (C) 2004-2005, John "Rowan" Littell. All rights reserved. Redistribution of this script, either in source or any compiled binary format, with or without modificiation, is permitted provided the following conditions are met: =over 4 =item 1. Redistributions must retain the above copyright notice, this list of conditions, and the following disclaimer either within the script itself or within the documentation or other materials accompanying the script. =item 2. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. =back THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. THIS SOFTWARE IS PROVIDED WITHOUT ANY OBLIGATION ON THE PART OF THE AUTHOR TO ASSIST IN ITS USE, CORRECTION, MODIFICATION, OR ENHANCEMENT. =head1 TRADEMARKS B is a registered trademark of Apple Computer, Inc. B, B, and B are registered trademarks of Sun Microsystems, Inc. =head1 SEE ALSO http://www.apple.com/ical, Sun ONE Calendar Server Programmer's Manual =head1 AUTHOR John "Rowan" Littell - littejo at earlham dot edu =cut