File Coverage

blib/lib/ICal/QuickAdd.pm
Criterion Covered Total %
statement 100 107 93.4
branch 10 20 50.0
condition 3 7 42.8
subroutine 18 19 94.7
pod 11 11 100.0
total 142 164 86.5


line stmt bran cond sub pod time code
1             package ICal::QuickAdd;
2 1     1   26581 use Params::Validate ':all';
  1         16472  
  1         263  
3 1     1   11 use strict;
  1         3  
  1         32  
4 1     1   7 use warnings;
  1         7  
  1         34  
5 1     1   6 use Fcntl qw(SEEK_END O_RDONLY);
  1         2  
  1         92  
6 1     1   6 use Carp;
  1         6  
  1         59  
7 1     1   5 use vars '$VERSION';
  1         3  
  1         1511  
8             $VERSION = '1.00';
9              
10             =head1 DESCRIPTION
11              
12             This is the guts of ICal::QuickAdd, of interest to developers. Most users
13             probably want the docs of L instead, which is a script which uses this
14             module.
15              
16             =head2 new()
17              
18             $iqa = ICal::QuickAdd->new('tomorrow at noon. Lunch with Bob') ;
19              
20             # Default to expecting a email message with the SMS in body, On STDIN
21             $iqa = ICal::QuickAdd->new();
22              
23             =cut
24              
25             sub new {
26 5     5 1 20482 my $class = shift;
27 5         12 my $str = shift;
28              
29 5         14 my $self = {};
30 5 50       25 unless ($str) {
31 0         0 require Mail::Audit;
32 0         0 my $m = Mail::Audit->new(
33             emergency=>"~/mail_audit_emergency_mbox",
34             # log =>'~/mail_audit_log',
35             # loglevel => 3,
36             );
37             # Look on the first line of the message.
38 0         0 $str = $m->body->[0];
39 0         0 $self->{from_email} = $m->from;
40 0         0 $self->{from_email_obj} = $m;
41             }
42 5         16 bless($self,$class);
43              
44 5 50       120 die "no quick-add message found in arg or email" unless $str;
45              
46 5         22 ($self->{dt},$self->{msg}) = $self->parse_date_and_summary($str);
47              
48 5         28 return $self;
49              
50             }
51              
52             =begin private
53              
54             =head2 _is_ical_file
55              
56             my ($is_ical,$line_ending) = _is_ical_file($filename);
57              
58             Returns whether or not we think this file is a iCalendar file by checking
59             that it ends with "END:VCALENDAR" as the standard mandates. We also return
60             the last line break of the file to see whether it is "\n" which Evolution
61             and Korganizer use (among others), or if it "\r\n" (CRLF), which is what
62             the iCalendar standard prescribes.
63              
64             =end private
65              
66             =cut
67              
68             sub _is_ical_file {
69 4     4   348996 my $filename = shift;
70 4         6 my $handle;
71 4 50       251 sysopen( $handle, $filename, O_RDONLY ) || croak "failed to open $filename: $!";
72 4         17 binmode $handle ;
73              
74 4         56 my $file_length = (stat($filename))[7];
75              
76             # "END:VCALENDAR" + \n or CRLF == 15 chars/bytes
77 4         8 my $end_vcal_len = 15;
78              
79             # A valid ics file would be much bigger
80 4 50       16 croak "not valid ICS file" unless ($file_length > $end_vcal_len);
81              
82             # seek to the end of the file and get its size
83 4 50       34 my $seek_pos = seek( $handle, -$end_vcal_len, SEEK_END ) or croak "failed to seek: $!";
84 4         6 my $last_chars;
85 4         141 read($handle, $last_chars, $end_vcal_len);
86             # The spec says we must end in CRLF, not just unix "\n"
87 4         29 my $is_ical_file = ($last_chars =~ m/END:VCALENDAR(\r?\n)$/s) ;
88 4         13 my $line_ending = $1;
89             # $is_ical_file || warn "last chars were: $last_chars";
90 4         73 return ($is_ical_file, $line_ending);
91             }
92              
93             =head2 parse_date_and_summary()
94              
95             $iqa->parse_date_and_summary($msg);
96              
97             Takes a string, such as short SMS text, and parses out a date
98             and event summary from it.
99              
100             Right now it's sort of dumb. It expects the event description
101             to come first, followed by a period, and then a summary. Example:
102              
103             tomorrow at noon. Lunch with Bob
104              
105             The dot was chosen as the delimiter because my cell phone allows
106             me to type it directly, using the "1" key.
107              
108             Limitations: A future version should also return an "$is_date" flag, to note if
109             the date found was a date or a date and time.
110              
111             =cut
112              
113             sub parse_date_and_summary {
114 5     5 1 10 my $self = shift;
115 5         11 my $in = shift;
116              
117 5         1567 require DateTime::Format::Natural;
118 5         193190 my ($date,$msg) = split '\.', $in;
119 5   50     46 $date ||= '';
120 5   50     20 $msg ||= '';
121 5         15 chomp $date;
122 5         12 chomp $msg;
123              
124             # trim leading and trailing whitespace
125 5         57 $msg =~ s/^\s+|\s+$//g;
126              
127 5         10 my $dt;
128 5         13 eval { $dt = DateTime::Format::Natural->new->parse_datetime(string => $date) };
  5         53  
129 5 50       178769 croak "error parsing date ($date). error was: $@" if $@;
130              
131 5         49 return ($dt, $msg);
132             }
133              
134             =head2 inject_into_ics()
135              
136             $iqa->inject_into_ics($filename);
137              
138             Injects a valid ical event block into the env entry into the end of $filename,
139             which is assumed to be a valid iCalendar file. If that assumption is wrong, the
140             file could be corrupted. Use the is_ical_file() to check first!
141              
142             Bugs: Currently always injects a Unix newline. This could corrupt an
143             ICS file with with CRLF line entries.
144              
145             =cut
146              
147             sub inject_into_ics {
148 1     1 1 7 my $self = shift;
149 1         3 my $filename = shift;
150              
151 1         5 my ($is_ical,$line_ending) = _is_ical_file($filename);
152 1 50       6 croak "$filename doesn't look like a valid ICS file" unless $is_ical;
153              
154 1         7 my $entry = $self->as_vevent->as_string;
155              
156 1 50       599 open( my $fh, "+<$filename") || croak "couldn't open $filename: $!";
157              
158             # END:VCALENDAR has 13 chars
159 1         5 my $perfect_length = 13 + length $line_ending;
160              
161             # seek to exactly the right spot to inject our file.
162 1 50       12 my $seek_pos = seek( $fh, -$perfect_length, SEEK_END ) or croak "failed to seek: $!";
163              
164 1   33     12 print $fh $entry || croak "couldn't print to fh: $!";
165              
166 1         4 print $fh "END:VCALENDAR".$line_ending;
167              
168 1 50       54 close ($fh) || croak "couldn't close fh: $!";
169              
170 1         7 return 1;
171              
172             }
173              
174             =head2 parsed_string()
175              
176             my $desc = $iqa->parsed_string;
177              
178             Return a short description. Useful for confirming to the user how the Quick Add string was
179             parsed.
180              
181             Limitations: the description returned currently always includes hours/minute compontent
182             and is in 24 hour time.
183              
184             =cut
185              
186             sub parsed_string {
187 3     3 1 16 my $self = shift;
188 3         16 my $dt = $self->get_dt;
189 3         16 return sprintf("Event: %s on %s %d, %d at %02d:%02d",
190             $self->get_msg, $dt->month_abbr, $dt->day, $dt->year, $dt->hour, $dt->minute);
191              
192             }
193              
194             =head2 as_vevent()
195              
196             my $vevent = $iqa->as_vevent;
197              
198             Return a L object representing the event.
199              
200             For now, hard-code a one hour duration
201              
202             =cut
203              
204             sub as_vevent {
205 2     2 1 4 my $self = shift;
206              
207             # XXX Could add caching here.
208              
209 2         1086 require Data::ICal::Entry::Event;
210 2         12531 require DateTime::Format::ICal;
211 2         83842 my $vevent = Data::ICal::Entry::Event->new;
212 2         136 $vevent->add_properties(
213             summary => $self->get_msg,
214             dtstart => DateTime::Format::ICal->format_datetime($self->get_dt),
215             dtend => DateTime::Format::ICal->format_datetime( $self->get_dt->add( hours => 1 ) ),
216             );
217 2         3091 return $vevent;
218              
219             }
220              
221             =head2 as_ical()
222              
223             my $data_ical = $iqa->as_ical;
224              
225             Returns a L object with the "PUBLISH" method set.
226              
227             The PUBLISH method is used when mailing iCalendar events.
228              
229             =cut
230              
231             sub as_ical {
232 1     1 1 3 my $self = shift;
233              
234 1         1128 require Data::ICal;
235              
236 1         10853 my $calendar = Data::ICal->new;
237 1         400 $calendar->add_entry( $self->as_vevent );
238 1         26 $calendar->add_properties( method => 'PUBLISH');
239              
240 1         124 return $calendar;
241             }
242              
243             =head2 as_ical_email()
244              
245             my $email_simple_obj = $iqa->as_ical_email(
246             To => $your_regular_email,
247             From => $from_email, # Defaults to $iqa->from_email
248             );
249              
250             Returns a ready-to-mail L object with an iCalendar body.
251             Extra headers can be passed in.
252              
253             =cut
254              
255             sub as_ical_email {
256 1     1 1 7 my $self = shift;
257 1         10 my %in = validate(@_, {
258             To => { type => SCALAR },
259             From => { type => SCALAR, default => $self->from_email },
260             });
261              
262 1         1117 require Email::Simple;
263 1         6691 my $email = Email::Simple->new('');
264 1         164 $email->header_set("Content-Type", "text/calendar; name=calendar.ics; charset=utf-8; METHOD=PUBLISH");
265 1         8448 $email->header_set(From => $in{From} );
266 1         52 $email->header_set(To => $in{To} );
267              
268 1         41 $email->header_set("Subject", $self->parsed_string );
269 1         99 $email->body_set( $self->as_ical->as_string );
270              
271 1     1   2691 use Email::Date;
  1         34958  
  1         272  
272 1         780 $email->header_set( Date => format_date );
273              
274 1         191 return $email;
275             }
276              
277              
278              
279             =head2 from_email()
280              
281             Returns the 'from' email address. It can also be used as a check
282             to see if the SMS came from an email at all, since will only be set in that case.
283              
284             =cut
285              
286             sub from_email {
287 1     1 1 2 my $self = shift;
288 1         24 return $self->{from_email};
289             }
290              
291             =head2 from_email_obj()
292              
293             If the input was an email, returns the object representing
294             the incoming message. Currently a L object.
295              
296             =cut
297              
298             sub from_email_obj {
299 0     0 1 0 my $self = shift;
300 0         0 return $self->{from_email_obj}
301              
302             }
303              
304             =head2 get_msg()
305              
306             Return the event name found in the SMS message.
307              
308             =cut
309              
310             sub get_msg {
311 6     6 1 17 my $self = shift;
312 6         51 return $self->{msg};
313             }
314              
315             =head2 get_dt()
316              
317             Returns DateTime object found in SMS.
318              
319             =cut
320              
321             sub get_dt {
322 8     8 1 147 my $self = shift;
323 8         62 return $self->{dt};
324             }
325              
326             =head1 CONTRIBUTING
327              
328             This project is managed using the darcs source control system
329             ( http://www.darcs.net/ ). My darcs archive is here:
330             http://mark.stosberg.com/darcs_hive/ICal-QuickAdd
331              
332             Contributing a patch can be as easy as:
333              
334             darcs get http://mark.stosberg.com/darcs_hive/ICal-QuickAdd
335             cd ICal-QuickAdd
336             # hack...
337             darcs record
338             darcs send
339              
340              
341             =head1 AUTHOR
342              
343             Mark Stosberg C<< mark@summersault.com >>
344              
345             =head1 LICENSE AND COPYRIGHT
346              
347             Copyright (c) 2007, Mark Stosberg C<< mark@summersault.com >>.
348             All rights reserved.
349              
350             This module is free software; you can redistribute it and/or
351             modify it under the same terms as Perl itself. See C.
352              
353              
354             =head1 DISCLAIMER OF WARRANTY
355              
356             BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
357             FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
358             OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
359             PROVIDE THE SOFTWARE ''AS IS'' WITHOUT WARRANTY OF ANY KIND, EITHER
360             EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
361             WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
362             ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
363             YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
364             NECESSARY SERVICING, REPAIR, OR CORRECTION.
365              
366             IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
367             WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
368             REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
369             LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
370             OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
371             THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
372             RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
373             FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
374             SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
375             SUCH DAMAGES.
376              
377              
378              
379             1;
380              
381             # vim: nospell