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