File Coverage

blib/lib/Net/Eboks.pm
Criterion Covered Total %
statement 38 207 18.3
branch 0 72 0.0
condition 0 41 0.0
subroutine 13 38 34.2
pod 0 18 0.0
total 51 376 13.5


line stmt bran cond sub pod time code
1             package Net::Eboks;
2              
3 1     1   759 use 5.010;
  1         4  
4 1     1   4 use strict;
  1         2  
  1         19  
5 1     1   10 use warnings;
  1         2  
  1         32  
6 1     1   578 use Encode qw(encode decode);
  1         8186  
  1         73  
7 1     1   955 use DateTime;
  1         402332  
  1         78  
8 1     1   670 use HTTP::Request;
  1         14919  
  1         38  
9 1     1   705 use Digest::SHA qw(sha256_hex);
  1         2669  
  1         83  
10 1     1   874 use XML::Simple;
  1         7453  
  1         7  
11 1     1   815 use LWP::UserAgent;
  1         18951  
  1         38  
12 1     1   571 use LWP::ConnCache;
  1         1052  
  1         32  
13 1     1   784 use MIME::Entity;
  1         81929  
  1         42  
14 1     1   841 use IO::Lambda qw(:all);
  1         11545  
  1         281  
15 1     1   594 use IO::Lambda::HTTP qw(http_request);
  1         20344  
  1         4194  
