[prev in list] [next in list] [prev in thread] [next in thread] 

List:       kde-pim
Subject:    [Kde-pim] Little re-write of ical2vcal
From:       Mike Heins <mheins () redhat ! com>
Date:       2001-10-13 3:46:28
[Download RAW message or body]

I don't know whether work has gone on with this program, but I had some
needs, found it in KDE 2.x, and re-wrote it a bit. Of course I am
offering the mods back to the project.

There are several feature additions, notably the ability to sync
directly from a Palm-compatible via IR or serial. You can also point
it at a web gateway which can publish your .vcs file to a URL (that
functionality is only on Interchange to date.)

Also, I cleaned up quite a few things, fixed a minor security hole,
and documented it. It is still all one file, but uses Perl's embedded
POD documentation.

I will be working on it some more, probably to allow bi-directional
sync, but thought what I had was enough improvement to pass along.

The file is attached.

Best,
Mike Heins
-- 
Red Hat, Inc., 3005 Nichols Rd., Hamilton, OH  45013
phone +1.513.523.7621      <mheins@redhat.com>

Friends don't let friends use Outlook. -- Bob Blaylock


#! /usr/bin/perl
# THE ABOVE LINE SHOULD POINT TO YOUR PERL EXECUTABLE!
# ical2vcal
# (c) 1998 Preston Brown
# Part of the KOrganizer Project
#
# (c) 2001 Mike Heins, Red Hat, Inc.
#
# This utility will read ~/.calendar in the ical format as input, and output 
# a file in the versit vCalendar format with a .vcs extension. It also works
# in conjunction with pilot-link to read from a Palm.
#
# This code is covered by the GNU Public License. Please see
# http://www.gnu.org for more information.
#

#use strict;

use vars qw/ $HaveLWP $opt_f $opt_d $opt_p $opt_u $opt_w $opt_h $opt_o $opt_v /;
my $pcount = 0;

use Getopt::Std;
BEGIN {
	eval {
		require LWP::UserAgent;
		import LWP::UserAgent;
		require HTTP::Request::Common;
		require Crypt::SSLeay;
		import HTTP::Request::Common qw(POST);
		$HaveLWP = 1;
	};
}

my @Out;

my $USAGE = <<EOF;
ical2vcal [-d device] [-f icalfile] [-o vcalfile] [icalfile] [vcalfile]

OPTIONS:
    -d device     Palm device to do direct hotsync
    -f icalfile   ical file (default ~/.calendar)
    -h            print this message
    -o vcalfile   VCAL (ICal RFC2445) file (default standard output)
    -p var=pass   password for web upload (if any)  
    -u var=user   username for web upload (if any)
    -v            be slightly verbose
    -w weburl     URL to post data to, implies no -o

EOF

=head1 NAME

ical2vcal - enhanced ical to vcal translation

=head1 SYNOPSIS

ical2vcal [-d device] [-f icalfile] [-o vcalfile] [icalfile] [vcalfile]

=head1 DESCRIPTION

This program translates the X11 C<ical> program format (NOT RFC 2445 ICal)
to KDE korganizer(1) C<vcal> (which, ironically is quasi-RFC 2445). The
C<vcal> format produced is not completely RFC 2445 compliant.

If you have a Palm-compatible handheld, and use the C<read-ical> program
supplied by the pilot-link utilities, you can read/beam directly from the
Palm to korganizer(). IrDA (infrared) and serial connections work equally
well. USB should also work, but has not been tested as of this writing.

If you have an HTTP server you can upload the calendar directly to it
by giving the URL in option -w. User and password can be supplied, but
obviously are not encrypted. Since the program does an HTTP POST, it
should be relatively secure.

=head1 OPTIONS

In the simplest case, you just  all the program by its name. 
It will read $HOME/.calendar and write to the standard output.

The options:

=over 4

=item -d device

The device name of a Palm-compatible device to do direct hotsync from.
A typical value would be C</dev/pilot>. See your handheld and computer
documentation on how to get it talking to the device. The pilot-link(7)
man page will be helpful here.  Mutually exclusive with the -f option.

=item -f icalfile

The ical file to read from; default is $HOME/.calendar. Mutually exclusive
with the -d option.

=item -h

Print rudimentary help message.

=item -o vcalfile

VCAL file to write; default is the standard output. A typical values
would be C<$HOME/.kde/share/apps/korganizer/schedule.vcs>.

Mutually exclusive with the -w option.

=item -p [pass | var=pass]

