File Coverage

lib/App/wsgetmail.pm
Criterion Covered Total %
statement 27 57 47.3
branch 2 22 9.0
condition 0 3 0.0
subroutine 7 10 70.0
pod 2 2 100.0
total 38 94 40.4


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   1033 use v5.10;
  2         6  
50              
51             package App::wsgetmail;
52              
53 2     2   494 use Moo;
  2         9307  
  2         9  
54              
55             our $VERSION = '0.06';
56              
57             =head1 NAME
58              
59             App::wsgetmail - Fetch mail from the cloud using webservices
60              
61             =head1 SYNOPSIS
62              
63             Run:
64              
65             wsgetmail [options] --config=wsgetmail.json
66              
67             where C looks like:
68              
69             {
70             "client_id": "abcd1234-xxxx-xxxx-xxxx-1234abcdef99",
71             "tenant_id": "abcd1234-xxxx-xxxx-xxxx-123abcde1234",
72             "secret": "abcde1fghij2klmno3pqrst4uvwxy5~0",
73             "global_access": 1,
74             "username": "rt-comment@example.com",
75             "folder": "Inbox",
76             "command": "/opt/rt5/bin/rt-mailgate",
77             "command_args": "--url=http://rt.example.com/ --queue=General --action=comment",
78             "command_timeout": 30,
79             "action_on_fetched": "mark_as_read"
80             }
81              
82             Using App::wsgetmail as a library looks like:
83              
84             my $getmail = App::wsgetmail->new({config => {
85             # The config hashref takes all the same keys and values as the
86             # command line tool configuration JSON.
87             }});
88             while (my $message = $getmail->get_next_message()) {
89             $getmail->process_message($message)
90             or warn "could not process $message->id";
91             }
92              
93             =head1 DESCRIPTION
94              
95             wsgetmail retrieves mail from a folder available through a web services API
96             and delivers it to another system. Currently, it only knows how to retrieve
97             mail from the Microsoft Graph API, and deliver it by running another command
98             on the local system.
99              
100             =head1 INSTALLATION
101              
102             perl Makefile.PL
103             make
104             make test
105             sudo make install
106              
107             C will be installed under C if you're using the
108             system Perl, or in the same directory as C if you built your own.
109              
110             =cut
111              
112 2     2   1895 use Clone 'clone';
  2         2057  
  2         92  
113 2     2   942 use Module::Load;
  2         1870  
  2         12  
114 2     2   761 use App::wsgetmail::MDA;
  2         6  
  2         1161  
