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-2026 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   253106 use v5.10;
  2         6  
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   582 use Moo;
  2         8609  
  2         15  
60 2     2   2663 use URI::Escape;
  2         1628  
  2         174  
61 2     2   1120 use URI;
  2         4549  
  2         72  
62 2     2   33 use JSON;
  2         5  
  2         20  
63 2     2   1427 use LWP::UserAgent;
  2         58034  
  2         121  
64 2     2   1252 use Azure::AD::ClientCredentials;
  2         559634  
  2         2073  
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. Defaults to
139             C. For Microsoft Government Cloud (GCC High),
140             set this to C. For DoD, use
141             C.
142              
143             =cut
144              
145             has resource_url => (
146             is => 'ro',
147             default => sub { return 'https://graph.microsoft.com/' }
148             );
149              
150             =head2 login_base_url
151              
152             A string with the base URL for OAuth authentication. Defaults to
153             C. For Microsoft Government Cloud
154             (GCC High and DoD), set this to C.
155              
156             =cut
157              
158             has login_base_url => (
159             is => 'ro',
160             default => sub { return 'https://login.windows.net' }
161             );
162              
163             =head2 resource_path
164              
165             A string with the REST API endpoint URL path.
166              
167             =cut
168              
169             has resource_path => (
170             is => 'ro',
171             default => sub { return 'v1.0' }
172             );
173              
174             has debug => (
175             is => 'rw',
176             default => sub { return 0 }
177             );
178              
179             has _ua => (
180             builder => '_build_authorised_ua',
181             is => 'ro',
182             lazy => 1,
183             );
184              
185             has _credentials => (
186             is => 'ro',
187             lazy => 1,
188             builder => '_build__credentials',
189             );
190              
191             has _access_token => (
192             is => 'ro',
193             lazy => 1,
194             builder => '_build__access_token',
195             );
196              
197             sub BUILD {
198 2     2 0 19 my ($self, $args) = @_;
199              
200 2 50       7 if ($args->{global_access}) {
201 0 0       0 unless ($args->{secret}) {
202 0         0 die "secret is required when using global_access";
203             }
204             }
205             else {
206 2 50 33     18 unless ($args->{username} && $args->{user_password}) {
207 0         0 die "username and user_password are required when not using global_access";
208             }
209             }
210             }
211              
212              
213             =head1 METHODS
214              
215             =head2 build_rest_uri(@endpoint_parts)
216              
217             Given a list of URL component strings, returns a complete URL string to
218             reach that endpoint from this object's C and C.
219              
220             =cut
221              
222             sub build_rest_uri {
223 4     4 1 9 my ($self, @endpoint_parts) = @_;
224 4         14 my $base_url = $self->resource_url . $self->resource_path;
225 4         30 return join('/', $base_url, @endpoint_parts);
226             }
227              
228             =head2 get_request($parts, $params)
229              
230             Makes a GET request to the API. C<$parts> is an arrayref of URL endpoint
231             strings with the specific endpoint to request. C<$params> is a hashref of
232             query parameters to send with the request.
233              
234             =cut
235              
236             sub get_request {
237 4     4 1 45 my ($self, $parts, $params) = @_;
238             # add error handling!
239 4         11 my $uri = URI->new($self->build_rest_uri(@$parts));
240 4 50       7901 warn "making GET request to url $uri" if ($self->debug);
241 4 50       21 $uri->query_form($params) if ($params);
242 4         638 return $self->_ua->get($uri);
243             }
244              
245             =head2 get_request_by_url($url)
246              
247             Makes a GET request to the URL in the C<$url> string.
248              
249             =cut
250              
251             sub get_request_by_url {
252 0     0 1 0 my ($self, $url) = @_;
253 0 0       0 warn "making GET request to url $url" if ($self->debug);
254 0         0 return $self->_ua->get($url);
255             }
256              
257             =head2 delete_request($parts, $params)
258              
259             Makes a DELETE request to the API. C<$parts> is an arrayref of URL endpoint
260             strings with the specific endpoint to request. C<$params> is unused.
261              
262             =cut
263              
264             sub delete_request {
265 0     0 1 0 my ($self, $parts, $params) = @_;
266 0         0 my $url = $self->build_rest_uri(@$parts);
267 0 0       0 warn "making DELETE request to url $url" if ($self->debug);
268 0         0 return $self->_ua->delete($url);
269             }
270              
271             =head2 post_request($path_parts, $post_data)
272              
273             Makes a POST request to the API. C<$path_parts> is an arrayref of URL
274             endpoint strings with the specific endpoint to request. C<$post_data> is a
275             reference to an array or hash of data to include in the POST request body.
276              
277             =cut
278              
279             sub post_request {
280 0     0 1 0 my ($self, $path_parts, $post_data) = @_;
281 0         0 my $url = $self->build_rest_uri(@$path_parts);
282 0 0       0 warn "making POST request to url $url" if ($self->debug);
283 0         0 return $self->_ua->post($url,$post_data);
284             }
285              
286             =head2 patch_request($path_parts, $patch_params)
287              
288             Makes a PATCH request to the API. C<$path_parts> is an arrayref of URL
289             endpoint strings with the specific endpoint to request. C<$patch_params> is
290             a hashref of data to include in the PATCH request body.
291              
292             =cut
293              
294             sub patch_request {
295 0     0 1 0 my ($self, $path_parts, $patch_params) = @_;
296 0         0 my $url = $self->build_rest_uri(@$path_parts);
297 0 0       0 warn "making PATCH request to url $url" if ($self->debug);
298 0         0 return $self->_ua->patch($url,%$patch_params);
299             }
300              
301             ######
302              
303             sub _build_authorised_ua {
304 2     2   16 my $self = shift;
305 2         9 my $ua = $self->_new_useragent;
306 2 50       9 warn "getting system access token" if ($self->debug);
307 2         27 $ua->default_header( Authorization => $self->_access_token() );
308 2         83 return $ua;
309             }
310              
311             sub _build__access_token {
312 2     2   13 my $self = shift;
313 2         3 my $access_token;
314 2 50       7 if ($self->global_access) {
315 0         0 $access_token = $self->_credentials->access_token;
316             }
317             else {
318 2         5 $access_token = $self->_get_user_access_token;
319             }
320 2         10 return $access_token;
321             }
322              
323             sub _get_user_access_token {
324 2     2   3 my $self = shift;
325 2         5 my $ua = $self->_new_useragent;
326 2         4 my $access_token;
327 2 50       6 warn "getting user access token" if ($self->debug);
328 2         18 my $oauth_login_url = sprintf('%s/%s/oauth2/token', $self->login_base_url, $self->tenant_id);
329 2         32 my $response = $ua->post( $oauth_login_url,
330             {
331             resource=> $self->resource_url,
332             client_id => $self->client_id,
333             grant_type=>'password',
334             username=>$self->username,
335             password=>$self->user_password,
336             scope=>'openid'
337             }
338             );
339 2         5587 my $raw_message = $response->content;
340             # check details
341 2 50       21 if ($response->is_success) {
342 2         18 my $token_details = decode_json( $response->content );
343 2         43 $access_token = "Bearer " . $token_details->{access_token};
344             }
345             else {
346             # throw error
347 0 0       0 warn "auth response from server : $raw_message" if ($self->debug);
348 0         0 die sprintf('unable to get user access token for user %s request failed with status %s ', $self->username, $response->status_line);
349             }
350 2         5 return $access_token;
351             }
352              
353             sub _build__credentials {
354 0     0     my $self = shift;
355 0           my $creds = Azure::AD::ClientCredentials->new(
356             resource_id => $self->resource_url,
357             client_id => $self->client_id,
358             secret_id => $self->secret,
359             tenant_id => $self->tenant_id,
360             ad_url => $self->login_base_url,
361             );
362 0           return $creds;
363             }
364              
365             sub _new_useragent {
366 0     0     return LWP::UserAgent->new();
367             }
368              
369             =head1 SEE ALSO
370              
371             =over 4
372              
373             =item * L
374              
375             =back
376              
377             =head1 AUTHOR
378              
379             Best Practical Solutions, LLC
380              
381             =head1 LICENSE AND COPYRIGHT
382              
383             This software is Copyright (c) 2020 by Best Practical Solutions, LLC
384              
385             This is free software, licensed under:
386              
387             The GNU General Public License, Version 2, June 1991
388              
389             =cut
390              
391             1;