File Coverage

lib/App/wsgetmail/MS365/Client.pm
Criterion Covered Total %
statement 52 77 67.5
branch 8 28 28.5
condition 1 3 33.3
subroutine 13 19 68.4
pod 6 7 85.7
total 80 134 59.7


line stmt bran cond sub pod time code
1             # BEGIN BPS TAGGED BLOCK {{{
2             #
3             # COPYRIGHT:
4             #
5             # This software is Copyright (c) 2020-2022 Best Practical Solutions, LLC
6             #
7             #
8             # (Except where explicitly superseded by other copyright notices)
9             #
10             #
11             # LICENSE:
12             #
13             # This work is made available to you under the terms of Version 2 of
14             # the GNU General Public License. A copy of that license should have
15             # been provided with this software, but in any event can be snarfed
16             # from www.gnu.org.
17             #
18             # This work is distributed in the hope that it will be useful, but
19             # WITHOUT ANY WARRANTY; without even the implied warranty of
20             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21             # General Public License for more details.
22             #
23             # You should have received a copy of the GNU General Public License
24             # along with this program; if not, write to the Free Software
25             # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26             # 02110-1301 or visit their web page on the internet at
27             # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28             #
29             #
30             # CONTRIBUTION SUBMISSION POLICY:
31             #
32             # (The following paragraph is not intended to limit the rights granted
33             # to you to modify and distribute this software under the terms of
34             # the GNU General Public License and is only of importance to you if
35             # you choose to contribute your changes and enhancements to the
36             # community by submitting them to Best Practical Solutions, LLC.)
37             #
38             # By intentionally submitting any modifications, corrections or
39             # derivatives to this work, or any other work intended for use with
40             # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41             # you are the copyright holder for those contributions and you grant
42             # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43             # royalty-free, perpetual, license to use, copy, create derivative
44             # works based on those contributions, and sublicense and distribute
45             # those contributions and any derivatives thereof.
46             #
47             # END BPS TAGGED BLOCK }}}
48              
49 2     2   157865 use v5.10;
  2         10  
50              
51             package App::wsgetmail::MS365::Client;
52              
53             =head1 NAME
54              
55             App::wsgetmail::MS365::Client - Low-level client to the Microsoft Graph API
56              
57             =cut
58              
59 2     2   446 use Moo;
  2         5597  
  2         8  
60 2     2   1807 use URI::Escape;
  2         1022  
  2         90  
61 2     2   480 use URI;
  2         2275  
  2         38  
62 2     2   8 use JSON;
  2         4  
  2         11  
63 2     2   773 use LWP::UserAgent;
  2         35019  
  2         66  
64 2     2   826 use Azure::AD::ClientCredentials;
  2         268746  
  2         1875  
