File Coverage

blib/lib/Net/Eboks.pm
Criterion Covered Total %
statement 44 255 17.2
branch 0 84 0.0
condition 0 63 0.0
subroutine 15 48 31.2
pod 0 24 0.0
total 59 474 12.4


line stmt bran cond sub pod time code
1             package Net::Eboks;
2              
3 1     1   219167 use 5.010;
  1         4  
4 1     1   7 use strict;
  1         2  
  1         29  
5 1     1   5 use warnings;
  1         25  
  1         105  
6 1     1   9 use Encode qw(encode decode);
  1         1  
  1         241  
7 1     1   1499 use DateTime;
  1         575988  
  1         63  
8 1     1   765 use HTTP::Request;
  1         48097  
  1         61  
9 1     1   722 use Digest::SHA qw(sha256_hex);
  1         3574  
  1         172  
10 1     1   1341 use XML::Simple;
  1         13198  
  1         13  
11 1     1   1397 use LWP::UserAgent;
  1         42493  
  1         58  
12 1     1   681 use LWP::ConnCache;
  1         1851  
  1         46  
13 1     1   1163 use MIME::Entity;
  1         134185  
  1         50  
14 1     1   12 use MIME::Base64;
  1         2  
  1         79  
15 1     1   1306 use IO::Lambda qw(:all);
  1         17877  
  1         429  
16 1     1   770 use IO::Lambda::HTTP qw(http_request);
  1         33110  
  1         100  
17 1     1   838 use Crypt::OpenSSL::RSA;
  1         4711  
  1         5539  
