File Coverage

blib/lib/Palm/Treo680MessagesDB.pm
Criterion Covered Total %
statement 118 122 96.7
branch 40 44 90.9
condition 13 18 72.2
subroutine 11 11 100.0
pod 1 1 100.0
total 183 196 93.3


line stmt bran cond sub pod time code
1             # $Id: Treo680MessagesDB.pm,v 1.15 2008/07/22 16:40:07 drhyde Exp $
2              
3             package Palm::Treo680MessagesDB;
4              
5 8     8   562770 use strict;
  8         22  
  8         707  
6 8     8   58 use warnings;
  8         17  
  8         332  
7              
8 8     8   14077 use Palm::Raw();
  8         5633  
  8         443  
9 8     8   16375 use DateTime;
  8         2650855  
  8         421  
10 8     8   24283 use Data::Hexdumper ();
  8         20546  
  8         9078  
11              
12 8     8   89 use vars qw($VERSION @ISA $timezone $incl_raw $debug $multipart);
  8         17  
  8         2055  
13              
14             $VERSION = '1.01';
15             @ISA = qw(Palm::Raw);
16             $timezone = 'Europe/London';
17             $debug = 0;
18             $incl_raw = 0;
19              
20             $multipart = {};
21              
22             sub import {
23 8     8   113 my $class = shift;
24 8         27 my %opts = @_;
25 8 100       50 $timezone = $opts{timezone} if(exists($opts{timezone}));
26 8 100       35 $incl_raw = $opts{incl_raw} if(exists($opts{incl_raw}));
27 8 100       46 $debug = $opts{debug} if(exists($opts{debug}));
28 8         70 Palm::PDB::RegisterPDBHandlers(__PACKAGE__, [MsSt => 'MsDb']);
29              
30 8 100       27671 if(!$debug) {
31 8     8   65 no warnings;
  8         19  
  8         38323  
32 7         22 my $orig_Load = \&Palm::PDB::Load;
33             *Palm::PDB::Load = sub {
34 6     6   757 $orig_Load->(@_);
35 3594 100 66     38452 $_[0]->{records} = [
36             grep {
37 6         46 $_->{type} ne 'unknown' &&
38             !(exists($_->{epoch}) && $_->{epoch} < 946684800) # 2000-01-01 00:00
39 6 50 33     2703 } @{$_[0]->{records}}
40             ] if(
41             $_[0]->{creator} eq 'MsSt' &&
42             $_[0]->{type} eq 'MsDb'
43             );
44             }
45 7         21750 }
46             }
47              
48             =head1 NAME
49              
50             Palm::Treo680MessagesDB - Handler for Treo 680 SMS message databases
51              
52             =head1 SYNOPSIS
53              
54             use Palm::PDB;
55             use Palm::Treo680MessagesDB timezone => 'Europe/London';
56             use Data::Dumper;
57              
58             my $pdb = Palm::PDB->new();
59             $pdb->Load("MessagesDB.pdb");
60             print Dumper(@{$pdb->{records}});
61              
62             =head1 DESCRIPTION
63              
64             This is a helper class for the Palm::PDB package, which parses the
65             database generated by a Treo 680 as a record of all your SMSes.
66              
67             =head1 OPTIONS
68              
69             You can set some global options when you 'use' the module:
70              
71             =over
72              
73             =item timezone
74              
75             Defaults to 'Europe/London'.
76              
77             =item incl_raw
78              
79             Whether to include the raw binary blob of data in the parsed records.
80             Defaults to false.
81              
82             =item debug
83              
84             Defaults to false.
85              
86             If false, unknown record-types and those which look like they weren't
87             parsed properly (eg they have an impossible timestamp) are suppressed.
88             This is done by over-riding Palm::PDB's C method.
89              
90             If true, include a hexadecimal dump of each record in the 'debug'
91             field, and don't suppress unknown or badly parsed records.
92              
93             =back
94              
95             =head1 METHODS
96              
97             This class inherits from Palm::Raw, so has all of its methods. The
98             folliwing are over-ridden, and differ from that in the parent class
99             thus:
100              
101             =head2 ParseRecord
102              
103             Returns data structures with the following keys:
104              
105             =over
106              
107             =item rawdata
108              
109             The raw data blob passed to the method. This is only present if the
110             incl_raw option is true.
111              
112             =item date
113              
114             The date of the message, if available, in YYYY-MM-DD format
115              
116             =item time
117              
118             The time of the message, if available, in HH:MM format
119              
120             =item epoch or timestamp (it's available under both names)
121              
122             The epoch time of the message, if available. Note that because
123             the database doesn't
124             store the timezone, we assume 'Europe/London' when converting this
125             to the seperate date and time fields. If you want to change
126             that, then suppy a timezone option when you 'use' the module.
127              
128             Note that this is always the Unix epoch time, even though PalmOS
129             uses an epoch based on 1904.
130              
131             =item name
132              
133             The name of the other party, which the Treo extracts from the SIM
134             phone-book or from the Palm address book at the time the SMS is saved.
135              
136             =item number or phone
137              
138             The number of the other party. This is not normalised so you might see
139             the same number in different formats, eg 07979866975 and +447979866975.
140             I may add number normalisation in the future.
141              
142             =item direction
143              
144             Either 'incoming', or 'outgoing'.
145              
146             =back
147              
148             Other fields may be added in the future.
149              
150             =cut
151              
152             sub ParseRecord {
153 4193     4193 1 304888 my $self = shift;
154 4193         39400 my %record = @_;
155              
156 4193         18197 $record{rawdata} = delete($record{data});
157 4193         10834 my $parsed = _parseblob($record{rawdata});
158 4193 100       28543 delete $record{rawdata} unless($incl_raw);
159              
160 4193         11611 return {%record, %{$parsed}};
  4193         98759  
161             }
162              
163             sub _parseblob {
164 4193     4193   5839 my $buf = shift;
165 4193         7212 my %record = ();
166              
167 4193         11099 my $type = 256 * ord(substr($buf, 10, 1)) + ord(substr($buf, 11, 1));
168 4193         20446 my($dir, $num, $name, $msg) = ('', '', '', '');
169 4193 100 66     26752 if($type == 0x400C || $type == 0x4009) { # 4009 not used by 680?
    100 100        
    100          
    100          
    100          
170 2247 50       40380 $dir = ($type == 0x400C) ? 'inbound' : 'outbound';
171              
172             # ASCIIZ number starting at 0x22
173 2247         27955 ($num = substr($buf, 0x22)) =~ s/\00.*//s;
174              
175             # immediately followed by ASCIIZ name, with some trailing 0s
176 2247         8777 $name = substr($buf, length($num) + 1 + 0x22);
177 2247         17186 $name =~ /^([^\00]*?)\00+(.*)$/s;
178 2247         10643 ($name, my $after_name) = ($1, $2);
179              
180             # four unknown bytes, then ASCIIZ message
181 2247         12968 ($msg = substr($after_name, 4)) =~ s/\00.*//s;
182              
183             # two unknown bytes, then 32-bit time_t, but with 1904 epoch
184 2247         13806 my $epoch = substr($after_name, 4 + length($msg) + 1 + 2, 4);
185              
186 2247         11966 $record{epoch} =
187             0x1000000 * ord(substr($epoch, 0, 1)) +
188             0x10000 * ord(substr($epoch, 1, 1)) +
189             0x100 * ord(substr($epoch, 2, 1)) +
190             ord(substr($epoch, 3, 1)) -
191             2082844800; # offset from Palm epoch (1904) to Unix
192              
193             # if is because DateTime::from_epoch seems to DTwrongT on Win32
194             # when you get a negative epoch
195 2247 100       8666 if($record{epoch} > 0) {
196 2009         11292 my $dt = DateTime->from_epoch(
197             epoch => $record{epoch},
198             time_zone => $timezone
199             );
200 2009         1690091 $record{date} = sprintf('%04d-%02d-%02d', $dt->year(), $dt->month(), $dt->day());
201 2009         52476 $record{time} = sprintf('%02d:%02d', $dt->hour(), $dt->minute());
202             }
203             } elsif($type == 0x0002) {
204 1505         3630 $dir = 'outbound';
205              
206             # ASCIIZ number starting at 0x46
207 1505         17832 ($num = substr($buf, 0x46)) =~ s/\00.*//s;
208              
209             # immediately followed by ASCIIZ name, with some trailing 0s
210             # some Trsm gibberish, then an ASCIIZ message
211             # $name = substr($buf, length($num) + 1 + 0x46);
212             # $name =~ /^([^\00]+)\00+.Trsm....([^\00]+)\00.*$/s;
213             # ($name, $msg) = ($1, $2);
214 1505         9132 ($name = substr($buf, length($num) + 1 + 0x46)) =~ s/\00.*//s;
215 1505 100       5508 $name = undef unless(length($name));
216 1505 100 100     8269 $name .= " (may be truncated)" if($name && length($name) == 31);
217 1505         14821 ($msg = $buf) =~ s/^.*?Trsm....(([^\00]+)\00.*)$/$2/s;
218              
219             # 32-bit time_t, but with 1904 epoch
220 1505         5379 my $epoch = substr($buf, 0x24, 4);
221 1505         14559 $record{epoch} =
222             0x1000000 * ord(substr($epoch, 0, 1)) +
223             0x10000 * ord(substr($epoch, 1, 1)) +
224             0x100 * ord(substr($epoch, 2, 1)) +
225             ord(substr($epoch, 3, 1)) -
226             2082844800;
227 1505         12934 my $dt = DateTime->from_epoch(
228             epoch => $record{epoch},
229             time_zone => $timezone
230             );
231 1505         1318223 $record{date} = sprintf('%04d-%02d-%02d', $dt->year(), $dt->month(), $dt->day());
232 1505         28647 $record{time} = sprintf('%02d:%02d', $dt->hour(), $dt->minute());
233              
234 1505 100 66     32801 if($msg eq "\01N@" && length($1) == 14) { # no real body. bleh
235 7         27 delete @record{qw(epoch date time)};
236 7         38 $type = 'unknown';
237             }
238             } elsif($type == 0x0001) {
239 7         22 $dir = 'outbound';
240              
241             # number field at 0x4C, possibly including some leading crap
242             # then an ASCIIZ number
243 7         60 ($num = substr($buf, 0x4C)) =~ s/(^\00*[^\00]+)\00.*/$1/s;
244              
245             # immediately followed by ASCIIZ name, with some trailing 0s
246 7         46 ($name = substr($buf, length($num) + 0x4C + 1)) =~ s/\00.*//s;
247              
248             # ASCIIZ message, prefixed by 0x20 0x02 16-bit length word
249 7         37 $msg = substr($buf, length($num) + 0x4C + 1 + length($name) + 1);
250 7         99 $msg =~ s/^.*\x20\x02..|\00.*$//g;
251            
252 7         36 $num =~ s/^[^0-9+]+//; # clean leading rubbish from number
253              
254 7         25 my $epoch = substr($buf, 0x24, 4);
255 7         41 $record{epoch} =
256             0x1000000 * ord(substr($epoch, 0, 1)) +
257             0x10000 * ord(substr($epoch, 1, 1)) +
258             0x100 * ord(substr($epoch, 2, 1)) +
259             ord(substr($epoch, 3, 1)) -
260             2082844800;
261 7         59 my $dt = DateTime->from_epoch(
262             epoch => $record{epoch},
263             time_zone => $timezone
264             );
265 7         4059 $record{date} = sprintf('%04d-%02d-%02d', $dt->year(), $dt->month(), $dt->day());
266 7         125 $record{time} = sprintf('%02d:%02d', $dt->hour(), $dt->minute());
267              
268 7 50       115 if($num eq '') {
269 0         0 delete @record{qw(epoch date time)};
270 0         0 $type = 'unknown';
271             }
272             } elsif($type == 0x0000 && substr($buf, 0x0040, 1) ne "\00") {
273 14         33 $dir = 'outbound';
274              
275             # message first, preceded by 0x2002 and 16 bit length
276 14         140 ($msg = $buf) =~ s/^.*\040\02..//s;
277 14         75 $msg =~ s/\00.*//s;
278              
279             # then some cruft, ASCIIZ number and name
280             # find number by finding *last* sequence of 6 or more digits, then
281             # going back 1 to find a + if it's there
282 14         646 ($num, $name) = split(/\00/, ($buf =~ /(\+?\d{6,}\00[^\00]+\00)/g)[-1]);
283              
284 14         78 my $epoch = substr($buf, index($buf, "\x80\00") + 2, 4);
285 14         79 $record{epoch} =
286             0x1000000 * ord(substr($epoch, 0, 1)) +
287             0x10000 * ord(substr($epoch, 1, 1)) +
288             0x100 * ord(substr($epoch, 2, 1)) +
289             ord(substr($epoch, 3, 1)) -
290             2082844800;
291 14         84 my $dt = DateTime->from_epoch(
292             epoch => $record{epoch},
293             time_zone => $timezone
294             );
295 14         8364 $record{date} = sprintf('%04d-%02d-%02d', $dt->year(), $dt->month(), $dt->day());
296 14         244 $record{time} = sprintf('%02d:%02d', $dt->hour(), $dt->minute());
297              
298 14 50       284 if($num eq '') {
299 0         0 delete @record{qw(epoch date time)};
300 0         0 $type = 'unknown';
301             }
302             } elsif($type == 0x0000) {
303 343         1045 $dir = 'outbound';
304              
305             # number field at 0x4C, possibly including some leading crap
306             # then an ASCIIZ number
307 343         3208 ($num = substr($buf, 0x4C)) =~ s/(^\00*[^\00]+)\00.*/$1/s;
308              
309             # immediately followed by ASCIIZ name, with some trailing 0s
310 343         2223 ($name = substr($buf, length($num) + 0x4C + 1)) =~ s/\00.*//s;
311              
312             # ASCIIZ message, prefixed by 0x20 0x02 16-bit length word
313 343         3465 $msg = substr($buf, length($num) + 0x4C + 1 + length($name) + 1);
314 343         5711 $msg =~ s/^.*\x20\x02..|\00.*$//g;
315            
316 343         1066 $num =~ s/^[^0-9+]+//; # clean leading rubbish from number
317              
318 343         780 my $epoch = substr($buf, 0x24, 4);
319 343         1758 $record{epoch} =
320             0x1000000 * ord(substr($epoch, 0, 1)) +
321             0x10000 * ord(substr($epoch, 1, 1)) +
322             0x100 * ord(substr($epoch, 2, 1)) +
323             ord(substr($epoch, 3, 1)) -
324             2082844800;
325 343         1810 my $dt = DateTime->from_epoch(
326             epoch => $record{epoch},
327             time_zone => $timezone
328             );
329 343         577619 $record{date} = sprintf('%04d-%02d-%02d', $dt->year(), $dt->month(), $dt->day());
330 343         6501 $record{time} = sprintf('%02d:%02d', $dt->hour(), $dt->minute());
331              
332 343 100       5866 if($num eq '') {
333 7         29 delete @record{qw(epoch date time)};
334 7         43 $type = 'unknown';
335             }
336             } else {
337 77         126 $type = 'unknown';
338             }
339 4193 100       51746 $record{debug} = "\n".Data::Hexdumper::hexdump(data => $buf) if($debug);
340 4193         4330775 $record{device} = 'Treo 680';
341 4193         14689 $record{direction} = $dir; # inbound or outbound
342 4193         13303 $record{phone} = $record{number} = $num;
343 4193         18891 $record{timestamp} = $record{epoch};
344 4193         8931 $record{name} = $name;
345 4193         10034 $record{text} = $msg;
346 4193 100       32850 $record{type} = $type eq 'unknown' ? $type : sprintf('0x%04X', $type);
347 4193         20893 return \%record;
348             }
349              
350             =head1 BUGS, LIMITATIONS and FEEDBACK
351              
352             The database structure is undocumented. Consequently it has had to be
353             reverse-engineered. There appear to be several message formats in
354             the database. Some have a superficial resemblance to those used by
355             the 650 (and which is partially documented by Palm) but there is no
356             publicly available documentation that I could find for the others -
357             if you know where I can get docs, please let me know!
358              
359             I can only reverse-engineer record formats that appear on my phone, so
360             there may be some missing. In addition, I may decode some formats
361             incorrectly because they're not quite what I thought they were. If
362             this affects you, please please please send me the offending data.
363              
364             There is currently no support for creating a new database, or for
365             editing the contents of an existing database. If you need that
366             functionality, please submit a patch with tests. I will *not* write
367             this myself unless I need it. Behaviour if you try to create or
368             edit a database is currently undefined, but editing a database will
369             almost certainly break it.
370              
371             If you find any bugs please report them either using
372             L or by email. Ideally, I would like to receive
373             sample data and a test file, which fails with the latest version of
374             the module but will pass when I fix the bug.
375              
376             Sample data can be either in the form of a complete database, or a
377             dump of just a single record structure, which *must* include the
378             raw binary data -
379             use the 'incl_raw' option when you load the module, and save the
380             data structure to a file using Data::Dumper.
381             Feel free to obscure
382             real names, phone numbers, and messages in the data, but you
383             should ensure that phone numbers are correctly formed, and that
384             you don't change the length of any parts of the message. Also,
385             please don't change any non-human-readable parts of the record.
386              
387             =head1 SEE ALSO
388              
389             L, which handles SMS messages databases on some other models
390             of Treo.
391              
392             L
393              
394             L
395              
396             =head1 THANKS TO
397              
398             Michal Seliga, for sample MMS data
399              
400             =head1 AUTHOR, COPYRIGHT and LICENCE
401              
402             Copyright 2008 David Cantrell Edavid@cantrell.org.ukE
403              
404             This software is free-as-in-speech software, and may be used,
405             distributed, and modified under the terms of either the GNU
406             General Public Licence version 2 or the Artistic Licence. It's
407             up to you which one you use. The full text of the licences can
408             be found in the files GPL2.txt and ARTISTIC.txt, respectively.
409              
410             =head1 CONSPIRACY
411              
412             This module is also free-as-in-mason software.
413              
414             =cut
415              
416             1;