File Coverage

blib/lib/Palm/SMS.pm
Criterion Covered Total %
statement 43 117 36.7
branch 8 36 22.2
condition 0 6 0.0
subroutine 7 10 70.0
pod 4 4 100.0
total 62 173 35.8


line stmt bran cond sub pod time code
1             # SMS.pm
2             #
3             # Perl module for reading, manipulating, and writing the .pdb files
4             # used by Handspring SMS application on PalmOS devices such as
5             # Handspring Treo 270.
6             #
7             # This code is provided "as is" with no warranty. The exact terms
8             # under which you may use and (re)distribute it are detailed
9             # in the GNU General Public License, in the file COPYING.
10             #
11             # Copyright (C) 2005 Free Software Foundation, Inc.
12             #
13             # Written by Lorenzo Cappelletti
14             #
15             #
16             # $Id: SMS.pm,v 1.1 2009/01/10 16:17:59 drhyde Exp $
17              
18             package Palm::SMS;
19              
20 1     1   32344 use strict;
  1         3  
  1         46  
21 1     1   7 use warnings;
  1         1  
  1         40  
22 1     1   656 use Palm::Raw();
  1         550  
  1         31  
23 1     1   7 use vars qw($VERSION @ISA @folders);
  1         2  
  1         1599  
24              
25             $VERSION = 0.04;
26              
27             @ISA = qw(Palm::Raw);
28              
29             =head1 NAME
30              
31             Palm::SMS - parse SMS database files
32              
33             =head1 SYNOPSIS
34              
35             use Palm::PDB;
36             use Palm::SMS;
37              
38             my $pdb = new Palm::PDB;
39             $pdb->Load("sms.pdb");
40              
41             my $record = $pdb->{records}[0];
42             print "$record->{text}\n";
43              
44             =head1 DESCRIPTION
45              
46             The SMS PDB handler is a helper class for the Palm::PDB module. It is
47             intended for reading, manipulating, and writing the .pdb files used by
48             Handspring SMS application on PalmOS devices such as Handspring Treo
49             270.
50              
51             Palm::SMS module is the result of a reverse engineering attempt of
52             trasforming a Handspring Treo 270 SMS PDB file into a plain text file.
53             The PDB test file was produced by Handspring's application SMS
54             v. 3.5H.
55              
56             Due to lack of knowledge about how PDB files work and what format SMS
57             database files conform to, at present this module is not suitable for
58             from-scratch SMS generation. Conversely, you may find it extremely
59             useful if you intend to extract SMS messages from merged PDB files and
60             convert them to a human readable form.
61              
62             =head2 Fields
63              
64             $record = $pdb->{records}[N];
65              
66             $record->{name}
67             $record->{firstName}
68             $record->{phone}
69             $record->{folder}
70             $record->{timestamp}
71             $record->{text}
72              
73             $record->{smsh}
74             $record->{unknown1}
75             $record->{unknown2}
76             $record->{unknown3}
77              
78             The fields provided for each record are the following:
79              
80             =over
81              
82             =item name
83              
84             A string containing the name of the person who wrote the message.
85              
86             =item firstname
87              
88             A string containing the first name of the person who wrote the
89             message.
90              
91             =item phone
92              
93             A string containing the phone number.
94              
95             =item timestamp
96              
97             An integer which represents the number of seconds elapsed from Unix
98             epoch to message creation time.
99              
100             It is worth noticing that there is no way of retrieving neither the TZ
101             nor the DST out of data stored in the PDB file. This timestamp always
102             expresses the time of your handheld's clock at message creation time.
103             Hence, I suggest passing the value C as third argument to
104             L's time2str() to get the right timestamp
105             rappresentation:
106              
107             use Date::Format;
108             ...
109             $timestamp = time2str("%T %%Z", $record->{timestamp}, "GMT");
110             $timestamp = time2str($timestamp, $record->{timestamp} );
111              
112             =item folder
113              
114             An integer which represents in which folder the message was stored.
115             English folder names (such as I or I) are available as
116              
117             $Palm::SMS::folders[$record->{folder}];
118              
119             =item text
120              
121             A string containing the message body.
122              
123             =back
124              
125             The module provides additional fields which will probably be less
126             commonly used.
127              
128             =over
129              
130             =item smsh
131              
132             This string of four bytes ("I") is present at the start of each
133             record.
134              
135             =item unknown1
136              
137             =item unknown2
138              
139             =item unknown3
140              
141             These fields contain a chunk of bytes whose function is not yet known.
142             Please, refer to the L method.
143              
144             =back
145              
146             =head2 Fields for the Treo 680
147              
148             The Treo 680 uses different software, and Palm have not documented its
149             message format. Consequently only some information is available, and
150             some records that are extracted are extracted incorrectly. The
151             following fields are available, and have been reverse-engineered from
152             a single sample database. Consequently, you should treat their
153             values with suspicion.
154              
155             Treo 680 databases are read-only.
156              
157             If the Palm::Treo680MessagesDB module is available, then that will be
158             used instead. Over time, that module is intended to do a better job,
159             as and when I figure out new bits of the puzzle.
160              
161             =over
162              
163             =item device
164              
165             This will always be "Treo 680"
166              
167             =item direction
168              
169             The direction of the SMS relative to your phone. This will be either
170             'inbound' or 'outbound'
171              
172             =item number
173              
174             The other party's phone number (same as for any other device)
175              
176             =item name
177              
178             The other party's name (same as for any other device)
179              
180             =item text
181              
182             The text of the message (same as for any other device)
183              
184             =item type
185              
186             A number representing the type of message. If this is 'unknown' then
187             none of the above fields will be populated.
188              
189             =item rawdata
190              
191             The raw binary data of the record
192              
193             =back
194              
195             =head1 METHODS
196              
197             =cut
198              
199             @folders = ( # SMS folder names
200             "Inbox",
201             "Sent",
202             "Pending", # guessed
203             );
204              
205             my $EPOCH_1904 = 2082844800; # Difference between Palm's
206             # epoch (Jan. 1, 1904) and
207             # Unix's epoch (Jan. 1, 1970),
208             # in seconds.
209              
210             sub import {
211 1     1   12 &Palm::PDB::RegisterPDBHandlers(__PACKAGE__,
212             [ "SMS!", "DATA" ],
213             );
214 1         21 &Palm::PDB::RegisterPDBHandlers(__PACKAGE__,
215             [ "HsCh", "SMct" ],
216             );
217 1 50   1   263 eval "use Palm::Treo680MessagesDB" ||
  0         0  
  0         0  
  1         78  
218             &Palm::PDB::RegisterPDBHandlers(__PACKAGE__, # GSM Treo 680
219             [ "MsSt", "MsDb" ], # Messaging app v 2.6.1
220             );
221             }
222              
223             =head2 new
224              
225             $pdb = new Palm::SMS;
226              
227             Creates a new PDB, initialized with the various Palm::SMS fields
228             and an empty record list.
229              
230             Use this method if you're creating a SMS PDB from scratch.
231              
232             =cut
233              
234             #'
235             sub new {
236 0     0 1 0 my $classname = shift;
237 0         0 my $self = $classname->SUPER::new(@_); # no need to rebless
238 0         0 $self->{name} = "SMS Messages"; # default
239 0         0 $self->{creator} = "SMS!";
240 0         0 $self->{type} = "DATA";
241 0         0 $self->{attributes}{resource} = 0; # not a resource db
242              
243              
244 0         0 $self->{sort} = undef; # empty sort block
245              
246 0         0 $self->{records} = []; # empty list of records
247              
248 0         0 return $self;
249             }
250              
251             =head2 new_Record
252              
253             $record = $pdb->new_Record;
254              
255             $record->{phone} = "1234567890";
256             $record->{folder} = 1;
257             ...
258              
259             $phone = $record->{phone};
260             $folder = $Palm::SMS::folders[$record->{folder}];
261             ...
262              
263             Creates a new SMS record, with blank values for all of the fields.
264              
265             C does B add the new record to C<$pdb>. For that,
266             you want C<$pdb-Eappend_Record>.
267              
268             Default field values are:
269              
270             name : undef
271             firstName: undef
272             phone : undef
273             timestamp: localtime()
274             folder : 1
275             text : undef
276              
277             smsh : "SMSh"
278             unknown1 : undef
279             unknown2 : undef
280             unknown3 : undef
281              
282             =cut
283              
284             sub new_Record {
285 0     0 1 0 my $classname = shift;
286 0         0 my $retval = $classname->SUPER::new_Record(@_);
287              
288 0         0 $retval->{name} = undef;
289 0         0 $retval->{firstName} = undef;
290 0         0 $retval->{phone} = undef;
291 0         0 $retval->{timestamp} = localtime;
292 0         0 $retval->{folder} = 1;
293 0         0 $retval->{text} = undef;
294              
295 0         0 $retval->{smsh} = "SMSh";
296 0         0 $retval->{unknown1} = undef;
297 0         0 $retval->{unknown2} = undef;
298 0         0 $retval->{unknown2} = undef;
299              
300 0         0 return $retval;
301             }
302              
303             =head2 ParseRecord
304              
305             ParseRecord() returns a parsed representation of the record, typically
306             as a reference to a record object or anonymous hash. It is
307             automatically called from within L and, as such, is not
308             intented to be used directly from applications.
309              
310             The record structure which an SMS posses is:
311              
312             smsh : 4-byte ASCII string
313             unknown1 : 2-byte data whose function is unknown
314             timestamp: 32-bit, big-endian, unsigned integer rappresenting
315             the number of seconds since 1904
316             unknown2 : 26-byte data whose function is unknown
317             phone : Null terminated string
318             name : Null terminated string
319             firstname: Null terminated string
320             unknown3 : 16-byte data whose function is unknown
321             text : Null terminated (sent messages only) string
322              
323             I field value is copied from I field which is
324             computed by Palm::PDB and then delted since there is no application
325             info block (see L) in the PDB file.
326              
327             I is empty for messages belonging to category 1 (folder
328             I).
329              
330             It is worth noticing that length, offset, and even availability of
331             I data are not preserved between module version when their
332             meaning becomes clear.
333              
334             =cut
335              
336             sub ParseRecord {
337 2     2 1 1266 my $self = shift;
338 2         8 my %record = @_;
339 2         3 my @unpack;
340              
341             my $smsh; # each record starts with "SMSh": SMS handler?
342             # not on Treo 680
343 0         0 my $unknown1;
344 0         0 my $timestamp;
345 0         0 my $unknown2;
346 0         0 my $name;
347 0         0 my $firstName;
348 0         0 my $unknown3;
349 0         0 my $phone;
350 0         0 my $folder;
351 0         0 my $text;
352              
353 2 50       16 if ($self->{creator} eq "HsCh") {
    50          
    100          
    50          
    0          
354 0         0 ($smsh,
355             $unknown1,
356             $timestamp,
357             $unknown2,
358             $text,
359             ) = unpack("a2 A4 N a24 Z* a*", $record{data});
360 0 0       0 if ($timestamp eq "") {$timestamp=$EPOCH_1904;}
  0         0  
  0         0  
361             else {$timestamp -= 14400;}
362 0 0       0 if ($smsh eq "\0\0" ) { $phone="Target"; }
  0         0  
363 0         0 else { $phone="Me"; }
364             } elsif($self->{creator} eq 'MsSt') { # Treo 680
365 0         0 my $buf = $record{data};
366 0         0 my $type = 256 * ord(substr($buf, 10, 1)) + ord(substr($buf, 11, 1));
367 0         0 my($dir, $num, $name, $msg) = ('', '', '', '');
368 0 0 0     0 if($type == 0x400C || $type == 0x4009) { # 4009 not used by 680?
    0          
    0          
369 0 0       0 $dir = ($type == 0x400C) ? 'inbound' : 'outbound';
370 0         0 ($num, $name, $msg) = (split(/\00+/, substr($buf, 34)))[0, 1, 3];
371 0         0 $msg = substr($msg, 1);
372             } elsif($type == 0) {
373 0         0 $dir = 'outbound';
374 0         0 ($num, $name, $msg) = split(/\00+/, substr($buf, 0x4C), 3);
375 0         0 $msg =~ s/^.{9}//s;
376 0         0 $msg =~ s/\00.*$//s;
377             } elsif($type == 0x0002) {
378 0         0 $dir = 'outbound';
379 0         0 ($num, $name, $msg) = split(/\00+/, substr($buf, 0x46), 3);
380 0         0 $msg =~ s/^.Trsm....//s;
381 0         0 $msg =~ s/\00.*$//s;
382             } else {
383 0         0 $type = 'unknown';
384             }
385 0         0 @record{qw(device type direction phone name text rawdata)} =
386             ("Treo 680", $type, $dir, $num, $name, $msg, $buf);
387             } elsif ($record{category} == 0) {
388             ### Inbox folder ###
389 1         2 my $nameFlag; # whether name and firstName are available
390             my $extra; # temporary string
391              
392 1         6 ($smsh,
393             $unknown1,
394             $timestamp,
395             $unknown2,
396             $phone,
397             $extra,
398             ) = unpack("A4 a2 N a26 Z* a*", $record{data});
399              
400             # unknown2 tells whether name and firstName are available
401 1         4 $nameFlag = unpack("x7 H", $unknown2);
402 1 50       4 if ($nameFlag eq "4") {
403 1         5 ($name,
404             $firstName,
405             $extra,
406             ) = unpack("Z* Z* a*", $extra);
407             }
408              
409             # $extra's head contains unknown3 followed by "\d\0"
410 1         12 ($unknown3, $text) = $extra =~ m/(.*?\d\0)([^\0]+)$/;
411              
412             } elsif ($record{category} == 1) {
413             ### Sent folder ###
414 1         2 my $unpack;
415              
416 1         10 ($smsh,
417             $unknown1,
418             $timestamp,
419             $unknown2,
420             $phone,
421             $name,
422             $firstName,
423             $text,
424             ) = unpack("A4 a2 N a26 Z* Z* Z* Z*", $record{data});
425 1         3 $unknown3 = "";
426              
427             } elsif ($record{category} == 2) {
428             ### Pending folder ###
429 0         0 die "Never tried to parse a message from Pending folder";
430              
431             } else {
432 0         0 die "Unknown category";
433              
434             }
435              
436             # Work out common extracted values
437 2         4 $timestamp -= $EPOCH_1904;
438              
439             # Assign extracted values to record
440 2 50       7 if($self->{creator} ne 'MsSt') {
441 2         4 $record{name} = $name;
442 2         4 $record{firstName} = $firstName;
443 2         5 $record{phone} = $phone;
444 2         3 $record{timestamp} = $timestamp;
445 2         3 $record{folder} = $record{category};
446 2         4 $record{text} = $text;
447              
448 2         4 $record{smsh} = $smsh;
449 2         2 $record{unknown1} = $unknown1;
450 2         4 $record{unknown2} = $unknown2;
451 2         4 $record{unknown3} = $unknown3;
452             }
453              
454 2         4 delete $record{data};
455              
456 2         7 return \%record;
457             }
458              
459             =head2 PackRecord
460              
461             This is the converse of L. PackRecord()
462             takes a record as returned by ParseRecord() and returns a string of
463             raw data that can be written to the database file. As
464             L, this function is not intended to be
465             used directly from applications.
466              
467             Because there are chunk of record data whose function is unknown (see
468             L), this method may produce an invalid
469             result, expecially when passed record was created from scratch via
470             L.
471              
472             This method is granted to work if the record being packed has been
473             unpacked from an existing PDB and no information has been added.
474              
475             =cut
476              
477             sub PackRecord {
478 0     0 1   my $self = shift;
479 0           my $record = shift;
480 0           my $retval;
481             my $pack;
482              
483 0           $pack = "A4 a2 N a26 Z*";
484              
485 0 0         if ($record->{folder} == 0) {
    0          
    0          
486             ### Inbox folder ###
487 0 0 0       if (not ($record->{name} or $record->{firstName})) {
488 0           $pack .= " a* A*";
489 0           $retval = pack($pack,
490             $record->{smsh},
491             $record->{unknown1},
492             $record->{timestamp} + $EPOCH_1904,
493             $record->{unknown2},
494             $record->{phone},
495             $record->{unknown3},
496             $record->{text});
497              
498             } else {
499 0           $pack .= " Z* Z* a* A*";
500 0           $retval = pack($pack,
501             $record->{smsh},
502             $record->{unknown1},
503             $record->{timestamp} + $EPOCH_1904,
504             $record->{unknown2},
505             $record->{phone},
506             $record->{name},
507             $record->{firstName},
508             $record->{unknown3},
509             $record->{text});
510              
511             }
512              
513             } elsif ($record->{folder} ==1) {
514             ### Sent folder ###
515 0           $pack .= " Z* Z* Z*";
516 0           $retval = pack($pack,
517             $record->{smsh},
518             $record->{unknown1},
519             $record->{timestamp} + $EPOCH_1904,
520             $record->{unknown2},
521             $record->{phone},
522             $record->{name},
523             $record->{firstName},
524             $record->{text});
525              
526             } elsif ($record->{folder} == 2) {
527             ### Pending folder ###
528 0           die "Never tried to pack a message to Pending folder";
529              
530             } else {
531 0           die "Unknown category";
532              
533             }
534              
535 0           return $retval;
536             }
537              
538             1;
539             __END__