The password to send to the CGI you intend to upload to. If there is
an equals sign, the string is sent as a pair. If there is no equals
sign, then the variable name will be C<pass>. If not set, no password
is sent.

=item -u [user | var=user]

The username to send to the CGI you intend to upload to. If there is
an equals sign, the string is sent as a pair. If there is no equals
sign, then the variable name will be C<user>. If not set, no password
is sent.

=item -v

Be a little verbose, putting output file name on STDERR and showing
the HTTP response code and reason.

=item -w weburl

URL to post data to, implies no -o.

=back
EOF

=head1 BUGS

This program only writes from the Palm to the system.

You can't have an equals sign in your password (or username, for
that matter).

This program should be compatible with the original ical2vcal(0),
though Priority, Todo/Done, and some other things are now supported
and if you are used to the Priority always being 0 then you will be
in for a surprise.

=head1 SEE ALSO

pilot-link(7), read-ical(1)

=head1 AUTHORS

Originally by Preston Brown,  part of the KOrganizer Project. Re-written
and expanded by Mike Heins for use with Interchange (http://ic.redhat.com).

=cut

$VERSION = '0.01';

getopts('d:f:ho:p:u:vw:');

use POSIX qw/tmpnam/;

my $filename;
my $outfilename;

if($opt_h) {
	print $USAGE;
	exit 2;
}

if($opt_d) {
	## This is a direct hotsync with read-ical
	$filename = tmpnam();
}
elsif ($opt_f) {
	$filename = $opt_f;
}
else {
	$filename = "$ENV{HOME}/.calendar";
}

if($opt_o) {
	$outfilename = $opt_o;
}
else {
	if(@ARGV > 2) {
		warn $USAGE;
		exit 2;
	}
	if(@ARGV == 2) {
	    if($opt_d) {
			warn "-d conflicts with trailing command line arguments.\n\n$USAGE";
			exit 1;
	    }
	    elsif ($opt_f) {
		    warn "-f conflicts with trailing command line arguments.\n\n$USAGE";
		    exit 1;
	    }
	    $filename = shift(@ARGV);
	}
	$outfilename =    shift(@ARGV);
}

if($opt_w and $outfilename) {
	warn "-w conflicts with trailing command line arguments.\n\n$USAGE";
	exit 1;
}

my $exitstatus = 0;

if($opt_d) {
	system('read-ical', $opt_d, $filename);
	if($?) {
		die "Failed to sync from Palm: $!\n";
	}
}

if (!open(ICALFILE, "< $filename")) {
    $! = -1;
    exit;
}

if($outfilename) {
	if (!open(VCALFILE, ">$outfilename")) {
		$! = -1;
		exit;
	}
}


$line = <ICALFILE>;

&write_header;

if ($line =~ /Calendar(\s+)\[v/) {
    while ($line = &getLine) {
		if (($line =~ /^Appt/) || ($line =~ /^Note/)) {
			&process_appointment;
			&write_appointment;
		}
		else {
			# silently skip line
		}
    }
}
else {
    # not a ical file?!
    $! = -2;
    exit;
}

close(ICALFILE);

my $caldata = join "\n", @Out;


if($outfilename) {
warn "outputfile='$outfilename'\n" if $opt_v;
	print VCALFILE $caldata;
	close(VCALFILE);
}
elsif ($opt_w) {
warn "send URL: $opt_w\n" if $opt_v;
	if(! $HaveLWP) {
		die "Must have LWP and HTTP::Request::Common for web sync.\n";
	}
	my %header = ( 'User-Agent' => "LWP::UserAgent (ical2vcal version $VERSION)");
	my @query;

	my $uvar;
	my $user;

	if(! $opt_u) {
		# do nothing
	}
	elsif($opt_u =~ /=/) {
		($uvar, $user) = split /=/, $opt_u, 2;
	}
	else {
		$uvar = 'user';
		$user = $opt_u;
	}

	my $pvar;
	my $pass;

	if(! $opt_p) {
		# do nothing
	}
	elsif($opt_p =~ /=/) {
		($pvar, $pass) = split /=/, $opt_p, 2;
	}
	else {
		$pvar = 'pass';
		$pass = $opt_p;
	}

	push @query, $uvar, $user
		if $uvar;
	push @query, $pvar, $pass
		if $pvar;
	push @query, 'ical', $caldata;
#use Data::Dumper;

	my $ua = new LWP::UserAgent;
	my $req = HTTP::Request::Common::POST($opt_w, \@query, %header);
#warn "request=" . Dumper($req);

	my $resp = $ua->request($req);
#warn "request=" . Dumper($resp);
	my $status = $resp->status_line();
#warn "status=$status\n";

	my $response_code;
	$status =~ /(\d+)/
		and $response_code = $1;
warn "response code: $response_code\n" if $opt_v;
	$result_page = $resp->content();
warn "result: $result_page\n" if $opt_v;
	if($status !~ /^20/) {
		die "Failed to put calender: $result_page\n";
	}
}
else {
warn "sending to standard output.\n" if $opt_v;
	print $caldata;
}

my(%current);

sub getLine
{
    $_ = <ICALFILE>;
    if (!defined($_)) {
		&write_footer;
		if ($exitstatus) {
	warn("exiting on $exitstatus");
			$! = $exitstatus;
			exit;
		}
		else {
			return;
		}
    }
    s/\\\[/\(/g;
    s/\\\]/\)/g;
    $pcount += tr/\[//;
    $pcount -= tr/\]//;
    return $_;
}

sub process_appointment {

	%current = ();
    # this is a count of the total # of parentheses.
    while ($pcount) {
	$line = &getLine;
	
	# check to see if more is left to be processed.
	if ($pcount > 0) {
 	    # process the line.

 	    if ($line =~ /^Start/) {
		# start time (minutes since midnight)
			$_ = $line;
			($totalmin) = /\[(\d+)\]/;
			$min = $totalmin % 60;
			$hour = int ($totalmin / 60);
			$current{"starthour"} = $hour;
			$current{startmin} = $min;
		
 	    }
		elsif ($line =~ /^Length/) {
			# time length (minutes)
			$_ = $line;
			($lengthmin) = /\[(\d+)\]/;
			$min = $lengthmin % 60;
			$hour = int ($lengthmin / 60);
			$current{endhour} = $hour;
			$current{endmin} = $min;
	    }
		elsif ($line =~ /^Uid/) {
			# unique identifier
			$line =~ /\[(.+)\]/
				and
			$current{uid} = $1 ;
		}
		elsif ($line =~ /^Owner/) {
			# appointment's owner
			$line =~ /\[(\w+)\]/
					and
			$current{attendee} = $1;
		}
		elsif ($line =~ /^Priority/) {
			# appointment's owner
			$line =~ /\[(\d+)\]/
				and
			$current{priority} = $1;
		}
		elsif ($line =~ /^Contents/) {
			# description
			$description = "";
			$_ = $line;
			# special case where it's all in one place:
			if (/\[(.*)\]/) {
				$summary = $1;
			}
			else {
				($summary) = /\[(.*)/;
				$_ = &getLine;
				while (!(/\]$/)) {
				chop;
				$description = $description . " " . $_;
				$_ = &getLine;
				}
				/(.*)\]$/;
				$description = $description . $1;
			}
			$current{summary} = $summary;
			if (length($description) > 0) {
				$summary = $summary . "...";
				$current{description} = $description;
			}
	    }
		elsif ($line =~ /^Text/) {
			$description = "";
			$_ = $line;
			if (/\[\d+\s+\[(.*)\]\]/) {
				$summary = $1;
			}
			else {
				($summary) = /\[\d+\s+\[(.*)$/;
				$_ = &getLine;
				while (!(/\]$/)) {
				chop;
				$description = $description . " " . $_;
				$_ = &getLine;
				}
				/^(.*)\]\]/;
				$description = $description . $1;
			}
			$current{summary} = $summary;
			if (length($description) > 0) {
				$summary = $summary . "...";
				$current{description} = $description;
			}
	    }
		elsif ($line =~ /^Alarms/) {
			$line =~ /(\d+)\]/
				and $current{alarmtime} = $1;
	    }
		elsif ($line =~ /^Todo/) {
			$current{todo} = 1;
	    }
		elsif ($line =~ /^Dates/) {
			# dates to occur on
			&process_dates;
	    }
		elsif ($line =~ /^\]/) {
			# do nothing
	    }
		elsif ($line =~ /^Hilite/) {
			# do nothing
	    }
		elsif ($line =~ /^Remind/) {
			# do nothing
	    }
		elsif ($line =~ /^Todo/) {
			# no support for todo's yet
			$exitstatus = 1;
		}
		elsif ($line =~ /^Done/) {
			$current{done} = 1;
	    }

	} # if $pcount > 0

    } # while pcount

    if (defined($current{starthour})) {
		# fix up end time, just peg it at the end of the day
		$endhour = $current{starthour} + $current{endhour};
		$endmin = $current{startmin} + $current{endmin};
		$endhour = $endhour + int ($endmin / 60);
		$endmin = $endmin % 60;
		$current{endhour} = $endhour;
		$current{endmin} = $endmin;
		if ($endhour >= 24) {
			$current{endhour} = 23;
			$current{endmin} = 55;
		}
    }
}