16              
17              
18             our $VERSION = '0.04';
19              
20             sub new
21             {
22 0     0 0   my ( $class, %opts ) = @_;
23 0           my $self = bless {
24             cpr => '0000000000',
25             password => '',
26             activation => '',
27             country => 'DK',
28             type => 'P',
29             deviceid => 'DEADBEEF-1337-1337-1337-000000000000',
30             datetime => DateTime->now->strftime('%Y-%m-%d %H:%M:%SZ'),
31             root => 'rest.e-boks.dk',
32              
33             nonce => '',
34             sessionid => '',
35             response => "3a1a51f235a8bd6bbc29b2caef986a1aeb77018d60ffdad9c5e31117e7b6ead3", # XXX
36             uid => undef,
37             uname => undef,
38             conn_cache => LWP::ConnCache->new,
39              
40             %opts,
41             }, $class;
42              
43 0           $self->{challenge} = sha256_hex(sha256_hex(join(':', @{$self}{qw(activation deviceid type cpr country password datetime)})));
  0            
44              
45 0           return $self;
46             }
47              
48             sub response
49             {
50 0     0 0   my ($self, $decode, $response) = @_;
51              
52 0 0         unless ($response->is_success) {
53 0   0       my $sl = $response->message // $response-> status_line;
54 0           chomp $sl;
55 0           $sl =~ s/\+/ /g;
56 0           return undef, $sl;
57             }
58            
59 0           for ( split /,\s*/, $response->header('x-eboks-authenticate')) {
60 0 0         warn "bad x-eboks-authenticate: $_\n" unless m/^(sessionid|nonce)="(.*?)"$/;
61 0           $self->{$1} = $2;
62             }
63            
64 0 0         return $response->decoded_content unless $decode;
65            
66 0 0         my %options = ref($decode) ? %$decode : ();
67 0           my $content = $response->decoded_content;
68 0 0 0       if ( $content !~ /[^\x00-\xff]/ && $content =~ /[\x80-\xff]/ ) {
69             # try to upgrade
70 0           eval {
71 0           my $c = decode('latin1', $content);
72 0           $content = $c;
73             };
74             }
75 0           my $xml = XMLin($content, ForceArray => 1, %options);
76 0 0 0       if ( $xml && ref($xml) eq 'HASH' ) {
77 0           return $xml;
78             } else {
79 0           return undef, "xml returned is not a hash";
80             }
81             }
82              
83             sub login
84             {
85 0     0 0   my $self = shift;
86              
87 0 0         return undef if defined $self->{uid};
88              
89 0           my $authstr = 'logon ' . join(',', map { "$_=\"$self->{$_}\"" } qw(deviceid datetime challenge));
  0            
90 0           my $content = <<XML;
91             <Logon xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:eboks:mobile:1.0.0">
92             <User identity="$self->{cpr}" identityType="$self->{type}" nationality="$self->{country}" pincode="$self->{password}"/>
93             </Logon>
94             XML
95              
96             my $login = HTTP::Request->new(
97             'PUT',
98             'https://' . $self->{root} . '/mobile/1/xml.svc/en-gb/session',
99             [
100             'Content-Type' => 'application/xml',
101             'Content-Length' => length($content),
102             'X-EBOKS-AUTHENTICATE' => $authstr,
103             'Accept' => '*/*',
104             'Accept-Language' => 'en-US',
105             'Accept-Encoding' => 'gzip,deflate',
106             'Host' => $self->{root},
107 0           ],
108             $content
109             );
110 0           $login->protocol('HTTP/1.1');
111              
112             return $login, sub {
113 0     0     my ($xml, $error) = $self-> response({ForceArray => 0}, @_);
114 0 0         return $xml, $error unless $xml;
115 0 0         return undef, "'User' is not present in response" unless exists $xml->{User};
116              
117 0           $self->{uid} = $xml->{User}->{userId};
118 0           $self->{uname} = $xml->{User}->{name};
119 0           return $self->{uname};
120 0           };
121             }
122              
123 0     0 0   sub ua { LWP::UserAgent->new(conn_cache => shift->{conn_cache}) }
124              
125             sub get
126             {
127 0     0 0   my ($self, $path) = @_;
128 0           my $authstr = join(',', map { "$_=\"$self->{$_}\"" } qw(deviceid nonce sessionid response));
  0            
129             my $get = HTTP::Request->new(
130             'GET',
131             'https://' . $self->{root} . '/' . $path,
132             [
133             'X-EBOKS-AUTHENTICATE' => $authstr,
134             'Accept' => '*/*',
135             'Accept-Language' => 'en-US',
136             'Host' => $self->{root},
137 0           ],
138             );
139 0           $get->protocol('HTTP/1.1');
140 0           return $get;
141             }
142              
143             sub xmlget
144             {
145 0     0 0   my ( $self, $uri, $path, %xmlopt ) = @_;
146             return
147             $self->get($uri), sub {
148 0     0     my ($xml, $error) = $self-> response(\%xmlopt, @_);
149 0 0         return $xml, $error unless $xml;
150 0   0       for my $step ( @{ $path // [] } ) {
  0            
151 0 0         return undef, "key '$step' not found" unless ref $xml;
152 0 0         if ( ref($xml) eq 'ARRAY') {
153 0           $xml = $xml->[$step];
154             } else {
155 0           $xml = $xml->{$step};
156             }
157             }
158              
159 0   0       my $key = $xmlopt{KeyAttr} // 'name';
160 0           while ( my ( $k, $v ) = each %$xml ) {
161 0 0 0       $v->{$key} = $k if defined($v) && ref($v) eq 'HASH';
162             }
163              
164 0           return $xml;
165 0           };
166             }
167              
168             sub folders
169             {
170 0     0 0   my $self = shift;
171 0 0         return undef unless $self->{uid};
172 0           $self-> xmlget("/mobile/1/xml.svc/en-gb/$self->{uid}/0/mail/folders", ['FolderInfo']);
173             }
174              
175             sub messages
176             {
177 0     0 0   my ($self, $folder_id, $offset, $limit) = @_;
178 0 0         return undef unless $self->{uid};
179 0   0       $limit //= 1;
180 0   0       $offset //= 0;
181 0           $self-> xmlget(
182             "/mobile/1/xml.svc/en-gb/$self->{uid}/0/mail/folder/$folder_id?skip=$offset&take=$limit",
183             [ qw(Messages 0 MessageInfo) ],
184             KeyAttr => 'id'
185             );
186             }
187              
188             sub message
189             {
190 0     0 0   my ($self, $folder_id, $message_id) = @_;
191 0 0         return undef unless $self->{uid};
192 0           $self-> xmlget(
193             "/mobile/1/xml.svc/en-gb/$self->{uid}/0/mail/folder/$folder_id/message/$message_id",
194             [],
195             KeyAttr => 'id'
196             );
197             }
198              
199             sub content
200             {
201 0     0 0   my ( $self, $folder_id, $content_id ) = @_;
202             return
203             $self-> get( "/mobile/1/xml.svc/en-gb/$self->{uid}/0/mail/folder/$folder_id/message/$content_id/content" ), sub {
204 0     0     $self-> response( 0, @_ )
205 0           };
206             }
207              
208 0     0 0   sub attachments { $_[1]->{Attachements}->[0]->{AttachmentInfo} }
209              
210             sub filename {
211 0     0 0   my $fn = $_[1]-> {name};
212 0           $fn =~ s[:\\\/][_];
213 0           my $fmt = lc($_[1]->{format});
214 0 0         $fmt = 'txt' if $fmt eq 'plain';
215             return $fn . '.' .lc($_[1]->{format})
216 0           }
217              
218             sub mime_type
219             {
220 0     0 0   my $fmt = lc $_[1]->{format};
221 0 0         if ( $fmt =~ /^(pdf)$/ ) {
    0          
    0          
222 0           return "application/$fmt";
223             } elsif ( $fmt =~ /^(gif|jpg|jpeg|tiff|png|webp)$/) {
224 0           return "image/$fmt";
225             } elsif ( $fmt =~ /^(txt|text|html|plain)$/) {
226 0 0         $fmt = 'plain' if $fmt =~ /^(txt|text)$/;
227 0           return "text/$fmt";
228             } else {
229 0           return "application/$fmt";
230             }
231             }
232              
233             sub first_value
234             {
235 0     0 0   my ($self, $entry) = @_;
236 0 0         if ( ref($entry) eq 'HASH') {
    0          
237 0           my $k = (sort keys %$entry)[0];
238 0           return $entry->{$k};
239             } elsif ( ref($entry) eq 'ARRAY') {
240 0           return $entry->[0];
241             } else {
242 0           return "bad entry";
243             }
244             }
245              
246             sub assemble_mail
247             {
248 0     0 0   my ( $self, %opt ) = @_;
249              
250 0           my $msg = $opt{message};
251 0           my $sender = $self->first_value($msg->{Sender});
252 0 0         $sender = $sender->{content} if ref($sender) eq 'HASH';
253 0   0       $sender //= 'unknown';
254              
255 0   0       my $received = $msg->{receivedDateTime} // '';
256 0           my $date;
257 0 0         if ( $received =~ /^(\d{4})-(\d{2})-(\d{2})T(\d\d):(\d\d):(\d\d)/) {
258 0           $date = DateTime->new(
259             year => $1,
260             month => $2,
261             day => $3,
262             hour => $4,
263             minute => $5,
264             second => $6,
265             );
266             } else {
267 0           $date = DateTime->now;
268             }
269 0           $received = $date->strftime('%a, %d %b %Y %H:%M:%S %z');
270              
271             my $mail = MIME::Entity->build(
272             From => $opt{from} // ( encode('MIME-Q', $sender) . ' <noreply@e-boks.dk>' ) ,
273             To => $opt{to} // ( encode('MIME-Q', $self->{uname}) . ' <' . ( $ENV{USER} // 'you' ) . '@localhost>' ),
274             Subject => $opt{subject} // encode('MIME-Header', $msg->{name}),
275             Data => $opt{data} // encode('utf-8', "Mail from $sender"),
276 0   0       Date => $opt{date} // $received,
      0        
      0        
      0        
      0        
      0        
277             Charset => 'utf-8',
278             Encoding => 'quoted-printable',
279             'X-Net-Eboks' => "v/$VERSION",
280             );
281              
282 0           my @attachments;
283 0 0         push @attachments, [ $msg, $opt{body} ] if exists $opt{body};
284              
285 0           my $attachments = $self->attachments($msg);
286 0           for my $att_id ( sort keys %$attachments ) {
287 0           push @attachments, [ $attachments->{$att_id}, $opt{attachments}->{$att_id} ];
288             }
289              
290 0           for ( @attachments ) {
291 0           my ( $msg, $body ) = @$_;
292 0           my $fn = $self->filename($msg);
293 0           my $entity = $mail->attach(
294             Type => $self->mime_type($msg),
295             Encoding => 'base64',
296             Data => $body,
297             Filename => $fn,
298             );
299              
300             # XXX hack filename for utf8
301 0 0         next unless $fn =~ m/[^\x00-\x80]/;
302 0           $fn = Encode::encode('utf-8', $fn);
303 0           $fn =~ s/([^A-Za-z])/'%'.sprintf("%02x",ord($1))/ge;
  0            
304 0           for ( 'Content-disposition', 'Content-type') {
305 0           my $v = $entity->head->get($_);
306 0           $v =~ s/name="(.*)"/name*=.utf-8''$fn/;
307 0           $entity->head->replace($_, $v);
308             }
309             }
310              
311             return
312 0           'From noreply@localhost ' .
313             $date->strftime('%a %b %d %H:%M:%S %Y') . "\n" .
314             $mail->stringify
315             ;
316             }
317              
318             sub fetch_request
319             {
320 0     0 0   my ($self, $request, $callback) = @_;
321 0 0   0     return lambda { undef, "bad request" } unless $request;
  0            
322             return lambda {
323             context $request,
324             conn_cache => $self->{conn_cache}, #XXX
325 0     0     keep_alive => 1; #XXX
326             http_request {
327 0           my $response = shift;
328 0 0         return undef, $response unless ref $response;
329 0           return $callback->($response);
330 0           }};
  0            
331             }
332              
333             sub fetch_message_and_attachments
334             {
335 0     0 0   my ($self, $message ) = @_;
336              
337             return lambda {
338 0     0     context $self-> fetch_request( $self->message( $message->{folderId}, $message->{id} ) );
339             tail {
340 0           my ($xml, $error) = @_;
341 0 0         return ($xml, $error) unless defined $xml;
342              
343 0           my $attachments = $self-> attachments( $xml );
344 0           my @attachments = keys %$attachments;
345 0           my %opt = (
346             message => $xml,
347             attachments => {},
348             );
349              
350 0           context $self-> fetch_request( $self-> content( $message->{folderId}, $message->{id} ));
351             tail {
352 0           my ($body, $error) = @_;
353 0 0         return ($body, $error) unless defined $body;
354 0           $opt{body} = $body;
355            
356 0 0         my $att_id = shift @attachments or return \%opt;
357 0           context $self-> fetch_request( $self-> content( $message->{folderId}, $att_id ));
358             tail {
359 0           my ($att_body, $error) = @_;
360 0 0         return ($att_body, $error) unless defined $att_body;
361              
362 0           $opt{attachments}->{$att_id} = $att_body;
363 0 0         $att_id = shift @attachments or return \%opt;
364 0           context $self-> fetch_request( $self-> content( $message->{folderId}, $att_id ));
365 0           again;
366 0           }}}};
  0            
  0            
  0            
367             }
368              
369             sub list_all_messages
370             {
371 0     0 0   my ( $self, $folder_id ) = @_;
372              
373 0           my $offset = 0;
374 0           my $limit = 1000;
375              
376 0           my %ret;
377              
378             return lambda {
379 0     0     context $self-> fetch_request( $self-> messages( $folder_id, $offset, $limit ));
380             tail {
381 0           my ($xml, $error) = @_;
382 0 0         return ($xml, $error) unless $xml;
383              
384 0           %ret = ( %ret, %$xml );
385 0 0         return \%ret if keys(%$xml) < $limit;
386              
387 0           $offset += $limit;
388 0           context $self-> fetch_request( $self-> messages( $folder_id, $offset, $limit ));
389 0           again;
390 0           }};
  0            
391             }
392              
393             1;
394              
395             __DATA__
396              
397             =pod
398              
399             =head1 NAME
400              
401             Net::Eboks - perl API for http://eboks.dk/
402              
403             =head1 DESCRIPTION
404              
405             Read-only interface for eboks. See README for more info.
406              
407             =head1 AUTHOR
408              
409             Dmitry Karasik <dmitry@karasik.eu.org>
410              
411             =cut