File Coverage

blib/lib/Acme/PIA/Export.pm
Criterion Covered Total %
statement 3 130 2.3
branch 0 62 0.0
condition 0 14 0.0
subroutine 1 10 10.0
pod 5 9 55.5
total 9 225 4.0


line stmt bran cond sub pod time code
1             package Acme::PIA::Export;
2            
3             our $VERSION = "0.019";
4            
5 1     1   32538 use IO::Socket;
  1         36912  
  1         7  
6            
7             =pod
8            
9             =head1 NAME
10            
11             Acme::PIA::Export - Export contacts, calendars or todos from Arcor's PIA messaging
12            
13             =head1 DESCRIPTION
14            
15             This module is intended to help export data from the PIA messaging web application
16             that comes with the free Arcor mail account at www.arcor.de
17            
18             It lets the user retrieve his data in CSV or XLS (not yet implemented) format or
19             as hashes.
20            
21             If you don't know what PIA is, you will most probably not need this module.
22            
23             =head2 EXAMPLE
24            
25             use Acme::PIA::Export;
26            
27             my $pia = Acme::PIA::Export->new(
28            
29             "username" => "mylogin",
30             "password" => "verysecret"
31            
32             );
33            
34             $pia->export( "contacts" );
35            
36             foreach my $contact ( $pia->entries() ) {
37            
38             print "$contact->{NAME}, $contact->{VORNAME}\n";
39            
40             }
41            
42             $pia->export_csv( file => "C:/my/piacontacts.csv" );
43            
44             =head2 FUNCTIONS
45            
46             =over
47            
48             =item new( [ key => value, ...] )
49            
50             Creates and returns a new Acme::PIA::Export object.
51            
52             Parameters are given as key => value pairs. The most commonly used are "username" and "password".
53             If you expirience problems you can also give the parameter "DEBUG" => 1
54             to get verbose output from all functions.
55            
56             =item export( SCOPE )
57            
58             Export all objects for the given scope (contacts, calendar etc.) and stores them in the
59             Acme::PIA::Export object.
60            
61             ATTENTION: Only "contacts" scope is implemented up to now! Look out for future releases.
62            
63             =item entries()
64            
65             Retrieve a list with all entries as hashes.
66            
67             =item entries_csv( [key => value, ...] ) [NOT YET IMPLEMENTED]
68            
69             Retrieve entries in csv format. Without arguments they are returned as a list of lines, including
70             column headers as first row. The column headers can be turned off by setting the option 'headers => 0'.
71            
72             If the parameter 'file => "/path/to/file.csv"' is given, then the output is saved to the given file
73             directly and the number of rows written (excluding the column headers) is returned.
74            
75             TODO: In future releases, there will be the option to pass along a "fields" paramter as a reference
76             to an array that holds the names of the columns to be exported.
77            
78             =item entries_xls( file => /path/to/file.xls [, key => value, ...] ) [NOT YET IMPLEMENTED]
79            
80             Retrieve entries in xls format.
81            
82             The parameter 'file => "/path/to/file.xls"' is mandatory. The output is saved to the given file
83             directly and the number of rows written (excluding the column headers) is returned.
84            
85             The column headers can be turned off by setting the option 'headers => 0'.
86            
87             TODO: In future releases, there will be the option to pass along a "fields" paramter as a reference
88             to an array that holds the names of the columns to be exported.
89            
90             =item fields( [SCOPE] )
91            
92             Retrieve a list with all the column names. If you already did an export(), you can call fields() without
93             parameters to get the column names for the type of the last export. Otherwise, pass the name of the scope
94             as a string.
95            
96             You can rely on that the order of column names is the same as the field order returned by the entries_XXX
97             methods.
98            
99             =back
100            
101             =head1 AUTHOR
102            
103             Christian Winter
104            
105             =head1 LICENSE
106            
107             This peace of code is licensed under the same terms as Perl itself.
108             You should have received a copy of this license together with your
109             Perl version. You can read it at http://www.perl.org or by typing
110             "perldoc L" or "perldoc L"
111            
112             =head1 BUGS
113            
114             Please report those to the author.
115            
116             =cut
117            
118             ###########################################################################################
119             #
120             # END OF POD DOCUMENTATION
121             #
122             ###########################################################################################
123            
124            
125            
126             my $server = "www.arcor.de";
127             my $query_url = "http://$server/office/sync/servlet/Exchange";
128            
129             our %fields = ( "contacts" => {
130             "CLIENT" => 2,
131             "ID" => 5,
132             "CURRDATE" => 6,
133             "VORNAME" => 11,
134             "NAME" => 13,
135             "FIRMA" => 16,
136             "STRASSE_BUSI" => 17,
137             "ORT_BUSI" => 18,
138             "NULL" => 19,
139             "PLZ_BUSI" => 20,
140             "LAND_BUSI" => 21,
141             "STRASSE_PRVT" => 22,
142             "ORT_PRVT" => 23,
143             "PLZ_PRVT" => 25,
144             "LAND_PRVT" => 26,
145             "TEL_BUSI" => 33,
146             "MOBIL_BUSI" => 34,
147             "FAX_BUSI" => 35,
148             "TEL_PRVT" => 39,
149             "FAX_PRVT" => 41,
150             "MOBIL_PRVT" => 43,
151             "MESSENGER" => 46,
152             "MAIL_PRVT" => 51,
153             "MAIL_BUSI" => 52,
154             "HOMEPAGE_PRVT" => 54,
155             "HOMEPAGE_BUSI" => 55,
156             "NICKNAME_PRVT" => 62,
157             "LAST_MODIFIED" => 71
158             },
159             "calendar" => {
160             }
161             );
162            
163            
164             our %ordered_fields = (
165             "contacts" => [
166             "CLIENT" ,
167             "ID" ,
168             "CURRDATE" ,
169             "VORNAME" ,
170             "NAME" ,
171             "FIRMA" ,
172             "STRASSE_BUSI" ,
173             "ORT_BUSI" ,
174             "NULL" ,
175             "PLZ_BUSI" ,
176             "LAND_BUSI" ,
177             "STRASSE_PRVT" ,
178             "ORT_PRVT" ,
179             "PLZ_PRVT" ,
180             "LAND_PRVT" ,
181             "TEL_BUSI" ,
182             "MOBIL_BUSI" ,
183             "FAX_BUSI" ,
184             "TEL_PRVT" ,
185             "FAX_PRVT" ,
186             "MOBIL_PRVT" ,
187             "MESSENGER" ,
188             "MAIL_PRVT" ,
189             "MAIL_BUSI" ,
190             "HOMEPAGE_PRVT" ,
191             "HOMEPAGE_BUSI" ,
192             "NICKNAME_PRVT" ,
193             "LAST_MODIFIED"
194             ],
195             "calendar" => [
196             ]
197             );
198            
199            
200             our %scopes = (
201             "contacts" => "contacts",
202             "calendar" => "calendar"
203             );
204            
205             sub new {
206 0     0 1   my $self = {};
207 0           my $class = shift;
208 0   0       bless $self, ref $class || $class;
209 0 0         if( @_ ) {
210 0           my %cfg = @_;
211 0           foreach( keys %cfg ) {
212 0           $self->{"cfg"}->{$_} = $cfg{$_};
213             }
214             }
215 0           $self;
216             }
217            
218            
219             sub do_connect {
220 0     0 0   my $self = shift;
221 0           my $sock = new IO::Socket::INET(
222             PeerAddr => $server,
223             PeerPort => 80,
224             Proto => 'TCP'
225             );
226 0 0         die "Unable to connect to $server:80 (Error: $!)" unless( $sock );
227 0           $sock->autoflush(1);
228 0           $self->{"sock"} = $sock;
229 0           $self;
230             }
231            
232            
233             sub export {
234 0     0 1   my $self = shift;
235 0   0       my $what = shift || "contacts";
236 0 0         unless( $scopes{$what} ) {
237 0           die "No such scope to export: $what";
238             }
239 0           $self->do_connect();
240 0           $self->send_request($what);
241 0           $self->get_response($what);
242 0           my $sock = $self->{"sock"};
243 0           $sock->close();
244             }
245            
246             sub send_request {
247 0     0 0   my $self = shift;
248 0           my $what = shift;
249 0 0         if( ! $self->{"cfg"}->{"username"} ) {
250 0           die "No Username given!";
251             }
252 0 0         if( ! $self->{"cfg"}->{"password"} ) {
253 0           die "No Password given!";
254             }
255 0 0         if( ! $self->{"cfg"}->{"client"} ) {
256 0   0       $self->{"cfg"}->{"client"} = uc($ENV{"hostname"}) || sprintf("%s-%0.5i", "Acme-PIA-Export", rand(99999));
257             }
258 0           my $requestbody = "$self->{cfg}->{username}~;~$self->{cfg}->{password}~;~$self->{cfg}->{client}~;~$scopes{$what};~export~;~O~;~~#~";
259 0           my $content_length = length($requestbody);
260            
261 0           my $request = "POST $query_url HTTP/1.1\n" .
262             "Pragma: no-cache\n" .
263             "Host: www.arcor.de\n" .
264             "Accept-Ranges: bytes\n" .
265             "Content-Type: text/html\n" .
266             "Content-Length: $content_length\n" .
267             "\n" .
268             $requestbody;
269 0 0         if( $self->{"cfg"}->{"DEBUG"} ) {
270 0           print "Sending request:$/$request$/-------------------------------$/";
271             }
272 0           my $sock = $self->{"sock"};
273 0           my $res = print $sock $request;
274 0 0         die "Failed writing to Socket on $server:80 (Error: $!)" unless( $res );
275             }
276            
277            
278            
279             sub get_response {
280 0     0 0   my $self = shift;
281 0           my $what = shift;
282            
283 0           my $head = 1;
284 0           my $return_head;
285             my $return_body;
286 0           my $bodysize;
287 0           my $read_chunked = 1;
288            
289 0           my $sock = $self->{"sock"};
290            
291 0           LOOP: while( my $line = readline($sock) ) {
292 0           (my $debugline = $line) =~ s/\r|\n//smg;
293 0 0         print "Reading line from socket: $debugline$/" if( $self->{"cfg"}->{"DEBUG"} );
294 0 0 0       if( $head && $line eq "\r\n" ) {
295 0           $head = 0;
296 0           last LOOP;
297             } else {
298 0           $return_head .= $line;
299             }
300             }
301 0           $self->{"data"}->{"head"} = $return_head;
302 0 0         if( $return_head =~ /Content-Length:\s(\d+)/sm ) {
    0          
303 0           $bodysize = $1;
304             } elsif( $return_head =~ /Transfer-Encoding:.chunked/sm ) {
305 0           $read_chunked = 1;
306             } else {
307 0           die "Unable to parse Content-Length while chunked encoding not used in return header:\n$return_head";
308             }
309 0 0         if( $read_chunked ) {
310 0           while( my $size = readline( $sock ) ) {
311 0           $size =~ s/\r|\n//gsm;
312 0           $size = hex($size);
313 0 0         print "Reading Chunk of $size Bytes\n" if( $self->{"cfg"}->{"DEBUG"} );
314 0 0         read($sock,my $return_buffer,$size) or last;
315 0           $return_body .= $return_buffer;
316 0           readline($sock);
317             }
318             } else {
319 0 0         unless( read( $sock, $return_body, $bodysize ) ) {
320 0 0         if( defined( $return_body ) ) {
321 0           die "Unexpected end of input reading from socket $server:80!";
322             } else {
323 0           die "Error reading return data from socket $server:80 (Error: $!)";
324             }
325             }
326             }
327 0           my @rows = split /\r?\n/, $return_body;
328 0 0         print "Got back " . scalar(@rows) . " rows of data$/" if( $self->{"cfg"}->{"DEBUG"} );
329 0           $self->{"data"}->{"scope"} = $what;
330 0           $self->{"data"}->{"rows"} = \@rows;
331 0           $self->{"data"}->{"entries"} = ();
332 0           foreach my $entry ( @rows ) {
333 0 0         print "Processing entry $entry$/" if( $self->{"cfg"}->{"DEBUG"} );
334 0           push @{$self->{"data"}->{"entries"}}, $self->parseentry( $entry, $what );
  0            
335             }
336 0           return scalar( @rows );
337             }
338            
339             sub entries {
340 0     0 1   my $self = shift;
341 0           return @{$self->{"data"}->{"entries"}};
  0            
342             }
343            
344             sub fields {
345 0     0 1   my $self = shift;
346 0 0         my $what = (@_)?shift:$self->{"data"}->{"scope"};
347            
348 0 0         die "No scope configured. Either pass as parameter or invoke fields() after a successful export." unless( $what );
349 0 0         die "No such scope. Please check your spelling." unless( $ordered_fields{$what} );
350            
351 0           return @{$ordered_fields{$what}};
  0            
352             }
353            
354             sub parseentry {
355 0     0 0   my $self = shift;
356 0           chomp(my $row = shift);
357 0           my $what = shift;
358 0 0         print "Parsing entry of type $what$/" if( $self->{"cfg"}->{"DEBUG"} );
359 0           my %entry;
360 0           $row =~ s/\r//;
361 0           my @values = split /~;~/, $row;
362 0           foreach( @{$ordered_fields{$what}} ) {
  0            
363 0 0         print "Processing field $_ for type $what$/" if( $self->{"cfg"}->{"DEBUG"} );
364 0           my $val = $values[$fields{$what}->{$_}];
365 0 0         $entry{$_} = ($val ne "NULL")?$val:"";
366             }
367 0           return \%entry;
368             }
369            
370             sub entries_csv {
371 0     0 1   my $self = shift;
372 0 0         my %parms = (scalar @_)?@_:();
373 0           my $row0 = "";
374 0           my @result;
375 0 0 0       unless( defined($parms{"header"}) && $parms{"header"} == 0 ) {
376 0           $row0 = join ";", @{$ordered_fields{$self->{"data"}->{"scope"}}};
  0            
377             }
378 0 0         if( $parms{"file"} ) {
379 0 0         open( O, "> $parms{file}" ) or die $!;
380 0           print O $row0.$/;
381             } else {
382 0           push @result, $row0;
383             }
384 0           my $count = 0;
385 0           foreach my $entry ( $self->entries() ) {
386 0           $count++;
387 0           my @row;
388 0           foreach my $field ( @{$ordered_fields{$self->{"data"}->{"scope"}}} ) {
  0            
389 0           push @row, $entry->{$field};
390             }
391 0 0         if( $parms{"file"} ) {
392 0           print O join(";", @row).$/;
393             } else {
394 0           push @result, join(";", @row);
395             }
396             }
397 0 0         if( $parms{"file"} ) {
398 0           close O;
399 0           return $count;
400             }
401 0           return @result;
402             }
403            
404             1;