#!/usr/bin/perl -w
# iCal to WCAP HTTPS 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
# Copyright (C) 2008-2009, Frederic Pariente

#########
# 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);
use Crypt::SSLeay;

#########
# globals
my $CALSERVER = "";
my $REALM = "icalds wcap gateway";
my $LOCALADDR = "";
my $LOCALPORT = 7081;
my $SSL_PORT = 7443;
my $DEBUG = 0;
my $SERVER_ID = "icalds";
my $SERVER_VERSION = "2.1";
my $PIDFILE = "/var/tmp/icald.pid";
my $LOGFILE = "/var/tmp/icald.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 = '<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<HTML><HEAD>
<TITLE>401 Authorization Required</TITLE>
</HEAD><BODY>
<H1>Authorization Required</H1>
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.<P>
</BODY></HTML>
';
	$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);

        my $munged_content = '';
        foreach my $line (split(/\r\n/, $req->content)) {
            # delete problematic (iCal) extensions
            if ( $line =~ /^X-WR-/ || $line =~ /^ ;X-WR-/ ) {
                next;
            }

            $munged_content .= "$line\r\n";
        }

	    #wcap_import ($id, $calname, $req->content);
	    wcap_import ($id, $calname, $munged_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 = '<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<HTML><HEAD>
<TITLE>301 Moved Permanently</TITLE>
</HEAD><BODY>
<H1>Moved Permanently</H1>
The document has moved <A HREF="'.$ssl_uri.'">here</A>.<P>
<HR>
</BODY></HTML>
';
	$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 = '<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<HTML><HEAD>
<TITLE>401 Authorization Required</TITLE>
</HEAD><BODY>
<H1>Authorization Required</H1>
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.<P>
</BODY></HTML>
';
	$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);

	# mung content to take out what iCal/iSync may not like
	my $munged_content = '';
	foreach my $line (split(/\r\n/, $content)) {
	    # strip HTML tags out from summary field generated by NameFinder
            if ($line =~ /^SUMMARY:<html>/) {
                $line =~ s/<([^>]|\n)*>//g;
                $line =~ s/\n //g;
            }
	    # delete problematic (Sun Calendar) extensions
            if ($line =~ /^X-(NSCP|S1CS)-/ ||
                $line =~ /^ ;X-(NSCP|S1CS)-/ ||
                $line =~ /^ ;MEMBER/) {
	        next;
	    }

	    $munged_content .= "$line\r\n";
	}

	if ($DEBUG) {
		open (T, ">/var/tmp/icald.ics");
		print T $munged_content;
		close (T);
	}

	$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 = '<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<HTML><HEAD>
<TITLE>501 Method Not Implemented</TITLE>
</HEAD><BODY>
<H1>Method Not Implemented</H1>
Invalid method request<P>
</BODY></HTML>
';
    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 = "https://$CALSERVER/$command.wcap?$argstring";
    } else {
	$url = "https://$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, ">>/var/tmp/icald.debug");
	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 ($request, $url);
    $url = "https://$CALSERVER/$command.wcap";

    if ($command eq "import") {
        $url .= '?id=' . $args[0] . '&calid=' . $args[1] . '&content-in=' . $args[2];
        $request = POST(
            $url,
            Content_Type => 'form-data',
            Content => [
                     Upload => [
                          undef, "ical.ics",
                          Content_Type => 'text/calendar',
                          Content => $content
                        ]
            ]
        );

    }
    if ($command eq "export") {
        $request = POST(
            $url,
            Content_Type => 'form-data',
            Content => [id => $args[0], calid => $args[1], "content-out" => $args[2],
                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'
                ]
            ]
        );
    }
    if ($command eq "login") {
        $request = POST(
            $url,
            Content_Type => 'form-data',
            Content => [user => $args[0], password => $args[1]]);
    }
#   my $response = $browser->request(POST $url, Content_Type => 'form-data',
#      Content      => [id => $id, calid => $calid, "content-out" => $out]);

    my $browser = LWP::UserAgent->new;
    $browser->agent("$SERVER_ID/$SERVER_VERSION");
    my $response = $browser->request($request);

    if ($DEBUG) {
	open (T, ">>/var/tmp/icald.debug");
	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_post("login", "", "$user", "$pass");
    my $tid;
    if ($content =~ /var id='(\S+)'/) {
	# WCAP pre 3.0 (Calendar 5.x)
	$tid = $1;
    } elsif ($content =~ /X-NSCP-WCAP-SESSION-ID:(\S+)/) {
	# 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", "$calid", "text/calendar")
}

# export a calendar in text/calendar format
sub wcap_export ($$) {
    my ($id, $calid) = @_;

    wcap_command_post ("export", "", "$id", "$calid", "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<icald> [-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<icald> 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</tmp/ical.log>.

=item B<-e> SSL port

Specifies the port number that an SSL tunnel for the B<icald> daemon
listens on.  If, for example, B<stunnel> or Apache is configured to
proxy SSL to B<icald> and a request for a F<loginssl> 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<https> 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<icald wcap
gateway>.

=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<icald> 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<icald> 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<CALENDAR>

If the URI includes only B<CALENDAR>, 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<USERNAME>/B<CALENDAR>

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<CALENDAR>

=item /login/B<USERNAME>/B<CALENDAR>

In the case where one would like to force authentication for
subscribing to a calendar, B<login> 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<CALENDAR>

=item /loginssl/B<USERNAME>/B<CALENDAR>

These are identical to the B<login> 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<icald>.

=over 8

=item /B<CALENDAR>

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<USERNAME>/B<CALENDAR>

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<USERNAME>/B<USERNAME>

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<USERNAME> and B<OTHERUSER>
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<icald> 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<icald>.  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<icald> 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<icald> 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.
Copyright (C) 2008, Frederic Pariente.  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<iCal> is a registered trademark of Apple Computer, Inc.

B<Sun ONE>, B<Sun Java>, and B<iPlanet> are registered trademarks of
Sun Microsystems, Inc.

=head1 SEE ALSO

http://www.apple.com/ical, Apple iCal Homepage
http://docs.sun.com/app/docs/coll/1313.2, Sun Calendar Server 6.3 Documentation

=cut
