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