65              
66             =head1 DESCRIPTION
67              
68             This class performs the actual REST requests to support
69             L.
70              
71             =head1 ATTRIBUTES
72              
73             The following attributes are received from L and have
74             the same meaning:
75              
76             =over 4
77              
78             =item * secret
79              
80             =cut
81              
82             has secret => (
83             is => 'ro',
84             required => 0,
85             );
86              
87             =item * client_id
88              
89             =cut
90              
91             has client_id => (
92             is => 'ro',
93             required => 1,
94             );
95              
96             =item * tenant_id
97              
98             =cut
99              
100             has tenant_id => (
101             is => 'ro',
102             required => 1,
103             );
104              
105             =item * username
106              
107             =cut
108              
109             has username => (
110             is => 'ro',
111             required => 0
112             );
113              
114             =item * user_password
115              
116             =cut
117              
118             has user_password => (
119             is => 'ro',
120             required => 0
121             );
122              
123             =item * global_access
124              
125             =item * debug
126              
127             =cut
128              
129             has global_access => (
130             is => 'ro',
131             default => sub { return 0 }
132             );
133              
134             =back
135              
136             =head2 resource_url
137              
138             A string with the URL for the overall API endpoint.
139              
140             =cut
141              
142             has resource_url => (
143             is => 'ro',
144             default => sub { return 'https://graph.microsoft.com/' }
145             );
146              
147             =head2 resource_path
148              
149             A string with the REST API endpoint URL path.
150              
151             =cut
152              
153             has resource_path => (
154             is => 'ro',
155             default => sub { return 'v1.0' }
156             );
157              
158             has debug => (
159             is => 'rw',
160             default => sub { return 0 }
161             );
162              
163             has _ua => (
164             builder => '_build_authorised_ua',
165             is => 'ro',
166             lazy => 1,
167             );
168              
169             has _credentials => (
170             is => 'ro',
171             lazy => 1,
172             builder => '_build__credentials',
173             );
174              
175             has _access_token => (
176             is => 'ro',
177             lazy => 1,
178             builder => '_build__access_token',
179             );
180              
181             sub BUILD {
182 1     1 0 10 my ($self, $args) = @_;
183              
184 1 50       4 if ($args->{global_access}) {
185 0 0       0 unless ($args->{secret}) {
186 0         0 die "secret is required when using global_access";
187             }
188             }
189             else {
190 1 50 33     11 unless ($args->{username} && $args->{user_password}) {
191 0         0 die "username and user_password are required when not using global_access";
192             }
193             }
194             }
195              
196              
197             =head1 METHODS
198              
199             =head2 build_rest_uri(@endpoint_parts)
200              
201             Given a list of URL component strings, returns a complete URL string to
202             reach that endpoint from this object's C and C.
203              
204             =cut
205              
206             sub build_rest_uri {
207 2     2 1 5 my ($self, @endpoint_parts) = @_;
208 2         8 my $base_url = $self->resource_url . $self->resource_path;
209 2         14 return join('/', $base_url, @endpoint_parts);
210             }
211              
212             =head2 get_request($parts, $params)
213              
214             Makes a GET request to the API. C<$parts> is an arrayref of URL endpoint
215             strings with the specific endpoint to request. C<$params> is a hashref of
216             query parameters to send with the request.
217              
218             =cut
219              
220             sub get_request {
221 2     2 1 29 my ($self, $parts, $params) = @_;
222             # add error handling!
223 2         5 my $uri = URI->new($self->build_rest_uri(@$parts));
224 2 50       6526 warn "making GET request to url $uri" if ($self->debug);
225 2 50       13 $uri->query_form($params) if ($params);
226 2         372 return $self->_ua->get($uri);
227             }
228              
229             =head2 get_request_by_url($url)
230              
231             Makes a GET request to the URL in the C<$url> string.
232              
233             =cut
234              
235             sub get_request_by_url {
236 0     0 1 0 my ($self, $url) = @_;
237 0 0       0 warn "making GET request to url $url" if ($self->debug);
238 0         0 return $self->_ua->get($url);
239             }
240              
241             =head2 delete_request($parts, $params)
242              
243             Makes a DELETE request to the API. C<$parts> is an arrayref of URL endpoint
244             strings with the specific endpoint to request. C<$params> is unused.
245              
246             =cut
247              
248             sub delete_request {
249 0     0 1 0 my ($self, $parts, $params) = @_;
250 0         0 my $url = $self->build_rest_uri(@$parts);
251 0 0       0 warn "making DELETE request to url $url" if ($self->debug);
252 0         0 return $self->_ua->delete($url);
253             }
254              
255             =head2 post_request($path_parts, $post_data)
256              
257             Makes a POST request to the API. C<$path_parts> is an arrayref of URL
258             endpoint strings with the specific endpoint to request. C<$post_data> is a
259             reference to an array or hash of data to include in the POST request body.
260              
261             =cut
262              
263             sub post_request {
264 0     0 1 0 my ($self, $path_parts, $post_data) = @_;
265 0         0 my $url = $self->build_rest_uri(@$path_parts);
266 0 0       0 warn "making POST request to url $url" if ($self->debug);
267 0         0 return $self->_ua->post($url,$post_data);
268             }
269              
270             =head2 patch_request($path_parts, $patch_params)
271              
272             Makes a PATCH request to the API. C<$path_parts> is an arrayref of URL
273             endpoint strings with the specific endpoint to request. C<$patch_params> is
274             a hashref of data to include in the PATCH request body.
275              
276             =cut
277              
278             sub patch_request {
279 0     0 1 0 my ($self, $path_parts, $patch_params) = @_;
280 0         0 my $url = $self->build_rest_uri(@$path_parts);
281 0 0       0 warn "making PATCH request to url $url" if ($self->debug);
282 0         0 return $self->_ua->patch($url,%$patch_params);
283             }
284              
285             ######
286              
287             sub _build_authorised_ua {
288 1     1   10 my $self = shift;
289 1         3 my $ua = $self->_new_useragent;
290 1 50       6 warn "getting system access token" if ($self->debug);
291 1         17 $ua->default_header( Authorization => $self->_access_token() );
292 1         50 return $ua;
293             }
294              
295             sub _build__access_token {
296 1     1   10 my $self = shift;
297 1         1 my $access_token;
298 1 50       4 if ($self->global_access) {
299 0         0 $access_token = $self->_credentials->access_token;
300             }
301             else {
302 1         3 $access_token = $self->_get_user_access_token;
303             }
304 1         4 return $access_token;
305             }
306              
307             sub _get_user_access_token {
308 1     1   2 my $self = shift;
309 1         3 my $ua = $self->_new_useragent;
310 1         3 my $access_token;
311 1 50       4 warn "getting user access token" if ($self->debug);
312 1         6 my $oauth_login_url = sprintf('https://login.windows.net/%s/oauth2/token', $self->tenant_id);
313 1         24 my $response = $ua->post( $oauth_login_url,
314             {
315             resource=> $self->resource_url,
316             client_id => $self->client_id,
317             grant_type=>'password',
318             username=>$self->username,
319             password=>$self->user_password,
320             scope=>'openid'
321             }
322             );
323 1         4161 my $raw_message = $response->content;
324             # check details
325 1 50       11 if ($response->is_success) {
326 1         13 my $token_details = decode_json( $response->content );
327 1         21 $access_token = "Bearer " . $token_details->{access_token};
328             }
329             else {
330             # throw error
331 0 0       0 warn "auth response from server : $raw_message" if ($self->debug);
332 0         0 die sprintf('unable to get user access token for user %s request failed with status %s ', $self->username, $response->status_line);
333             }
334 1         3 return $access_token;
335             }
336              
337             sub _build__credentials {
338 0     0     my $self = shift;
339 0           my $creds = Azure::AD::ClientCredentials->new(
340             resource_id => $self->resource_url,
341             client_id => $self->client_id,
342             secret_id => $self->secret,
343             tenant_id => $self->tenant_id
344             );
345 0           return $creds;
346             }
347              
348             sub _new_useragent {
349 0     0     return LWP::UserAgent->new();
350             }
351              
352             =head1 SEE ALSO
353              
354             =over 4
355              
356             =item * L
357              
358             =back
359              
360             =head1 AUTHOR
361              
362             Best Practical Solutions, LLC
363              
364             =head1 LICENSE AND COPYRIGHT
365              
366             This software is Copyright (c) 2020 by Best Practical Solutions, LLC
367              
368             This is free software, licensed under:
369              
370             The GNU General Public License, Version 2, June 1991
371              
372             =cut
373              
374             1;