sub write_header {
    push @Out, "BEGIN:VCALENDAR";
    push @Out, "PRODID:-//K Desktop Environment//NONSGML KOrganizer//EN";
    push @Out, "VERSION:1.0";
}

sub write_footer {
    push @Out, "END:VCALENDAR";
}

sub write_appointment {
    if (defined($current{tossme})) {
        return;
    }

    if (defined($current{todo})) {
        push @Out, "BEGIN:VTODO";
    }
	else {
        push @Out, "BEGIN:VEVENT";
    }
    $tmpstr = &date_to_str($current{startyear},
                           $current{startmonth},
                           $current{startday});
    if (defined($current{starthour})) {
        $tmpstr = $tmpstr . &time_to_str($current{starthour},
                               $current{startmin});
    }
	else {
        $tmpstr = $tmpstr . &time_to_str("0","0");
    }
    push @Out, "DCREATED:" . $tmpstr;
    push @Out, "UID:" . $current{uid};
    push @Out, "SEQUENCE:0";
    push @Out, "LAST-MODIFIED:$tmpstr";
    push @Out, "DTSTART:$tmpstr";
    if (defined($current{starthour})) {
        $tmpstr = &date_to_str($current{startyear},
                              $current{startmonth},
                              $current{startday}) . 
                                  &time_to_str($current{endhour},
                                               $current{endmin});
    } 
    push @Out, "DTEND:$tmpstr";
    if (defined($current{summary})) {
        $summary = $current{summary};
        push @Out, "SUMMARY:$summary";
    }
    if (defined($current{description})) {
        $description = $current{description};
        push @Out, "DESCRIPTION:$description";
    }
    if (defined($current{attendee})) {
        $attendee = "ATTENDEE;ROLE=OWNER:" . $current{attendee};
        push @Out, $attendee;
    }
    
    if (defined($current{alarm})) {
        
    }

    if (defined($current{repeats})) {
        # wow what a mess
        $rule = "RRULE:";
        if ($current{repeats} eq "DAILY") {
            $rule = $rule . "D" . $current{period};
        }
		elsif ($current{repeats} eq "WEEKLY") {
            $rule = $rule . "W1" . " ";
            $rule = $rule . $current{weekdays};
                
        }
		elsif ($current{repeats} eq "MONTHLY") {
            $rule = $rule . "MD" . $current{period};
            $rule = $rule . " " . $current{startday};
        }
        if ($current{endrepeat} && ($current{endrepeat} =~ /T/)) {
            $rule = $rule . " " . $current{endrepeat};
        }
		elsif ($current{endrepeat}) {
            $rule = $rule . " \#" . $current{endrepeat};
        }
		else {
            $rule = $rule . " \#0";
        }
        push @Out, $rule;
    }
    if (defined($current{exceptions})) {
        $exceptions = "EXDATE:" . $current{exceptions};
        chop($exceptions);
        push @Out, $exceptions;
    }
	if($current{todo}) {
		if($current{done}) {
			$current{status} = 'COMPLETED';
		}
		else {
			$current{status} = 'NEEDS-ACTION';
		}
	}
	else {
			$current{status} = 'TENTATIVE';
	}
    push @Out, "STATUS:$current{status}";
    push @Out, "CLASS:PUBLIC";
    push @Out, "PRIORITY:" . ($current{priority} || 0);
    push @Out, "TRANSP:0";
    push @Out, "RELATED-TO:0";
    if (defined($current{todo})) {
        push @Out, "END:VTODO\n";
    }
	else {
        push @Out, "END:VEVENT\n";
    }
}

