| 1 |
#!/usr/bin/perl |
|---|
| 2 |
# cybozu2ical: Convert Cybozu Office calendar into iCalendar format |
|---|
| 3 |
# |
|---|
| 4 |
# $Id$ |
|---|
| 5 |
|
|---|
| 6 |
use strict; |
|---|
| 7 |
use lib 'lib'; |
|---|
| 8 |
|
|---|
| 9 |
use Encode qw( decode_utf8 encode ); |
|---|
| 10 |
use Data::ICal; |
|---|
| 11 |
use Data::ICal::Entry::Event; |
|---|
| 12 |
use Data::ICal::Entry::TimeZone; |
|---|
| 13 |
use Data::ICal::Entry::TimeZone::Standard; |
|---|
| 14 |
use DateTime; |
|---|
| 15 |
use WWW::CybozuOffice6::Calendar; |
|---|
| 16 |
use URI; |
|---|
| 17 |
use Pod::Usage; |
|---|
| 18 |
use Getopt::Long; |
|---|
| 19 |
|
|---|
| 20 |
our $VERSION = '0.32'; |
|---|
| 21 |
|
|---|
| 22 |
### |
|---|
| 23 |
### TRICK (stop escaping for 'exdate' property) |
|---|
| 24 |
### |
|---|
| 25 |
*Data::ICal::Property::_value_as_string = sub { |
|---|
| 26 |
my $self = shift; |
|---|
| 27 |
my $key = shift; |
|---|
| 28 |
my $value = defined( $self->value() ) ? $self->value() : ''; |
|---|
| 29 |
|
|---|
| 30 |
unless ( $self->vcal10 ) { |
|---|
| 31 |
my $lc_key = lc($key); |
|---|
| 32 |
$value =~ s/\\/\\/gs; |
|---|
| 33 |
$value =~ s/\Q;/\\;/gs |
|---|
| 34 |
unless ( $lc_key eq 'rrule' || $lc_key eq 'exdate' ); |
|---|
| 35 |
$value =~ s/,/\\,/gs |
|---|
| 36 |
unless ( $lc_key eq 'rrule' || $lc_key eq 'exdate' ); |
|---|
| 37 |
$value =~ s/\n/\\n/gs; |
|---|
| 38 |
$value =~ s/\\N/\\N/gs; |
|---|
| 39 |
} |
|---|
| 40 |
|
|---|
| 41 |
return $value; |
|---|
| 42 |
|
|---|
| 43 |
}; |
|---|
| 44 |
|
|---|
| 45 |
### |
|---|
| 46 |
### Utility subroutines |
|---|
| 47 |
### |
|---|
| 48 |
sub to_icaldate { |
|---|
| 49 |
my ( $dt, $is_full_day ) = @_; |
|---|
| 50 |
$is_full_day |
|---|
| 51 |
? $dt->ymd('') |
|---|
| 52 |
: $dt->ymd('') . 'T' |
|---|
| 53 |
. $dt->hms('') |
|---|
| 54 |
. ( $dt->time_zone->is_utc ? 'Z' : '' ); |
|---|
| 55 |
} |
|---|
| 56 |
|
|---|
| 57 |
sub encode_string { |
|---|
| 58 |
my ( $enc, $text ) = @_; |
|---|
| 59 |
if ( $enc eq 'ncr' ) { |
|---|
| 60 |
$text =~ s/(\P{ASCII})/sprintf("&#%d;", ord($1))/eg; |
|---|
| 61 |
} |
|---|
| 62 |
else { |
|---|
| 63 |
$text = encode( $enc, $text ); |
|---|
| 64 |
} |
|---|
| 65 |
$text; |
|---|
| 66 |
} |
|---|
| 67 |
|
|---|
| 68 |
sub read_yaml { |
|---|
| 69 |
my $file = shift; |
|---|
| 70 |
my $yaml; |
|---|
| 71 |
if ( eval('require YAML::Tiny') ) { |
|---|
| 72 |
($yaml) = YAML::Tiny::LoadFile($file); |
|---|
| 73 |
} |
|---|
| 74 |
elsif ( eval('require YAML') ) { |
|---|
| 75 |
($yaml) = YAML::LoadFile($file); |
|---|
| 76 |
} |
|---|
| 77 |
if ($@) { |
|---|
| 78 |
die "Faild to read yaml file: $@"; |
|---|
| 79 |
} |
|---|
| 80 |
$yaml; |
|---|
| 81 |
} |
|---|
| 82 |
|
|---|
| 83 |
### |
|---|
| 84 |
### Main part |
|---|
| 85 |
### |
|---|
| 86 |
|
|---|
| 87 |
# Handle command-line options |
|---|
| 88 |
my %opt = ( |
|---|
| 89 |
conf => 'config.yaml', |
|---|
| 90 |
'compat-google-calendar' => 0, |
|---|
| 91 |
'uid' => 1, |
|---|
| 92 |
'url' => 0, |
|---|
| 93 |
); |
|---|
| 94 |
GetOptions( \%opt, 'output=s', 'conf=s', 'compat-google-calendar', 'debug', |
|---|
| 95 |
'input-csv=s', 'output-csv=s', 'help', 'uid!', 'url!' ) |
|---|
| 96 |
or pod2usage(2); |
|---|
| 97 |
pod2usage(1) if $opt{help}; |
|---|
| 98 |
|
|---|
| 99 |
# Read configuration file |
|---|
| 100 |
my $cfg = read_yaml( $opt{conf} ); |
|---|
| 101 |
my $time_zone = $cfg->{time_zone} || 'Asia/Tokyo'; |
|---|
| 102 |
|
|---|
| 103 |
# Generate a root 'calendar' object |
|---|
| 104 |
my $vcalendar = Data::ICal->new(); |
|---|
| 105 |
$vcalendar->add_properties( |
|---|
| 106 |
prodid => "-//as-is.net/Cybozu2ICal $VERSION//EN", |
|---|
| 107 |
calscale => 'GREGORIAN', |
|---|
| 108 |
method => 'PUBLISH', |
|---|
| 109 |
$cfg->{calname} ? ( 'X-WR-CALNAME' => $cfg->{calname} ) : (), |
|---|
| 110 |
'X-WR-TIMEZONE' => $time_zone |
|---|
| 111 |
); |
|---|
| 112 |
|
|---|
| 113 |
# Obtain Cybozu Office 6/7 Calendar items |
|---|
| 114 |
my $cal = WWW::CybozuOffice6::Calendar->new(%$cfg); |
|---|
| 115 |
|
|---|
| 116 |
if ( $opt{'input-csv'} ) { |
|---|
| 117 |
$cal->read_from_csv_file( $opt{'input-csv'} ) |
|---|
| 118 |
or die "Failed to read CSV file: $opt{'input-csv'}"; |
|---|
| 119 |
} |
|---|
| 120 |
else { |
|---|
| 121 |
$cal->request() |
|---|
| 122 |
or die "Failed to get Cybozu Office 6 Calendar"; |
|---|
| 123 |
} |
|---|
| 124 |
|
|---|
| 125 |
# Output the calendar CSV for debugging |
|---|
| 126 |
if ( $opt{'output-csv'} ) { |
|---|
| 127 |
local *FH; |
|---|
| 128 |
open FH, ">$opt{'output-csv'}" or die "Failed to write $opt{'output-csv'}"; |
|---|
| 129 |
print FH "$_\n" for $cal->response; |
|---|
| 130 |
close FH; |
|---|
| 131 |
} |
|---|
| 132 |
|
|---|
| 133 |
# For each items, generate an 'event entry' and append it to the calendar |
|---|
| 134 |
for my $item ( $cal->get_items() ) { |
|---|
| 135 |
my $vevent = Data::ICal::Entry::Event->new(); |
|---|
| 136 |
my %args = ( |
|---|
| 137 |
summary => decode_utf8( $item->summary ), |
|---|
| 138 |
description => decode_utf8( $item->description ), |
|---|
| 139 |
created => to_icaldate( $item->created ), |
|---|
| 140 |
dtstamp => to_icaldate( $item->modified ), |
|---|
| 141 |
); |
|---|
| 142 |
|
|---|
| 143 |
if ( $item->is_full_day ) { |
|---|
| 144 |
$args{dtstart} = |
|---|
| 145 |
[ to_icaldate( $item->start, 1 ), { VALUE => 'DATE' } ]; |
|---|
| 146 |
$args{dtend} = [ to_icaldate( $item->end, 1 ), { VALUE => 'DATE' } ]; |
|---|
| 147 |
} |
|---|
| 148 |
else { |
|---|
| 149 |
$args{dtstart} = |
|---|
| 150 |
[ to_icaldate( $item->start, 0 ), { TZID => $time_zone } ]; |
|---|
| 151 |
$args{dtend} = [ to_icaldate( $item->end, 0 ), { TZID => $time_zone } ]; |
|---|
| 152 |
} |
|---|
| 153 |
|
|---|
| 154 |
# handle frequency |
|---|
| 155 |
if ( $item->can('rrule') ) { |
|---|
| 156 |
|
|---|
| 157 |
# rrule |
|---|
| 158 |
my %rrule = %{ $item->rrule }; |
|---|
| 159 |
$rrule{UNTIL} = to_icaldate( $rrule{UNTIL}, $item->is_full_day ) |
|---|
| 160 |
if $rrule{UNTIL}; |
|---|
| 161 |
$rrule{WKST} = 'SU' |
|---|
| 162 |
if $opt{'compat-google-calendar'}; |
|---|
| 163 |
|
|---|
| 164 |
my @rrule_list; |
|---|
| 165 |
for (qw(FREQ COUNT INTERVAL BYMONTH BYMONTHDAY WKST BYDAY UNTIL)) { |
|---|
| 166 |
push @rrule_list, $_ . '=' . $rrule{$_} |
|---|
| 167 |
if exists $rrule{$_}; |
|---|
| 168 |
} |
|---|
| 169 |
$args{rrule} = join ';', @rrule_list; |
|---|
| 170 |
|
|---|
| 171 |
# exdate |
|---|
| 172 |
if ( $item->exdates ) { |
|---|
| 173 |
if ( $item->is_full_day ) { |
|---|
| 174 |
my $exdate = join ',', |
|---|
| 175 |
map { to_icaldate( $_, 1 ) } $item->exdates; |
|---|
| 176 |
$args{exdate} = [ $exdate, { VALUE => 'DATE' } ]; |
|---|
| 177 |
} |
|---|
| 178 |
else { |
|---|
| 179 |
my $exdate = join ',', |
|---|
| 180 |
map { to_icaldate( $_, 0 ) } $item->exdates; |
|---|
| 181 |
$args{exdate} = [ $exdate, { TZID => $time_zone } ]; |
|---|
| 182 |
} |
|---|
| 183 |
} |
|---|
| 184 |
|
|---|
| 185 |
} |
|---|
| 186 |
|
|---|
| 187 |
# set uid (recommended to be the identical syntax to RFC822) |
|---|
| 188 |
$args{uid} = |
|---|
| 189 |
$item->id . '@' . ( URI->new( $cal->url )->host || 'localhost' ) |
|---|
| 190 |
if $opt{uid}; |
|---|
| 191 |
|
|---|
| 192 |
$args{url} = $cal->url . '?page=ScheduleView&EID=' . $item->id |
|---|
| 193 |
if $opt{url}; |
|---|
| 194 |
|
|---|
| 195 |
# $args{class} = $item->is_private ? 'PRIVATE' : 'PUBLIC'; |
|---|
| 196 |
# $args{transp} = $item->is_private ? 'TRANSPARENT' : 'OPAQUE'; |
|---|
| 197 |
|
|---|
| 198 |
$args{comment} = decode_utf8( $item->comment ) |
|---|
| 199 |
if $opt{debug} && $item->comment; |
|---|
| 200 |
|
|---|
| 201 |
$vevent->add_properties(%args); |
|---|
| 202 |
$vcalendar->add_entry($vevent); |
|---|
| 203 |
} |
|---|
| 204 |
|
|---|
| 205 |
# Generate a 'timezone entry' and append it to the calendar |
|---|
| 206 |
my $vtimezone = Data::ICal::Entry::TimeZone->new(); |
|---|
| 207 |
$vtimezone->add_properties( tzid => $time_zone ); |
|---|
| 208 |
|
|---|
| 209 |
# probably we need to support the Daylight Saving Time, but not yet supported. |
|---|
| 210 |
my $standard = Data::ICal::Entry::TimeZone::Standard->new(); |
|---|
| 211 |
my $std = DateTime->new( |
|---|
| 212 |
year => 1970, |
|---|
| 213 |
month => 1, |
|---|
| 214 |
day => 1, |
|---|
| 215 |
hour => 0, |
|---|
| 216 |
minute => 0, |
|---|
| 217 |
second => 0, |
|---|
| 218 |
time_zone => $time_zone |
|---|
| 219 |
); |
|---|
| 220 |
my $offset = DateTime::TimeZone::offset_as_string( $std->offset ) || '+0900'; |
|---|
| 221 |
my $tzname = $cfg->{tzname} || 'JST'; |
|---|
| 222 |
|
|---|
| 223 |
$standard->add_properties( |
|---|
| 224 |
tzoffsetfrom => $offset, |
|---|
| 225 |
tzoffsetto => $offset, |
|---|
| 226 |
tzname => $tzname, |
|---|
| 227 |
dtstart => to_icaldate($std) |
|---|
| 228 |
); |
|---|
| 229 |
$vtimezone->add_entry($standard); |
|---|
| 230 |
|
|---|
| 231 |
$vcalendar->add_entry($vtimezone); |
|---|
| 232 |
|
|---|
| 233 |
# Outputs the calendar as a string |
|---|
| 234 |
my $text = |
|---|
| 235 |
encode_string( $cfg->{output_encoding} || 'utf8', $vcalendar->as_string ); |
|---|
| 236 |
if ( $opt{output} ) { |
|---|
| 237 |
local *FH; |
|---|
| 238 |
open FH, ">$opt{output}" or die "Failed to write $opt{output}"; |
|---|
| 239 |
print FH $text; |
|---|
| 240 |
close FH; |
|---|
| 241 |
} |
|---|
| 242 |
else { |
|---|
| 243 |
print $text; |
|---|
| 244 |
} |
|---|
| 245 |
|
|---|
| 246 |
1; |
|---|
| 247 |
__END__ |
|---|
| 248 |
|
|---|
| 249 |
=head1 NAME |
|---|
| 250 |
|
|---|
| 251 |
cybozu2ical - Convert Cybozu Office calendar into iCalendar format |
|---|
| 252 |
|
|---|
| 253 |
=head1 SYNOPSIS |
|---|
| 254 |
|
|---|
| 255 |
% cybozu2ical |
|---|
| 256 |
% cybozu2ical --conf /path/to/config.yaml |
|---|
| 257 |
|
|---|
| 258 |
=head1 DESCRIPTION |
|---|
| 259 |
|
|---|
| 260 |
C<cybozu2ical> is a command line application that fetches calendar |
|---|
| 261 |
items from Cybozu Office 6 or later, and converts them into an |
|---|
| 262 |
iCalendar file. It allows you to easily integrate the Cybozu Calendar |
|---|
| 263 |
into iCalendar-enabled Calendar applications, such as Microsoft |
|---|
| 264 |
Outlook, Apple iCal, and of course, Google Calendar. |
|---|
| 265 |
|
|---|
| 266 |
You can run this via crontab, for example, every 1 hour. |
|---|
| 267 |
|
|---|
| 268 |
=head1 REQUIREMENT |
|---|
| 269 |
|
|---|
| 270 |
This application requires perl 5.8.0 with following Perl modules |
|---|
| 271 |
installed on your box. |
|---|
| 272 |
|
|---|
| 273 |
=over 4 |
|---|
| 274 |
|
|---|
| 275 |
=item WWW::CybozuOffice6::Calendar |
|---|
| 276 |
|
|---|
| 277 |
=item Text::CSV_XS or Text::CSV |
|---|
| 278 |
|
|---|
| 279 |
=item DateTime |
|---|
| 280 |
|
|---|
| 281 |
=item LWP::UserAgent |
|---|
| 282 |
|
|---|
| 283 |
=item Class::Accessor::Fast |
|---|
| 284 |
|
|---|
| 285 |
=item Data::ICal |
|---|
| 286 |
|
|---|
| 287 |
=item YAML or YAML::Tiny |
|---|
| 288 |
|
|---|
| 289 |
=back |
|---|
| 290 |
|
|---|
| 291 |
=head1 OPTIONS |
|---|
| 292 |
|
|---|
| 293 |
=over 4 |
|---|
| 294 |
|
|---|
| 295 |
=item --output /path/to/output.ics |
|---|
| 296 |
|
|---|
| 297 |
Specify the output file. By default, this application outputs to |
|---|
| 298 |
STDOUT. |
|---|
| 299 |
|
|---|
| 300 |
=item --conf /path/to/config.yaml |
|---|
| 301 |
|
|---|
| 302 |
Specify the configuration file. By default, C<config.yaml> in the |
|---|
| 303 |
current directory will be used. |
|---|
| 304 |
|
|---|
| 305 |
=item --compat-google-calendar |
|---|
| 306 |
|
|---|
| 307 |
Output an iCalendar file compatible with Google Calendar. |
|---|
| 308 |
|
|---|
| 309 |
=item --debug |
|---|
| 310 |
|
|---|
| 311 |
Output CSV data in a COMMENT field of each events. It's just for |
|---|
| 312 |
debugging. |
|---|
| 313 |
|
|---|
| 314 |
=item --input-csv /path/to/input.csv |
|---|
| 315 |
|
|---|
| 316 |
Instead of requesting Cybozu Office 6 server, read from a local CSV |
|---|
| 317 |
file. |
|---|
| 318 |
|
|---|
| 319 |
=item --output-csv /path/to/output.csv |
|---|
| 320 |
|
|---|
| 321 |
Specify the output CSV file for debugging. |
|---|
| 322 |
|
|---|
| 323 |
=item --uid, --no-uid |
|---|
| 324 |
|
|---|
| 325 |
Enable/Disable UID fields of the iCalendar file. (Default: Enable) |
|---|
| 326 |
|
|---|
| 327 |
=item --url, --no-url |
|---|
| 328 |
|
|---|
| 329 |
Enable/Disable URL fields of the iCalendar file. (Default: Disable) |
|---|
| 330 |
|
|---|
| 331 |
=item --help |
|---|
| 332 |
|
|---|
| 333 |
Print out this message. |
|---|
| 334 |
|
|---|
| 335 |
=back |
|---|
| 336 |
|
|---|
| 337 |
=head1 CONFIGURATION |
|---|
| 338 |
|
|---|
| 339 |
The distributions includes a sample configuration file |
|---|
| 340 |
C<config.yaml.sample>. You can rename it to C<config.yaml> and |
|---|
| 341 |
configure C<cybozu2ical>. |
|---|
| 342 |
|
|---|
| 343 |
=over 4 |
|---|
| 344 |
|
|---|
| 345 |
=item cybozu_url |
|---|
| 346 |
|
|---|
| 347 |
Set the URL of your Cybozu Office 6 or later. |
|---|
| 348 |
|
|---|
| 349 |
=item calname |
|---|
| 350 |
|
|---|
| 351 |
Set the calendar name string. iCalendar applications which properly |
|---|
| 352 |
handle X-WR-CALNAME header, is expected to use this string as a |
|---|
| 353 |
calendar name. |
|---|
| 354 |
|
|---|
| 355 |
=item username, userid |
|---|
| 356 |
|
|---|
| 357 |
Set your username or userid for Cybozu Office. |
|---|
| 358 |
|
|---|
| 359 |
=item password |
|---|
| 360 |
|
|---|
| 361 |
Set your password for Cybozu Office. |
|---|
| 362 |
|
|---|
| 363 |
=item time_zone |
|---|
| 364 |
|
|---|
| 365 |
Set the timezone of your Cybozu Office (e.g., Asia/Tokyo). |
|---|
| 366 |
|
|---|
| 367 |
=item tzname |
|---|
| 368 |
|
|---|
| 369 |
Set the short timezone name of your Cybozu Office (e.g., JST). |
|---|
| 370 |
|
|---|
| 371 |
=item input_encoding |
|---|
| 372 |
|
|---|
| 373 |
Set the charset of Cybozu Office. By default, C<input_encoding> is |
|---|
| 374 |
"shiftjis". |
|---|
| 375 |
|
|---|
| 376 |
=item output_encoding |
|---|
| 377 |
|
|---|
| 378 |
Set the charset of the iCalendar file. By default, C<output_encoding> |
|---|
| 379 |
is "utf8". If you need to output multibyte strings as Numeric |
|---|
| 380 |
Character References for some reason, set C<output_encoding> to "ncr". |
|---|
| 381 |
|
|---|
| 382 |
=item calendar_driver |
|---|
| 383 |
|
|---|
| 384 |
Set the calendar driver that C<cybozu2ical> employs. By default, |
|---|
| 385 |
C<ApiCalendar> is used as C<calendar_driver>. |
|---|
| 386 |
|
|---|
| 387 |
Currently, C<ApiCalendar> and C<SyncCalendar> drivers are shipped with |
|---|
| 388 |
C<cybozu2ical>. If you are using Cybozu Office 6, C<SyncCalendar> is |
|---|
| 389 |
strongly recommended. Otherwise, you have to use C<ApiCalendar>. |
|---|
| 390 |
|
|---|
| 391 |
=item date_range (experimental) |
|---|
| 392 |
|
|---|
| 393 |
Set the date range of calendar, which means C<cybozu2ical> handles |
|---|
| 394 |
calendar items between N days before and after. Default C<date_range> |
|---|
| 395 |
is 30. |
|---|
| 396 |
|
|---|
| 397 |
=back |
|---|
| 398 |
|
|---|
| 399 |
=head1 DEVELOPMENT |
|---|
| 400 |
|
|---|
| 401 |
The development version is always available from the following |
|---|
| 402 |
subversion repository: |
|---|
| 403 |
|
|---|
| 404 |
http://code.as-is.net/svn/public/cybozu2ical/trunk/ |
|---|
| 405 |
|
|---|
| 406 |
You can browse the files via Trac from the following: |
|---|
| 407 |
|
|---|
| 408 |
http://code.as-is.net/public/browser/cybozu2ical/trunk/ |
|---|
| 409 |
|
|---|
| 410 |
Any comments, suggestions, or patches are welcome. |
|---|
| 411 |
|
|---|
| 412 |
=head1 LICENSE |
|---|
| 413 |
|
|---|
| 414 |
Copyright (c) 2008 Hirotaka Ogawa E<lt>hirotaka.ogawa at gmail.comE<gt>. |
|---|
| 415 |
All rights reserved. |
|---|
| 416 |
|
|---|
| 417 |
This library is free software; you can redistribute it and/or modify |
|---|
| 418 |
it under the terms of either: |
|---|
| 419 |
|
|---|
| 420 |
a) the GNU General Public License as published by the Free Software |
|---|
| 421 |
Foundation; either version 1, or (at your option) any later |
|---|
| 422 |
version, or |
|---|
| 423 |
|
|---|
| 424 |
b) the "Artistic License" which comes with Perl. |
|---|
| 425 |
|
|---|
| 426 |
=cut |
|---|