18              
19             our $VERSION = '0.11';
20              
21             sub new
22             {
23 0     0 0   my ( $class, %opts ) = @_;
24             my $self = bless {
25             cpr => '0000000000',
26             password => '',
27             country => 'DK',
28             type => 'P',
29             datetime => DateTime->now->strftime('%Y-%m-%d %H:%M:%SZ'),
30             root => 'rest.e-boks.dk',
31             mailapp => '/mobile/1/xml.svc/en-gb',
32             deviceid => 'DEADBEEF-1337-1337-1337-900000000002',
33              
34             nonce => '',
35             sessionid => '',
36             response => "3a1a51f235a8bd6bbc29b2caef986a1aeb77018d60ffdad9c5e31117e7b6ead3", # XXX
37             uid => undef,
38             uname => undef,
39             share_id => '0',
40             conn_cache => LWP::ConnCache->new,
41              
42 0   0       from => $ENV{MAILFROM} // 'noreply@e-boks.dk',
43              
44             %opts,
45             }, $class;
46              
47 0           return $self;
48             }
49              
50 0     0 0   sub set { $_[0]->{$_[1]} = $_[2] }
51              
52             sub response
53             {
54 0     0 0   my ($self, $decode, $response) = @_;
55              
56 0 0         unless ($response->is_success) {
57 0   0       my $sl = $response->message // $response-> status_line;
58 0           chomp $sl;
59 0           $sl =~ s/\+/ /g;
60 0           return undef, $sl;
61             }
62            
63 0           for ( split /,\s*/, $response->header('x-eboks-authenticate')) {
64 0 0         warn "bad x-eboks-authenticate: $_\n" unless m/^(sessionid|nonce)="(.*?)"$/;
65 0           $self->{$1} = $2;
66             }
67            
68 0 0         return $response->decoded_content unless $decode;
69            
70 0 0         my %options = ref($decode) ? %$decode : ();
71 0           my $content = $response->decoded_content;
72 0 0 0       if ( $content !~ /[^\x00-\xff]/ && $content =~ /[\x80-\xff]/ ) {
73             # try to upgrade
74 0           eval {
75 0           my $c = decode('latin1', $content);
76 0           $content = $c;
77             };
78             }
79 0           my $xml = XMLin($content, ForceArray => 1, %options);
80 0 0 0       if ( $xml && ref($xml) eq 'HASH' ) {
81 0           return $xml;
82             } else {
83 0           return undef, "xml returned is not a hash";
84             }
85             }
86              
87             sub login
88             {
89 0     0 0   my $self = shift;
90              
91 0 0         return undef if defined $self->{uid};
92              
93 0           local $self->{challenge} = sha256_hex(sha256_hex(join(':', EBOKS => @{$self}{
94 0           qw(deviceid type cpr country password datetime)})));
95 0           my $authstr = 'logon ' . join(',', map { "$_=\"$self->{$_}\"" } qw(deviceid datetime challenge));
  0            
96 0           my $content = <<XML;
97             <Logon xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
98             xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:eboks:mobile:1.0.0">
99             <User identity="$self->{cpr}" identityType="$self->{type}"
100             nationality="$self->{country}" pincode="$self->{password}"/>
101             </Logon>
102             XML
103              
104             my $login = HTTP::Request->new(
105             'PUT',
106             'https://' . $self->{root} . '/mobile/1/xml.svc/en-gb/session',
107             [
108             'Content-Type' => 'application/xml',
109             'Content-Length' => length($content),
110             'X-EBOKS-AUTHENTICATE' => $authstr,
111             'Accept' => '*/*',
112             'Accept-Language' => 'en-US',
113             'Accept-Encoding' => 'gzip,deflate',
114             'Host' => $self->{root},
115 0           ],
116             $content
117             );
118 0           $login->protocol('HTTP/1.1');
119              
120             return $login, sub {
121 0     0     my ($xml, $error) = $self-> response({ForceArray => 0}, @_);
122 0 0         return $xml, $error unless $xml;
123 0 0         return undef, "'User' is not present in response" unless exists $xml->{User};
124              
125 0           $self->{uid} = $xml->{User}->{userId};
126 0           $self->{uname} = $xml->{User}->{name};
127 0           return $self->{uname};
128 0           };
129             }
130              
131             sub login_nemid
132             {
133 0     0 0   my $self = shift;
134              
135 0 0         return undef if defined $self->{uid};
136              
137             # openssl genrsa -out id_rsa 2048
138 0           my $pk = Crypt::OpenSSL::RSA->new_private_key(<<'PVT');
139             -----BEGIN RSA PRIVATE KEY-----
140             MIIEpAIBAAKCAQEA2VUahnbWKIY4rn8jEthY9M2BoMIHoNQlY4YUL9pV+MpSKyy9
141             MjVKV6h8ERnj+1wxUJDR3ZJimYnvcruGqlSR+uhL8MJs7GqSSOL3zKbZiHmip1/j
142             /9Wzsu86VJibxd14/5r8OugIJDs+aeE6fxpKW1BtUiiUAvlbC4MwnAnCPemzl7gG
143             qi64xsSaVdoi0NzZpxI+ItP9x89eMw64F5GlIviGJ9hODyW3ckKSvgxEQGf7x9TN
144             toVt1Gxh4jdokalHmgNQy4zaqnzGLstl227HIEfbbzX/rK30FFVurFG0JAE9T7z7
145             b0S5RkGFx4GgKGRoFRd8HE+UptBa4JyvmvA3MQIDAQABAoIBAQCtEkbDWhOFxg2R
146             eJGXyk5c9OMMADhO7WKw9O9ShE7+hzAUTdaFC0ces3/JppKVc3+aJxnZl1+J4fyb
147             o5bEQgDWjPMc0dgoFV5VSNoJUb3eHu9W1tgcvjQShMww3i7+zTY0Z1oCFxGUuNEl
148             REVvPqKEQXItgT8Nd0H30wt815Cl9+RlaXMmNRq6aCB0GSUHpVGmgasiUejk7Zej
149             rp23LarcmZitiQXGt0yCbW35/6553Ph88W4cgfav6y+LKTxK06UkKh/QRJaCoKLV
150             BXVmb5HCZv5waU5eRaxV/TKTATDU45DuU3f76+OlQp0P/cH4EVXhC+95fLJ1XUxG
151             lSIQW+MhAoGBAO7N0o5llwbgNpxzZ6z4mRni3LUFpURozNT3q616qM7QGBRfDeHI
152             o1cZf8wRWmMBdA16iWT7xMTpRfHcHt8NW+XlzisQG5KOlrUh1ZtRrltFGPnxUd84
153             EgbUK+ArzU4mqZZMETZBmrJqVO1lB0dhrjqZ9SDUeXimNqZCoGHU/Z9VAoGBAOj7
154             d/bhp4OtRKBDgKF1IThikgnsZAcBfGCXaASrPFAbi1p4MNEHUNkQXG7V/PSxMlUf
155             y3U35Hsxexpq2Al98gw/TUBdgb/WfDJHole1fbXTd/Gh9H8RdMLSnOdLTCMXvW9r
156             e1DKt8/5fb87BE8xQUc9sXJ5mmx522WqEyOXZeBtAoGAbIUQCDHWXgOKDbLMDGi0
157             enUDwyeboOjXHHiohZ9WExWxu6AumMoqoCwwTTYdkxxX9sAWq9NV6f3wESbsyIQz
158             nNe/xwX84a72gb2sanbF+yf9X6fwgrXiS0Qj5C1DkR40tt4+fB94A1ga2/6rPh7/
159             pBXOtWqZAODXuNpSM+MsljkCgYBWV9u9wyMxyaUFP/8L1zzYiK9WviTT89kEcxg5
160             orxXc93RSXnN/cgYqdeXu/ZjOMhOg9oDNxOWFGBrCe3GlsZ9g3g9wmmzjum4OJQR
161             rVFJcXWiN0NFVFLRYPyFO4Kb/tBV2p948afti6julhCiyL5IiLSamDaCvSZyJvWw
162             2wsGgQKBgQCt6ityOP56Co60pUnhonmNLg98IvWnREn8xoGdjtrMuk+Ksf5sUX5i
163             fcogYnJ4ciLmJ37cVXfOtrRrDsjqSbHY07Oqb0qdIKPATJkiK0ltXG4hSvjB2LPU
164             XC53Clpk+n6+ltUJnUAFtl8g4jcUG9Bs+334WiX0n7Hx7yQlsvzBtA==
165             -----END RSA PRIVATE KEY-----
166             PVT
167 0           local $self->{challenge} = join(':', q(EBOKS), @{$self}{qw(deviceid type cpr country password datetime)});
  0            
168 0           $self->{challenge} = encode_base64($pk->sign($self->{challenge}));
169              
170 0           my $authstr = 'logon ' . join(',', map { "$_=\"$self->{$_}\"" } qw(deviceid datetime challenge));
  0            
171 0           my $content = <<XML;
172             <Logon xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
173             xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:eboks:mobile:1.0.0">
174             <App version="$VERSION" os="$^O" osVersion="1" Device="Net-Eboks"/>
175             <User identity="$self->{cpr}" identityType="$self->{type}"
176             nationality="$self->{country}" pincode="$self->{password}"/>
177             </Logon>
178             XML
179              
180             my $login = HTTP::Request->new(
181             'PUT',
182             'https://' . $self->{root} . $self->{mailapp} . '/session',
183             [
184             'Content-Type' => 'application/xml',
185             'Content-Length' => length($content),
186             'X-EBOKS-AUTHENTICATE' => $authstr,
187             'Accept' => '*/*',
188             'Accept-Language' => 'en-US',
189             'Accept-Encoding' => 'gzip,deflate',
190             'Host' => $self->{root},
191 0           ],
192             $content
193             );
194 0           $login->protocol('HTTP/1.1');
195              
196             return $login, sub {
197 0     0     my ($xml, $error) = $self-> response({ForceArray => 0}, @_);
198 0 0         return $xml, $error unless $xml;
199 0 0         return undef, "'User' is not present in response" unless exists $xml->{User};
200              
201 0           $self->{uid} = $xml->{User}->{userId};
202 0           $self->{uname} = $xml->{User}->{name};
203 0           return $self->{uname};
204 0           };
205             }
206              
207             sub public_key
208             {
209              
210             # openssl rsa -in id_rsa -outform PEM -pubout -out id_rsa.pub
211 0     0 0   return join '', grep {!/^--/} split /\n/, <<'PUB';
  0            
212             -----BEGIN PUBLIC KEY-----
213             MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2VUahnbWKIY4rn8jEthY
214             9M2BoMIHoNQlY4YUL9pV+MpSKyy9MjVKV6h8ERnj+1wxUJDR3ZJimYnvcruGqlSR
215             +uhL8MJs7GqSSOL3zKbZiHmip1/j/9Wzsu86VJibxd14/5r8OugIJDs+aeE6fxpK
216             W1BtUiiUAvlbC4MwnAnCPemzl7gGqi64xsSaVdoi0NzZpxI+ItP9x89eMw64F5Gl
217             IviGJ9hODyW3ckKSvgxEQGf7x9TNtoVt1Gxh4jdokalHmgNQy4zaqnzGLstl227H
218             IEfbbzX/rK30FFVurFG0JAE9T7z7b0S5RkGFx4GgKGRoFRd8HE+UptBa4JyvmvA3
219             MQIDAQAB
220             -----END PUBLIC KEY-----
221             PUB
222             }
223              
224             sub session_activate
225             {
226 0     0 0   my ($self, $ticket) = @_;
227              
228 0           my $pubkey = $self->public_key;
229 0           my $authstr = join(',', map { "$_=\"$self->{$_}\"" } qw(deviceid nonce sessionid response));
  0            
230 0           my $content = <<XML;
231             <?xml version="1.0" encoding="utf-8"?>
232             <Activation xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
233             xmlns:xsd="http://www.w3.org/2001/XMLSchema" deviceId="$self->{deviceid}"
234             deviceName="Net-Eboks" deviceOs="$^O" key="$pubkey" ticket="$ticket"
235             ticketType="KspWeb" xmlns="urn:eboks:mobile:1.0.0" />
236             XML
237              
238             my $login = HTTP::Request->new(
239             'PUT',
240             'https://' . $self->{root} . $self->{mailapp} . "/$self->{uid}/0/session/activate",
241             [
242             'Content-Type' => 'application/xml',
243             'Content-Length' => length($content),
244             'X-EBOKS-AUTHENTICATE' => $authstr,
245             'Accept' => '*/*',
246             'Accept-Language' => 'en-US',
247             'Accept-Encoding' => 'gzip,deflate',
248             'Host' => $self->{root},
249 0           ],
250             $content
251             );
252 0           $login->protocol('HTTP/1.1');
253              
254 0     0     return $login, sub { $self-> response(0, @_) };
  0            
255             }
256              
257 0     0 0   sub ua { LWP::UserAgent->new(conn_cache => shift->{conn_cache}) }
258              
259             sub get
260             {
261 0     0 0   my ($self, $path) = @_;
262 0           my $authstr = join(',', map { "$_=\"$self->{$_}\"" } qw(deviceid nonce sessionid response));
  0            
263             my $get = HTTP::Request->new(
264             'GET',
265             'https://' . $self->{root} . $self->{mailapp} . '/' . $path,
266             [
267             'X-EBOKS-AUTHENTICATE' => $authstr,
268             'Accept' => '*/*',
269             'Accept-Language' => 'en-US',
270             'Host' => $self->{root},
271 0           ],
272             );
273 0           $get->protocol('HTTP/1.1');
274 0           return $get;
275             }
276              
277             sub xmlget
278             {
279 0     0 0   my ( $self, $uri, $path, %xmlopt ) = @_;
280             return
281             $self->get($uri), sub {
282 0     0     my ($xml, $error) = $self-> response(\%xmlopt, @_);
283 0 0         return $xml, $error unless $xml;
284 0   0       for my $step ( @{ $path // [] } ) {
  0            
285 0 0         return undef, "key '$step' not found" unless ref $xml;
286 0 0         if ( ref($xml) eq 'ARRAY') {
287 0           $xml = $xml->[$step];
288             } else {
289 0           $xml = $xml->{$step};
290             }
291             }
292              
293 0   0       my $key = $xmlopt{KeyAttr} // 'name';
294 0           while ( my ( $k, $v ) = each %$xml ) {
295 0 0 0       $v->{$key} = $k if defined($v) && ref($v) eq 'HASH';
296             }
297              
298 0           return $xml;
299 0           };
300             }
301              
302             sub folders
303             {
304 0     0 0   my ($self, $share_id) = @_;
305 0 0         return undef unless $self->{uid};
306 0   0       $share_id //= $self->{share_id};
307 0           $self-> xmlget("$self->{uid}/$share_id/mail/folders", ['FolderInfo']);
308             }
309              
310             sub shares
311             {
312 0     0 0   my $self = shift;
313 0 0         return undef unless $self->{uid};
314 0           $self-> xmlget("$self->{uid}/0/shares?listType=active", ['Share']);
315             }
316              
317             sub messages
318             {
319 0     0 0   my ($self, $share_id, $folder_id, $offset, $limit) = @_;
320 0 0         return undef unless $self->{uid};
321 0   0       $share_id //= $self->{share_id};
322 0   0       $limit //= 1;
323 0   0       $offset //= 0;
324 0           $self-> xmlget(
325             "$self->{uid}/$share_id/mail/folder/$folder_id?skip=$offset&take=$limit",
326             [ qw(Messages 0 MessageInfo) ],
327             KeyAttr => 'id'
328             );
329             }
330              
331             sub message
332             {
333 0     0 0   my ($self, $share_id, $folder_id, $message_id) = @_;
334 0 0         return undef unless $self->{uid};
335 0   0       $share_id //= $self->{share_id};
336 0           $self-> xmlget(
337             "$self->{uid}/$share_id/mail/folder/$folder_id/message/$message_id",
338             [],
339             KeyAttr => 'id'
340             );
341             }
342              
343             sub content
344             {
345 0     0 0   my ( $self, $share_id, $folder_id, $content_id ) = @_;
346 0 0         return undef unless $self->{uid};
347 0   0       $share_id //= $self->{share_id};
348             return
349             $self-> get( "$self->{uid}/$share_id/mail/folder/$folder_id/message/$content_id/content" ), sub {
350 0     0     $self-> response( 0, @_ )
351 0           };
352             }
353              
354 0     0 0   sub attachments { $_[1]->{Attachements}->[0]->{AttachmentInfo} }
355              
356             sub filename {
357 0     0 0   my $fn = $_[1]-> {name};
358 0           $fn =~ s[:\\\/][_];
359 0           my $fmt = lc($_[1]->{format});
360 0 0         $fmt = 'txt' if $fmt eq 'plain';
361             return $fn . '.' .lc($_[1]->{format})
362 0           }
363              
364             sub mime_type
365             {
366 0     0 0   my $fmt = lc $_[1]->{format};
367 0 0         if ( $fmt =~ /^(pdf)$/ ) {
    0          
    0          
368 0           return "application/$fmt";
369             } elsif ( $fmt =~ /^(gif|jpg|jpeg|tiff|png|webp)$/) {
370 0           return "image/$fmt";
371             } elsif ( $fmt =~ /^(txt|text|html|plain)$/) {
372 0 0         $fmt = 'plain' if $fmt =~ /^(txt|text)$/;
373 0           return "text/$fmt";
374             } else {
375 0           return "application/$fmt";
376             }
377             }
378              
379             sub first_value
380             {
381 0     0 0   my ($self, $entry) = @_;
382 0 0         if ( ref($entry) eq 'HASH') {
    0          
383 0           my $k = (sort keys %$entry)[0];
384 0           return $entry->{$k};
385             } elsif ( ref($entry) eq 'ARRAY') {
386 0           return $entry->[0];
387             } else {
388 0           return "bad entry";
389             }
390             }
391              
392             sub safe_encode
393             {
394 0     0 0   my ($enc, $text) = @_;
395 0           utf8::downgrade($text, 'fail silently please');
396 0 0 0       return (utf8::is_utf8($text) || $text =~ /[\x80-\xff]/) ? encode($enc, $text) : $text;
397             }
398              
399             sub assemble_mail
400             {
401 0     0 0   my ( $self, %opt ) = @_;
402              
403 0           my $msg = $opt{message};
404 0           my $sender = $self->first_value($msg->{Sender});
405 0 0         $sender = $sender->{content} if ref($sender) eq 'HASH';
406 0   0       $sender //= 'unknown';
407              
408 0   0       my $received = $msg->{receivedDateTime} // '';
409 0           my $date;
410 0 0         if ( $received =~ /^(\d{4})-(\d{2})-(\d{2})T(\d\d):(\d\d):(\d\d)/) {
411 0           $date = DateTime->new(
412             year => $1,
413             month => $2,
414             day => $3,
415             hour => $4,
416             minute => $5,
417             second => $6,
418             );
419             } else {
420 0           $date = DateTime->now;
421             }
422 0           $received = $date->strftime('%a, %d %b %Y %H:%M:%S %z');
423              
424             my $mail = MIME::Entity->build(
425             From => $opt{from} // ( safe_encode('MIME-Q', $sender) . " <$self->{from}>" ) ,
426             To => $opt{to} // ( safe_encode('MIME-Q', $self->{uname}) . ' <' . ( $ENV{USER} // 'you' ) . '@localhost>' ),
427             Subject => $opt{subject} // safe_encode('MIME-Header', $msg->{name}),
428             Data => $opt{data} // encode('utf-8', "Mail from $sender"),
429             Date => $opt{date} // $received,
430             Charset => 'utf-8',
431             Encoding => 'quoted-printable',
432             'X-Net-Eboks' => "v/$VERSION",
433 0   0       'X-Net-Eboks-ShareId' => $opt{share_id} // '0',
      0        
      0        
      0        
      0        
      0        
      0        
434             );
435              
436 0           my @attachments;
437 0 0         push @attachments, [ $msg, $opt{body} ] if exists $opt{body};
438              
439 0           my $attachments = $self->attachments($msg);
440 0           for my $att_id ( sort keys %$attachments ) {
441 0           push @attachments, [ $attachments->{$att_id}, $opt{attachments}->{$att_id} ];
442             }
443              
444 0           for ( @attachments ) {
445 0           my ( $msg, $body ) = @$_;
446 0           my $fn = $self->filename($msg);
447 0           Encode::_utf8_off($body);
448 0           my $entity = $mail->attach(
449             Type => $self->mime_type($msg),
450             Encoding => 'base64',
451             Data => $body,
452             Filename => $fn,
453             );
454              
455             # XXX hack filename for utf8
456 0 0         next unless $fn =~ m/[^\x00-\x80]/;
457 0           $fn = Encode::encode('MIME-B', $fn);
458 0           for ( 'Content-disposition', 'Content-type') {
459 0           my $v = $entity->head->get($_);
460 0           $v =~ s/name="(.*)"/name="$fn"/;
461 0           $entity->head->replace($_, $v);
462             }
463             }
464              
465             return
466 0           'From noreply@localhost ' .
467             $date->strftime('%a %b %d %H:%M:%S %Y') . "\n" .
468             $mail->stringify
469             ;
470             }
471              
472             sub fetch_request
473             {
474 0     0 0   my ($self, $request, $callback) = @_;
475 0 0   0     return lambda { undef, "bad request" } unless $request;
  0            
476             return lambda {
477             context $request,
478             conn_cache => $self->{conn_cache}, #XXX
479 0     0     keep_alive => 1; #XXX
480             http_request {
481 0           my $response = shift;
482 0 0         return undef, $response unless ref $response;
483 0           return $callback->($response);
484 0           }};
  0            
485             }
486              
487             sub fetch_message_and_attachments
488             {
489 0     0 0   my ($self, $message ) = @_;
490            
491             return lambda {
492 0     0     context $self-> fetch_request( $self->message( $message->{shareId}, $message->{folderId}, $message->{id} ) );
493             tail {
494 0           my ($xml, $error) = @_;
495 0 0         return ($xml, $error) unless defined $xml;
496              
497 0           my $attachments = $self-> attachments( $xml );
498 0           my @attachments = keys %$attachments;
499             my %opt = (
500             message => $xml,
501             share_id => $message->{shareId},
502 0           attachments => {},
503             );
504              
505 0           context $self-> fetch_request( $self-> content( $message->{shareId}, $message->{folderId}, $message->{id} ));
506             tail {
507 0           my ($body, $error) = @_;
508 0 0         return ($body, $error) unless defined $body;
509 0           $opt{body} = $body;
510            
511 0 0         my $att_id = shift @attachments or return \%opt;
512 0           context $self-> fetch_request( $self-> content( $message->{shareId}, $message->{folderId}, $att_id ));
513             tail {
514 0           my ($att_body, $error) = @_;
515 0 0         return ($att_body, $error) unless defined $att_body;
516              
517 0           $opt{attachments}->{$att_id} = $att_body;
518 0 0         $att_id = shift @attachments or return \%opt;
519 0           context $self-> fetch_request( $self-> content( $message->{shareId}, $message->{folderId}, $att_id ));
520 0           again;
521 0           }}}};
  0            
  0            
  0            
522             }
523              
524             sub list_all_messages
525             {
526 0     0 0   my ( $self, $share_id, $folder_id ) = @_;
527              
528 0           my $offset = 0;
529 0           my $limit = 1000;
530 0   0       $share_id //= $self->{share_id};
531              
532 0           my %ret;
533              
534             return lambda {
535 0     0     context $self-> fetch_request( $self-> messages( $share_id, $folder_id, $offset, $limit ));
536             tail {
537 0           my ($xml, $error) = @_;
538 0 0         return ($xml, $error) unless $xml;
539              
540 0           $_->{shareId} = $share_id for values %$xml;
541 0           %ret = ( %ret, %$xml );
542 0 0         return \%ret if keys(%$xml) < $limit;
543              
544 0           $offset += $limit;
545 0           context $self-> fetch_request( $self-> messages( $share_id, $folder_id, $offset, $limit ));
546 0           again;
547 0           }};
  0            
548             }
549              
550             1;
551              
552             =pod
553              
554             =head1 NAME
555              
556             Net::Eboks - perl API for http://eboks.dk/
557              
558             =head1 DESCRIPTION
559              
560             Read-only interface for eboks. See README for more info.
561              
562             =head1 AUTHOR
563              
564             Dmitry Karasik <dmitry@karasik.eu.org>
565              
566             =cut