sub date_to_str
{
    $year = shift(@_);
    $month = shift(@_);
    $day = shift(@_);
    my($datestr);
    $datestr = sprintf("%04d%02d%02d",$year,$month,$day);
    return $datestr;
}

sub time_to_str
{
    $hour = shift(@_);
    $min = shift(@_);
    my($timestr);

    $timestr = sprintf("T%02d%02d00",$hour,$min);
    return $timestr;
}

sub process_dates
{
    # first, test for single
    $_ = $line;
    if (/\[Single/)  {
        &repeat_none;
    }
	elsif (/\[Days/) {
        &repeat_daily;
    }
	elsif (/\[WeekDays/) {
        &repeat_weekly;
    }
	elsif (/\[Months/) {
        &repeat_monthly;
    }
	elsif (/\[ComplexMonths/) {
        $exitstatus = 1;
        #printf("WARNING: complex repeating month entry detected, we don't support.\n");
        #printf("converting to a single occurrence on the original start date.\n");
        $line = &getLine;
        &repeat_none;
    }
	elsif (/\[Empty/) {
        # silently toss
		$current{tossme} = "TRUE";
    }
	else {
	$exitstatus = 1;
	#print "didn't understand line: $_";
    }
    while ($line = &getLine) {
	if ($line =~ /^\]/) {
	    return;
	}
	elsif ($line =~ /^Finish/) {
	    ($day, $month, $year) = /(\d+)\/(\d+)\/(\d+)/;
	    $current{"endrepeat"} = &date_to_str($year, $month, $day);
	    $current{"endrepeat"} = $current{"endrepeat"} . &time_to_str("0","0");
	}
	elsif ($line =~ /^Deleted/) {
	    ($day, $month, $year) = /(\d+)\/(\d+)\/(\d+)/;
	    if (defined($current{"exceptions"})) {
		$current{"exceptions"} = $current{"exceptions"} .
		    &date_to_str($year, $month, $day) . ";";
	    }
		else {
		$current{"exceptions"} = &date_to_str($year, $month, $day) .
		    ";";
	    }
	}
	else {
	    $exitstatus = 1;
	    #print "trashed line: $line";
	}
    }
}

sub repeat_none
{
    # just a one time shot
    ($day, $month, $year) = /(\d+)\/(\d+)\/(\d+)/;
    $current{"startmonth"} = $month;
    $current{"startday"} = $day;
    $current{"startyear"} = $year;
}

sub repeat_daily
{
    # repeats on a daily basis
    $current{"repeats"} = "DAILY";
    ($skip) = /(\d+)$/;
    $current{"period"} = $skip;
    $line = &getLine;
    ($day, $month, $year) = /(\d+)\/(\d+)\/(\d+)/;
    $current{"startmonth"} = $month;
    $current{"startday"} = $day;
    $current{"startyear"} = $year;
}

sub repeat_weekly
{
    # repeats on a weekly basis, a few days a week
    $current{"repeats"} = "WEEKLY";
    $startofdates = index($_,"WeekDays") + length("WeekDays");
    $endofdates = index($_,"Months");
    $datestr = substr($_,$startofdates,($endofdates-$startofdates));
    $datestr =~ s/^\s+//;
    @days = split(/\s+/,$datestr);
    $datestr = "";
    foreach $date (@days) {
	if ($date == 1) {
	    $datestr = $datestr . "SU ";
	}
	elsif ($date == 2) {
	    $datestr = $datestr . "MO ";
	}
	elsif ($date == 3) {
	    $datestr = $datestr . "TU ";
	}
	elsif ($date == 4) {
	    $datestr = $datestr . "WE ";
	}
	elsif ($date == 5) {
	    $datestr = $datestr . "TH ";
	}
	elsif ($date == 6) {
	    $datestr = $datestr . "FR ";
	}
	elsif ($date == 7) {
	    $datestr = $datestr . "SA ";
	}
    }
    # remove one trailing whitespace
    chop($datestr);
    $current{"weekdays"} = $datestr;
    $line = &getLine;
    ($day, $month, $year) = /(\d+)\/(\d+)\/(\d+)/;
    $current{"startmonth"} = $month;
    $current{"startday"} = $day;
    $current{"startyear"} = $year;
}

sub repeat_monthly {
    # repeats on a daily basis
    $current{"repeats"} = "MONTHLY";
    ($skip) = /(\d+)$/;
    $current{"period"} = $skip;
    $line = &getLine;
    ($day, $month, $year) = /(\d+)\/(\d+)\/(\d+)/;
    $current{"startmonth"} = $month;
    $current{"startday"} = $day;
    $current{"startyear"} = $year;
}



_______________________________________________
kde-pim mailing list
kde-pim@mail.kde.org
http://mail.kde.org/mailman/listinfo/kde-pim


[prev in list] [next in list] [prev in thread] [next in thread] 

Configure | About | News | Add a list | Sponsored by KoreLogic