115              
116             =head1 ATTRIBUTES
117              
118             =head2 config
119              
120             A hash ref that is passed to construct the C and C (see below).
121              
122             =cut
123              
124             has config => (
125             is => 'ro',
126             required => 1
127             );
128              
129             =head2 mda
130              
131             An instance of L created from our C object.
132              
133             =cut
134              
135             has mda => (
136             is => 'rw',
137             lazy => 1,
138             handles => [ qw(forward) ],
139             builder => '_build_mda'
140             );
141              
142             =head2 client_class
143              
144             The name of the App::wsgetmail package used to construct the
145             C. Default C.
146              
147             =cut
148              
149             has client_class => (
150             is => 'ro',
151             default => sub { 'MS365' }
152             );
153              
154             =head2 client
155              
156             An instance of the C created from our C object.
157              
158             =cut
159              
160             has client => (
161             is => 'ro',
162             lazy => 1,
163             handles => [ qw( get_next_message
164             get_message_mime_content
165             mark_message_as_read
166             delete_message) ],
167             builder => '_build_client'
168             );
169              
170              
171             has _post_fetch_action => (
172             is => 'ro',
173             lazy => 1,
174             builder => '_build__post_fetch_action'
175             );
176              
177              
178             sub _build__post_fetch_action {
179 1     1   11 my $self = shift;
180 1         1 my $fetched_action_method;
181 1         4 my $action = $self->config->{action_on_fetched};
182 1 50       4 return undef unless (defined $action);
183 1 50       4 if (lc($action) eq 'mark_as_read') {
    0          
184 1         2 $fetched_action_method = 'mark_message_as_read';
185             } elsif ( lc($action) eq "delete" ) {
186 0         0 $fetched_action_method = 'delete_message';
187             } else {
188 0         0 $fetched_action_method = undef;
189 0         0 warn "no recognised action for fetched mail, mailbox not updated";
190             }
191 1         4 return $fetched_action_method;
192             }
193              
194             =head1 METHODS
195              
196             =head2 process_message($message)
197              
198             Given a Message object, retrieves the full message content, delivers it
199             using the C, and then executes the configured post-fetch
200             action. Returns a boolean indicating success.
201              
202             =cut
203              
204             sub process_message {
205 0     0 1 0 my ($self, $message) = @_;
206 0         0 my $client = $self->client;
207 0         0 my $filename = $client->get_message_mime_content($message->id);
208 0 0       0 unless ($filename) {
209 0         0 warn "failed to get mime content for message ". $message->id;
210 0         0 return 0;
211             }
212 0         0 my $ok = $self->forward($message, $filename);
213 0 0       0 if ($ok) {
214 0         0 $ok = $self->post_fetch_action($message);
215             }
216 0 0       0 if ($self->config->{dump_messages}) {
217 0 0       0 warn "dumped message in file $filename" if ($self->config->{debug});
218             }
219             else {
220 0 0       0 unlink $filename or warn "couldn't delete message file $filename : $!";
221             }
222 0         0 return $ok;
223             }
224              
225             =head2 post_fetch_action($message)
226              
227             Given a Message object, executes the configured post-fetch action. Returns a
228             boolean indicating success.
229              
230             =cut
231              
232             sub post_fetch_action {
233 0     0 1 0 my ($self, $message) = @_;
234 0         0 my $method = $self->_post_fetch_action;
235 0         0 my $ok = 1;
236             # check for dry-run option
237 0 0       0 if ($self->config->{dry_run}) {
238 0         0 warn "dry run so not running $method action on fetched mail";
239 0         0 return 1;
240             }
241 0 0       0 if ($method) {
242 0         0 $ok = $self->$method($message->id);
243             }
244 0         0 return $ok;
245             }
246              
247             ###
248              
249             sub _build_client {
250 1     1   542 my $self = shift;
251 1         7 my $classname = 'App::wsgetmail::' . $self->client_class;
252 1         5 load $classname;
253 1         25 my $config = clone $self->config;
254 1         24 $config->{post_fetch_action} = $self->_post_fetch_action;
255 1         4 return $classname->new($config);
256             }
257              
258              
259             sub _build_mda {
260 0     0     my $self = shift;
261 0           my $config = clone $self->config;
262 0 0         if ( defined $self->config->{username}) {
263 0   0       $config->{recipient} //= $self->config->{username};
264             }
265 0           return App::wsgetmail::MDA->new($config);
266             }
267              
268             =head1 CONFIGURATION
269              
270             =head2 Configuring Microsoft 365 Client Access
271              
272             To use wsgetmail, first you need to set up the app in Microsoft 365.
273             Two authentication methods are supported:
274              
275             =over
276              
277             =item Client Credentials
278              
279             This method uses shared secrets and is preferred by Microsoft.
280             (See L)
281              
282             =item Username/password
283              
284             This method is more like previous connections via IMAP. It is currently
285             supported by Microsoft, but not recommended. (See L)
286              
287             =back
288              
289             This section walks you through each piece of configuration wsgetmail needs,
290             and how to obtain it.
291              
292             =over 4
293              
294             =item tenant_id
295              
296             wsgetmail authenticates to an Azure Active Directory (AD) tenant. This
297             tenant is identified by an identifier that looks like a UUID/GUID: it should
298             be mostly alphanumeric, with dividing dashes in the same places as shown in
299             the example configuration above. Microsoft documents how to find your tenant
300             ID, and create a tenant if needed, in the L<"Set up a tenant"
301             quickstart|https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-create-new-tenant>. Save
302             this as the C string in your wsgetmail configuration file.
303              
304             =item client_id
305              
306             You need to register wsgetmail as an application in your Azure Active
307             Directory tenant. Microsoft documents how to do this in the L<"Register an
308             application with the Microsoft identity platform"
309             quickstart|https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app#register-an-application>,
310             under the section "Register an application." When asked who can use this
311             application, you can leave that at the default "Accounts in this
312             organizational directory only (Single tenant)."
313              
314             After you successfully register the wsgetmail application, its information
315             page in your Azure account will display an "Application (client) ID" in the
316             same UUID/GUID format as your tenant ID. Save this as the C
317             string in your configuration file.
318              
319             After that is done, you need to grant wsgetmail permission to access the
320             Microsoft Graph mail APIs. Microsoft documents how to do this in the
321             L<"Configure a client application to access a web API"
322             quickstart|https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-configure-app-access-web-apis#application-permission-to-microsoft-graph>,
323             under the section "Add permissions to access Microsoft Graph." When selecting
324             the type of permissions, select "Application permissions." When prompted to
325             select permissions, select all of the following:
326              
327             =over 4
328              
329             =item * Mail.Read
330              
331             =item * Mail.Read.Shared
332              
333             =item * Mail.ReadWrite
334              
335             =item * Mail.ReadWrite.Shared
336              
337             =item * openid
338              
339             =item * User.Read
340              
341             =back
342              
343             =back
344              
345             =head3 Configuring client secret authentication
346              
347             We recommend you deploy wsgetmail by configuring it with a client
348             secret. Client secrets can be granted limited access to only the mailboxes
349             you choose. You can adjust or revoke wsgetmail's access without interfering
350             with other applications.
351              
352             Microsoft documents how to create a client secret in the L<"Register an
353             application with the Microsoft identity platform"
354             quickstart|https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app#add-a-client-secret>,
355             under the section "Add a client secret." Take care to record the secret
356             token when it appears; it will never be displayed again. It should look like
357             a completely random string, not a UUID/GUID.
358              
359             =over 4
360              
361             =item global_access
362              
363             Set this to C<1> in your wsgetmail configuration file.
364              
365             =item secret
366              
367             Set this to the secret token string you recorded earlier in your wsgetmail
368             configuration file.
369              
370             =item username
371              
372             wsgetmail will fetch mail from this user's account. Set this to an email
373             address string in your wsgetmail configuration file.
374              
375             =back
376              
377             =head3 Configuring user+password authentication
378              
379             If you do not want to use a client secret, you can also configure wsgetmail
380             to authenticate with a traditional username+password combination. As noted
381             above, this method is not recommended by Microsoft. It also does not work
382             for systems with federated authentication enabled.
383              
384             =over 4
385              
386             =item global_access
387              
388             Set this to C<0> in your wsgetmail configuration file.
389              
390             =item username
391              
392             wsgetmail will authenticate as this user. Set this to an email address
393             string in your wsgetmail configuration file.
394              
395             =item user_password
396              
397             Set this to the password string for C in your wsgetmail
398             configuration file.
399              
400             =back
401              
402             =head2 Configuring the mail delivery command
403              
404             Now that you've configured wsgetmail to access a mail account, all that's
405             left is configuring delivery. Set the following in your wsgetmail
406             configuration file.
407              
408             =over 4
409              
410             =item folder
411              
412             Set this to the name string of a mail folder to read.
413              
414             =item command
415              
416             Set this to an executable command. You can specify an absolute path,
417             or a plain command name which will be found from C<$PATH>. For each
418             email wsgetmail retrieves, it will run this command and pass the
419             message data to it via standard input.
420              
421             =item command_args
422              
423             Set this to a string with additional arguments to pass to C.
424             These arguments follow shell quoting rules: you can escape characters
425             with a backslash, and denote a single string argument with single or
426             double quotes.
427              
428             =item command_timeout
429              
430             Set this to the number of seconds the C has to return before
431             timeout is reached. The default value is 30.
432              
433             =item action_on_fetched
434              
435             Set this to a literal string C<"mark_as_read"> or C<"delete">.
436             For each email wsgetmail retrieves, after the configured delivery
437             command succeeds, it will take this action on the message.
438              
439             If you set this to C<"mark_as_read">, wsgetmail will only retrieve and
440             deliver messages that are marked unread in the configured folder, so it does
441             not try to deliver the same email multiple times.
442              
443             =back
444              
445             =head1 TESTING AND DEPLOYMENT
446              
447             After you write your wsgetmail configuration file, you can test it by running:
448              
449             wsgetmail --debug --dry-run --config=wsgetmail.json
450              
451             This will read and deliver messages, but will not mark them as read or
452             delete them. If there are any problems, those will be reported in the error
453             output. You can update your configuration file and try again until wsgetmail
454             runs successfully.
455              
456             Once your configuration is stable, you can configure wsgetmail to run
457             periodically through cron or a systemd service on a timer.
458              
459             =head1 LIMITATIONS
460              
461             =head2 Fetching from Multiple Folders
462              
463             wsgetmail can only read from a single folder each time it runs. If you need
464             to read multiple folders (possibly spanning different accounts), then you
465             need to run it multiple times with different configuration.
466              
467             If you only need to change a couple small configuration settings like the
468             folder name, you can use the C<--options> argument to override those from a
469             base configuration file. For example:
470              
471             wsgetmail --config=wsgetmail.json --options='{"folder": "Inbox"}'
472             wsgetmail --config=wsgetmail.json --options='{"folder": "Other Folder"}'
473              
474             NOTE: Setting C or C with C<--options> is not secure
475             and may expose your credentials to other users on the local system. If you
476             need to set these options, or just change a lot of settings in your
477             configuration, just run wsgetmail with different configurations:
478              
479             wsgetmail --config=account01.json
480             wsgetmail --config=account02.json
481              
482             =head2 Office 365 API Limits
483              
484             Microsoft applies some limits to the amount of API requests allowed as
485             documented in their L.
486             If you reach a limit, requests to the API will start failing for a period
487             of time.
488              
489             =head1 SEE ALSO
490              
491             =over 4
492              
493             =item * L
494              
495             =item * L
496              
497             =item * L
498              
499             =item * L
500              
501             =back
502              
503             =head1 AUTHOR
504              
505             Best Practical Solutions, LLC
506              
507             =head1 LICENSE AND COPYRIGHT
508              
509             This software is Copyright (c) 2015-2020 by Best Practical Solutions, LLC.
510              
511             This is free software, licensed under:
512              
513             The GNU General Public License, Version 2, June 1991
514              
515             =cut
516              
517             1;