[Bps-public-commit] app-wsgetmail branch prep-0.05-release created. f074c283e908c4fd2bc04150023383608b7baefc

BPS Git Server git at git.bestpractical.com
Fri Jan 28 20:42:53 UTC 2022


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "app-wsgetmail".

The branch, prep-0.05-release has been created
        at  f074c283e908c4fd2bc04150023383608b7baefc (commit)

- Log -----------------------------------------------------------------
commit f074c283e908c4fd2bc04150023383608b7baefc
Author: Brett Smith <brett at bestpractical.com>
Date:   Fri Jan 28 15:32:31 2022 -0500

    Release version 0.05

diff --git a/Changes b/Changes
index 27a51fc..a258d2a 100644
--- a/Changes
+++ b/Changes
@@ -1,5 +1,18 @@
 Revision history for App-wsgetmail
 
+0.05    24/1/22
+        Major documentation overhaul:
+        * The script pod provides full details about all available
+          configuration options, and refers to Microsoft documentation about
+          how to set up the client application
+        * Library pod documents attributes and methods of all classes
+        Add missing dependencies to Makefile.PL
+        Add MANIFEST.SKIP to support release tests
+        Provide interpreter substitution for the wsgetmail script
+
+0.04    18/8/20
+        Documentation improvements
+
 0.03    4/8/20
         Improved debugging
 
diff --git a/META.yml b/META.yml
index fb58366..f481d07 100644
--- a/META.yml
+++ b/META.yml
@@ -3,9 +3,9 @@ abstract: 'Fetch mail from the cloud using webservices'
 author:
   - 'Best Practical Solutions, LLC <modules at bestpractical.com>'
 build_requires:
-  ExtUtils::MakeMaker: 6.59
+  ExtUtils::MakeMaker: 6.36
 configure_requires:
-  ExtUtils::MakeMaker: 6.59
+  ExtUtils::MakeMaker: 6.36
 distribution_type: module
 dynamic_config: 1
 generated_by: 'Module::Install version 1.19'
@@ -13,25 +13,31 @@ license: gpl_2
 meta-spec:
   url: http://module-build.sourceforge.net/META-spec-v1.4.html
   version: 1.4
+module_name: App::wsgetmail
 name: App-wsgetmail
 no_index:
   directory:
     - inc
     - t
-    - xt
 requires:
   Azure::AD::ClientCredentials: 0
+  Clone: 0
+  File::Slurp: 0
+  File::Temp: 0
+  FindBin: 0
+  Getopt::Long: 0
   IPC::Run: 0
   JSON: 0
   LWP::UserAgent: '6.42'
+  Module::Load: 0
   Moo: 0
+  Pod::Usage: 0
   Test::LWP::UserAgent: 0
   Test::More: 0
   URI: 0
   URI::Escape: 0
-  perl: 5.10.1
+  strict: 0
+  warnings: 0
 resources:
   license: http://opensource.org/licenses/gpl-license.php
-version: '0.01'
-x_module_install_rtx_version: '0.41'
-x_requires_rt: 4.0.0
+version: '0.05'
diff --git a/lib/App/wsgetmail.pm b/lib/App/wsgetmail.pm
index e84bc1f..5a3f91f 100644
--- a/lib/App/wsgetmail.pm
+++ b/lib/App/wsgetmail.pm
@@ -50,7 +50,7 @@ package App::wsgetmail;
 
 use Moo;
 
-our $VERSION = '0.03';
+our $VERSION = '0.05';
 
 =head1 NAME
 
@@ -58,7 +58,7 @@ App::wsgetmail - Fetch mail from the cloud using webservices
 
 =head1 VERSION
 
-0.03
+0.05
 
 =head1 SYNOPSIS
 

commit 9531bfdf7b8723ced75d70b1f2f6606496b9c924
Author: Brett Smith <brett at bestpractical.com>
Date:   Mon Jan 24 15:45:03 2022 -0500

    Add license and headers throughout

diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..e77696a
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,339 @@
+		    GNU GENERAL PUBLIC LICENSE
+		       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+                          675 Mass Ave, Cambridge, MA 02139, USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+			    Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+

+		    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+

+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+

+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+

+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+			    NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+		     END OF TERMS AND CONDITIONS
+

+	    How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) 19yy  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) 19yy name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/MANIFEST b/MANIFEST
index 2280c25..a05f037 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -1,5 +1,6 @@
 bin/wsgetmail.in
 Changes
+COPYING
 inc/Module/AutoInstall.pm
 inc/Module/Install.pm
 inc/Module/Install/AutoInstall.pm
diff --git a/bin/wsgetmail.in b/bin/wsgetmail.in
index a8703d4..8c2feae 100755
--- a/bin/wsgetmail.in
+++ b/bin/wsgetmail.in
@@ -1,5 +1,53 @@
 #!/usr/bin/env perl
 ### before: #!@PERL@
+#
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 2020-2022 Best Practical Solutions, LLC
+#                                          <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
 
 use strict;
 use FindBin;
diff --git a/lib/App/wsgetmail.pm b/lib/App/wsgetmail.pm
index 1e4023b..e84bc1f 100644
--- a/lib/App/wsgetmail.pm
+++ b/lib/App/wsgetmail.pm
@@ -1,3 +1,51 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 2020-2022 Best Practical Solutions, LLC
+#                                          <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
 package App::wsgetmail;
 
 use Moo;
diff --git a/lib/App/wsgetmail/MDA.pm b/lib/App/wsgetmail/MDA.pm
index 32620a5..b882696 100644
--- a/lib/App/wsgetmail/MDA.pm
+++ b/lib/App/wsgetmail/MDA.pm
@@ -1,3 +1,51 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 2020-2022 Best Practical Solutions, LLC
+#                                          <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
 =head1 NAME
 
 App::wsgetmail::MDA - Deliver mail to another command's standard input
diff --git a/lib/App/wsgetmail/MS365.pm b/lib/App/wsgetmail/MS365.pm
index 17d7c43..286b9c3 100644
--- a/lib/App/wsgetmail/MS365.pm
+++ b/lib/App/wsgetmail/MS365.pm
@@ -1,3 +1,51 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 2020-2022 Best Practical Solutions, LLC
+#                                          <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
 package App::wsgetmail::MS365;
 
 =head1 NAME
diff --git a/lib/App/wsgetmail/MS365/Client.pm b/lib/App/wsgetmail/MS365/Client.pm
index 0abba71..a69b408 100644
--- a/lib/App/wsgetmail/MS365/Client.pm
+++ b/lib/App/wsgetmail/MS365/Client.pm
@@ -1,3 +1,51 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 2020-2022 Best Practical Solutions, LLC
+#                                          <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
 package App::wsgetmail::MS365::Client;
 
 =head1 NAME
diff --git a/lib/App/wsgetmail/MS365/Message.pm b/lib/App/wsgetmail/MS365/Message.pm
index 5741cf6..f8a694c 100644
--- a/lib/App/wsgetmail/MS365/Message.pm
+++ b/lib/App/wsgetmail/MS365/Message.pm
@@ -1,3 +1,51 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 2020-2022 Best Practical Solutions, LLC
+#                                          <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
 package App::wsgetmail::MS365::Message;
 use Moo;
 

commit 069141c210ad967b3e6666259ff2b94555258d17
Author: Brett Smith <brett at bestpractical.com>
Date:   Mon Jan 24 15:36:28 2022 -0500

    Reword headline to match following pod text

diff --git a/README.md b/README.md
index 7b6ff58..844db3a 100644
--- a/README.md
+++ b/README.md
@@ -229,7 +229,7 @@ periodically through cron or a systemd service on a timer.
 
 # LIMITATIONS
 
-## Fetching from Multiple Accounts
+## Fetching from Multiple Folders
 
 wsgetmail can only read from a single folder each time it runs. If you need
 to read multiple folders (possibly spanning different accounts), then you
diff --git a/bin/wsgetmail.in b/bin/wsgetmail.in
index fa3a1da..a8703d4 100755
--- a/bin/wsgetmail.in
+++ b/bin/wsgetmail.in
@@ -314,7 +314,7 @@ periodically through cron or a systemd service on a timer.
 
 =head1 LIMITATIONS
 
-=head2 Fetching from Multiple Accounts
+=head2 Fetching from Multiple Folders
 
 wsgetmail can only read from a single folder each time it runs. If you need
 to read multiple folders (possibly spanning different accounts), then you

commit 5110d9cb98e73cd3b160c00839ae7e146038a6bc
Author: Brett Smith <brett at bestpractical.com>
Date:   Mon Jan 24 15:22:53 2022 -0500

    Fix minor grammar typo in wsgetmail pod

diff --git a/README.md b/README.md
index 2704063..7b6ff58 100644
--- a/README.md
+++ b/README.md
@@ -80,7 +80,7 @@ Two authentication methods are supported:
 
 - Client Credentials
 
-    This method uses shared secrets and is preferred method by Microsoft.
+    This method uses shared secrets and is preferred by Microsoft.
     (See [Client credentials](https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-authentication-flows#client-credentials))
 
 - Username/password
diff --git a/bin/wsgetmail.in b/bin/wsgetmail.in
index 8db9b0b..fa3a1da 100755
--- a/bin/wsgetmail.in
+++ b/bin/wsgetmail.in
@@ -138,7 +138,7 @@ Two authentication methods are supported:
 
 =item Client Credentials
 
-This method uses shared secrets and is preferred method by Microsoft.
+This method uses shared secrets and is preferred by Microsoft.
 (See L<Client credentials|https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-authentication-flows#client-credentials>)
 
 =item Username/password

commit 1041124254e53caa91199cc94ecf33e5a638bcd4
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Fri Jan 21 14:25:42 2022 -0500

    Sync up Module::Install files

diff --git a/inc/Module/Install/Scripts.pm b/inc/Module/Install/Scripts.pm
index 2fc8f32..8c80fcd 100644
--- a/inc/Module/Install/Scripts.pm
+++ b/inc/Module/Install/Scripts.pm
@@ -1,3 +1,4 @@
+#line 1
 package Module::Install::Scripts;
 
 use strict 'vars';
diff --git a/inc/YAML/Tiny.pm b/inc/YAML/Tiny.pm
deleted file mode 100644
index fb157a6..0000000
--- a/inc/YAML/Tiny.pm
+++ /dev/null
@@ -1,872 +0,0 @@
-#line 1
-use 5.008001; # sane UTF-8 support
-use strict;
-use warnings;
-package YAML::Tiny; # git description: v1.72-7-g8682f63
-# XXX-INGY is 5.8.1 too old/broken for utf8?
-# XXX-XDG Lancaster consensus was that it was sufficient until
-# proven otherwise
-
-our $VERSION = '1.73';
-
-#####################################################################
-# The YAML::Tiny API.
-#
-# These are the currently documented API functions/methods and
-# exports:
-
-use Exporter;
-our @ISA       = qw{ Exporter  };
-our @EXPORT    = qw{ Load Dump };
-our @EXPORT_OK = qw{ LoadFile DumpFile freeze thaw };
-
-###
-# Functional/Export API:
-
-sub Dump {
-    return YAML::Tiny->new(@_)->_dump_string;
-}
-
-# XXX-INGY Returning last document seems a bad behavior.
-# XXX-XDG I think first would seem more natural, but I don't know
-# that it's worth changing now
-sub Load {
-    my $self = YAML::Tiny->_load_string(@_);
-    if ( wantarray ) {
-        return @$self;
-    } else {
-        # To match YAML.pm, return the last document
-        return $self->[-1];
-    }
-}
-
-# XXX-INGY Do we really need freeze and thaw?
-# XXX-XDG I don't think so.  I'd support deprecating them.
-BEGIN {
-    *freeze = \&Dump;
-    *thaw   = \&Load;
-}
-
-sub DumpFile {
-    my $file = shift;
-    return YAML::Tiny->new(@_)->_dump_file($file);
-}
-
-sub LoadFile {
-    my $file = shift;
-    my $self = YAML::Tiny->_load_file($file);
-    if ( wantarray ) {
-        return @$self;
-    } else {
-        # Return only the last document to match YAML.pm,
-        return $self->[-1];
-    }
-}
-
-
-###
-# Object Oriented API:
-
-# Create an empty YAML::Tiny object
-# XXX-INGY Why do we use ARRAY object?
-# NOTE: I get it now, but I think it's confusing and not needed.
-# Will change it on a branch later, for review.
-#
-# XXX-XDG I don't support changing it yet.  It's a very well-documented
-# "API" of YAML::Tiny.  I'd support deprecating it, but Adam suggested
-# we not change it until YAML.pm's own OO API is established so that
-# users only have one API change to digest, not two
-sub new {
-    my $class = shift;
-    bless [ @_ ], $class;
-}
-
-# XXX-INGY It probably doesn't matter, and it's probably too late to
-# change, but 'read/write' are the wrong names. Read and Write
-# are actions that take data from storage to memory
-# characters/strings. These take the data to/from storage to native
-# Perl objects, which the terms dump and load are meant. As long as
-# this is a legacy quirk to YAML::Tiny it's ok, but I'd prefer not
-# to add new {read,write}_* methods to this API.
-
-sub read_string {
-    my $self = shift;
-    $self->_load_string(@_);
-}
-
-sub write_string {
-    my $self = shift;
-    $self->_dump_string(@_);
-}
-
-sub read {
-    my $self = shift;
-    $self->_load_file(@_);
-}
-
-sub write {
-    my $self = shift;
-    $self->_dump_file(@_);
-}
-
-
-
-
-#####################################################################
-# Constants
-
-# Printed form of the unprintable characters in the lowest range
-# of ASCII characters, listed by ASCII ordinal position.
-my @UNPRINTABLE = qw(
-    0    x01  x02  x03  x04  x05  x06  a
-    b    t    n    v    f    r    x0E  x0F
-    x10  x11  x12  x13  x14  x15  x16  x17
-    x18  x19  x1A  e    x1C  x1D  x1E  x1F
-);
-
-# Printable characters for escapes
-my %UNESCAPES = (
-    0 => "\x00", z => "\x00", N    => "\x85",
-    a => "\x07", b => "\x08", t    => "\x09",
-    n => "\x0a", v => "\x0b", f    => "\x0c",
-    r => "\x0d", e => "\x1b", '\\' => '\\',
-);
-
-# XXX-INGY
-# I(ngy) need to decide if these values should be quoted in
-# YAML::Tiny or not. Probably yes.
-
-# These 3 values have special meaning when unquoted and using the
-# default YAML schema. They need quotes if they are strings.
-my %QUOTE = map { $_ => 1 } qw{
-    null true false
-};
-
-# The commented out form is simpler, but overloaded the Perl regex
-# engine due to recursion and backtracking problems on strings
-# larger than 32,000ish characters. Keep it for reference purposes.
-# qr/\"((?:\\.|[^\"])*)\"/
-my $re_capture_double_quoted = qr/\"([^\\"]*(?:\\.[^\\"]*)*)\"/;
-my $re_capture_single_quoted = qr/\'([^\']*(?:\'\'[^\']*)*)\'/;
-# unquoted re gets trailing space that needs to be stripped
-my $re_capture_unquoted_key  = qr/([^:]+(?::+\S(?:[^:]*|.*?(?=:)))*)(?=\s*\:(?:\s+|$))/;
-my $re_trailing_comment      = qr/(?:\s+\#.*)?/;
-my $re_key_value_separator   = qr/\s*:(?:\s+(?:\#.*)?|$)/;
-
-
-
-
-
-#####################################################################
-# YAML::Tiny Implementation.
-#
-# These are the private methods that do all the work. They may change
-# at any time.
-
-
-###
-# Loader functions:
-
-# Create an object from a file
-sub _load_file {
-    my $class = ref $_[0] ? ref shift : shift;
-
-    # Check the file
-    my $file = shift or $class->_error( 'You did not specify a file name' );
-    $class->_error( "File '$file' does not exist" )
-        unless -e $file;
-    $class->_error( "'$file' is a directory, not a file" )
-        unless -f _;
-    $class->_error( "Insufficient permissions to read '$file'" )
-        unless -r _;
-
-    # Open unbuffered with strict UTF-8 decoding and no translation layers
-    open( my $fh, "<:unix:encoding(UTF-8)", $file );
-    unless ( $fh ) {
-        $class->_error("Failed to open file '$file': $!");
-    }
-
-    # flock if available (or warn if not possible for OS-specific reasons)
-    if ( _can_flock() ) {
-        flock( $fh, Fcntl::LOCK_SH() )
-            or warn "Couldn't lock '$file' for reading: $!";
-    }
-
-    # slurp the contents
-    my $contents = eval {
-        use warnings FATAL => 'utf8';
-        local $/;
-        <$fh>
-    };
-    if ( my $err = $@ ) {
-        $class->_error("Error reading from file '$file': $err");
-    }
-
-    # close the file (release the lock)
-    unless ( close $fh ) {
-        $class->_error("Failed to close file '$file': $!");
-    }
-
-    $class->_load_string( $contents );
-}
-
-# Create an object from a string
-sub _load_string {
-    my $class  = ref $_[0] ? ref shift : shift;
-    my $self   = bless [], $class;
-    my $string = $_[0];
-    eval {
-        unless ( defined $string ) {
-            die \"Did not provide a string to load";
-        }
-
-        # Check if Perl has it marked as characters, but it's internally
-        # inconsistent.  E.g. maybe latin1 got read on a :utf8 layer
-        if ( utf8::is_utf8($string) && ! utf8::valid($string) ) {
-            die \<<'...';
-Read an invalid UTF-8 string (maybe mixed UTF-8 and 8-bit character set).
-Did you decode with lax ":utf8" instead of strict ":encoding(UTF-8)"?
-...
-        }
-
-        # Ensure Unicode character semantics, even for 0x80-0xff
-        utf8::upgrade($string);
-
-        # Check for and strip any leading UTF-8 BOM
-        $string =~ s/^\x{FEFF}//;
-
-        # Check for some special cases
-        return $self unless length $string;
-
-        # Split the file into lines
-        my @lines = grep { ! /^\s*(?:\#.*)?\z/ }
-                split /(?:\015{1,2}\012|\015|\012)/, $string;
-
-        # Strip the initial YAML header
-        @lines and $lines[0] =~ /^\%YAML[: ][\d\.]+.*\z/ and shift @lines;
-
-        # A nibbling parser
-        my $in_document = 0;
-        while ( @lines ) {
-            # Do we have a document header?
-            if ( $lines[0] =~ /^---\s*(?:(.+)\s*)?\z/ ) {
-                # Handle scalar documents
-                shift @lines;
-                if ( defined $1 and $1 !~ /^(?:\#.+|\%YAML[: ][\d\.]+)\z/ ) {
-                    push @$self,
-                        $self->_load_scalar( "$1", [ undef ], \@lines );
-                    next;
-                }
-                $in_document = 1;
-            }
-
-            if ( ! @lines or $lines[0] =~ /^(?:---|\.\.\.)/ ) {
-                # A naked document
-                push @$self, undef;
-                while ( @lines and $lines[0] !~ /^---/ ) {
-                    shift @lines;
-                }
-                $in_document = 0;
-
-            # XXX The final '-+$' is to look for -- which ends up being an
-            # error later.
-            } elsif ( ! $in_document && @$self ) {
-                # only the first document can be explicit
-                die \"YAML::Tiny failed to classify the line '$lines[0]'";
-            } elsif ( $lines[0] =~ /^\s*\-(?:\s|$|-+$)/ ) {
-                # An array at the root
-                my $document = [ ];
-                push @$self, $document;
-                $self->_load_array( $document, [ 0 ], \@lines );
-
-            } elsif ( $lines[0] =~ /^(\s*)\S/ ) {
-                # A hash at the root
-                my $document = { };
-                push @$self, $document;
-                $self->_load_hash( $document, [ length($1) ], \@lines );
-
-            } else {
-                # Shouldn't get here.  @lines have whitespace-only lines
-                # stripped, and previous match is a line with any
-                # non-whitespace.  So this clause should only be reachable via
-                # a perlbug where \s is not symmetric with \S
-
-                # uncoverable statement
-                die \"YAML::Tiny failed to classify the line '$lines[0]'";
-            }
-        }
-    };
-    my $err = $@;
-    if ( ref $err eq 'SCALAR' ) {
-        $self->_error(${$err});
-    } elsif ( $err ) {
-        $self->_error($err);
-    }
-
-    return $self;
-}
-
-sub _unquote_single {
-    my ($self, $string) = @_;
-    return '' unless length $string;
-    $string =~ s/\'\'/\'/g;
-    return $string;
-}
-
-sub _unquote_double {
-    my ($self, $string) = @_;
-    return '' unless length $string;
-    $string =~ s/\\"/"/g;
-    $string =~
-        s{\\([Nnever\\fartz0b]|x([0-9a-fA-F]{2}))}
-         {(length($1)>1)?pack("H2",$2):$UNESCAPES{$1}}gex;
-    return $string;
-}
-
-# Load a YAML scalar string to the actual Perl scalar
-sub _load_scalar {
-    my ($self, $string, $indent, $lines) = @_;
-
-    # Trim trailing whitespace
-    $string =~ s/\s*\z//;
-
-    # Explitic null/undef
-    return undef if $string eq '~';
-
-    # Single quote
-    if ( $string =~ /^$re_capture_single_quoted$re_trailing_comment\z/ ) {
-        return $self->_unquote_single($1);
-    }
-
-    # Double quote.
-    if ( $string =~ /^$re_capture_double_quoted$re_trailing_comment\z/ ) {
-        return $self->_unquote_double($1);
-    }
-
-    # Special cases
-    if ( $string =~ /^[\'\"!&]/ ) {
-        die \"YAML::Tiny does not support a feature in line '$string'";
-    }
-    return {} if $string =~ /^{}(?:\s+\#.*)?\z/;
-    return [] if $string =~ /^\[\](?:\s+\#.*)?\z/;
-
-    # Regular unquoted string
-    if ( $string !~ /^[>|]/ ) {
-        die \"YAML::Tiny found illegal characters in plain scalar: '$string'"
-            if $string =~ /^(?:-(?:\s|$)|[\@\%\`])/ or
-                $string =~ /:(?:\s|$)/;
-        $string =~ s/\s+#.*\z//;
-        return $string;
-    }
-
-    # Error
-    die \"YAML::Tiny failed to find multi-line scalar content" unless @$lines;
-
-    # Check the indent depth
-    $lines->[0]   =~ /^(\s*)/;
-    $indent->[-1] = length("$1");
-    if ( defined $indent->[-2] and $indent->[-1] <= $indent->[-2] ) {
-        die \"YAML::Tiny found bad indenting in line '$lines->[0]'";
-    }
-
-    # Pull the lines
-    my @multiline = ();
-    while ( @$lines ) {
-        $lines->[0] =~ /^(\s*)/;
-        last unless length($1) >= $indent->[-1];
-        push @multiline, substr(shift(@$lines), $indent->[-1]);
-    }
-
-    my $j = (substr($string, 0, 1) eq '>') ? ' ' : "\n";
-    my $t = (substr($string, 1, 1) eq '-') ? ''  : "\n";
-    return join( $j, @multiline ) . $t;
-}
-
-# Load an array
-sub _load_array {
-    my ($self, $array, $indent, $lines) = @_;
-
-    while ( @$lines ) {
-        # Check for a new document
-        if ( $lines->[0] =~ /^(?:---|\.\.\.)/ ) {
-            while ( @$lines and $lines->[0] !~ /^---/ ) {
-                shift @$lines;
-            }
-            return 1;
-        }
-
-        # Check the indent level
-        $lines->[0] =~ /^(\s*)/;
-        if ( length($1) < $indent->[-1] ) {
-            return 1;
-        } elsif ( length($1) > $indent->[-1] ) {
-            die \"YAML::Tiny found bad indenting in line '$lines->[0]'";
-        }
-
-        if ( $lines->[0] =~ /^(\s*\-\s+)[^\'\"]\S*\s*:(?:\s+|$)/ ) {
-            # Inline nested hash
-            my $indent2 = length("$1");
-            $lines->[0] =~ s/-/ /;
-            push @$array, { };
-            $self->_load_hash( $array->[-1], [ @$indent, $indent2 ], $lines );
-
-        } elsif ( $lines->[0] =~ /^\s*\-\s*\z/ ) {
-            shift @$lines;
-            unless ( @$lines ) {
-                push @$array, undef;
-                return 1;
-            }
-            if ( $lines->[0] =~ /^(\s*)\-/ ) {
-                my $indent2 = length("$1");
-                if ( $indent->[-1] == $indent2 ) {
-                    # Null array entry
-                    push @$array, undef;
-                } else {
-                    # Naked indenter
-                    push @$array, [ ];
-                    $self->_load_array(
-                        $array->[-1], [ @$indent, $indent2 ], $lines
-                    );
-                }
-
-            } elsif ( $lines->[0] =~ /^(\s*)\S/ ) {
-                push @$array, { };
-                $self->_load_hash(
-                    $array->[-1], [ @$indent, length("$1") ], $lines
-                );
-
-            } else {
-                die \"YAML::Tiny failed to classify line '$lines->[0]'";
-            }
-
-        } elsif ( $lines->[0] =~ /^\s*\-(\s*)(.+?)\s*\z/ ) {
-            # Array entry with a value
-            shift @$lines;
-            push @$array, $self->_load_scalar(
-                "$2", [ @$indent, undef ], $lines
-            );
-
-        } elsif ( defined $indent->[-2] and $indent->[-1] == $indent->[-2] ) {
-            # This is probably a structure like the following...
-            # ---
-            # foo:
-            # - list
-            # bar: value
-            #
-            # ... so lets return and let the hash parser handle it
-            return 1;
-
-        } else {
-            die \"YAML::Tiny failed to classify line '$lines->[0]'";
-        }
-    }
-
-    return 1;
-}
-
-# Load a hash
-sub _load_hash {
-    my ($self, $hash, $indent, $lines) = @_;
-
-    while ( @$lines ) {
-        # Check for a new document
-        if ( $lines->[0] =~ /^(?:---|\.\.\.)/ ) {
-            while ( @$lines and $lines->[0] !~ /^---/ ) {
-                shift @$lines;
-            }
-            return 1;
-        }
-
-        # Check the indent level
-        $lines->[0] =~ /^(\s*)/;
-        if ( length($1) < $indent->[-1] ) {
-            return 1;
-        } elsif ( length($1) > $indent->[-1] ) {
-            die \"YAML::Tiny found bad indenting in line '$lines->[0]'";
-        }
-
-        # Find the key
-        my $key;
-
-        # Quoted keys
-        if ( $lines->[0] =~
-            s/^\s*$re_capture_single_quoted$re_key_value_separator//
-        ) {
-            $key = $self->_unquote_single($1);
-        }
-        elsif ( $lines->[0] =~
-            s/^\s*$re_capture_double_quoted$re_key_value_separator//
-        ) {
-            $key = $self->_unquote_double($1);
-        }
-        elsif ( $lines->[0] =~
-            s/^\s*$re_capture_unquoted_key$re_key_value_separator//
-        ) {
-            $key = $1;
-            $key =~ s/\s+$//;
-        }
-        elsif ( $lines->[0] =~ /^\s*\?/ ) {
-            die \"YAML::Tiny does not support a feature in line '$lines->[0]'";
-        }
-        else {
-            die \"YAML::Tiny failed to classify line '$lines->[0]'";
-        }
-
-        if ( exists $hash->{$key} ) {
-            warn "YAML::Tiny found a duplicate key '$key' in line '$lines->[0]'";
-        }
-
-        # Do we have a value?
-        if ( length $lines->[0] ) {
-            # Yes
-            $hash->{$key} = $self->_load_scalar(
-                shift(@$lines), [ @$indent, undef ], $lines
-            );
-        } else {
-            # An indent
-            shift @$lines;
-            unless ( @$lines ) {
-                $hash->{$key} = undef;
-                return 1;
-            }
-            if ( $lines->[0] =~ /^(\s*)-/ ) {
-                $hash->{$key} = [];
-                $self->_load_array(
-                    $hash->{$key}, [ @$indent, length($1) ], $lines
-                );
-            } elsif ( $lines->[0] =~ /^(\s*)./ ) {
-                my $indent2 = length("$1");
-                if ( $indent->[-1] >= $indent2 ) {
-                    # Null hash entry
-                    $hash->{$key} = undef;
-                } else {
-                    $hash->{$key} = {};
-                    $self->_load_hash(
-                        $hash->{$key}, [ @$indent, length($1) ], $lines
-                    );
-                }
-            }
-        }
-    }
-
-    return 1;
-}
-
-
-###
-# Dumper functions:
-
-# Save an object to a file
-sub _dump_file {
-    my $self = shift;
-
-    require Fcntl;
-
-    # Check the file
-    my $file = shift or $self->_error( 'You did not specify a file name' );
-
-    my $fh;
-    # flock if available (or warn if not possible for OS-specific reasons)
-    if ( _can_flock() ) {
-        # Open without truncation (truncate comes after lock)
-        my $flags = Fcntl::O_WRONLY()|Fcntl::O_CREAT();
-        sysopen( $fh, $file, $flags )
-            or $self->_error("Failed to open file '$file' for writing: $!");
-
-        # Use no translation and strict UTF-8
-        binmode( $fh, ":raw:encoding(UTF-8)");
-
-        flock( $fh, Fcntl::LOCK_EX() )
-            or warn "Couldn't lock '$file' for reading: $!";
-
-        # truncate and spew contents
-        truncate $fh, 0;
-        seek $fh, 0, 0;
-    }
-    else {
-        open $fh, ">:unix:encoding(UTF-8)", $file;
-    }
-
-    # serialize and spew to the handle
-    print {$fh} $self->_dump_string;
-
-    # close the file (release the lock)
-    unless ( close $fh ) {
-        $self->_error("Failed to close file '$file': $!");
-    }
-
-    return 1;
-}
-
-# Save an object to a string
-sub _dump_string {
-    my $self = shift;
-    return '' unless ref $self && @$self;
-
-    # Iterate over the documents
-    my $indent = 0;
-    my @lines  = ();
-
-    eval {
-        foreach my $cursor ( @$self ) {
-            push @lines, '---';
-
-            # An empty document
-            if ( ! defined $cursor ) {
-                # Do nothing
-
-            # A scalar document
-            } elsif ( ! ref $cursor ) {
-                $lines[-1] .= ' ' . $self->_dump_scalar( $cursor );
-
-            # A list at the root
-            } elsif ( ref $cursor eq 'ARRAY' ) {
-                unless ( @$cursor ) {
-                    $lines[-1] .= ' []';
-                    next;
-                }
-                push @lines, $self->_dump_array( $cursor, $indent, {} );
-
-            # A hash at the root
-            } elsif ( ref $cursor eq 'HASH' ) {
-                unless ( %$cursor ) {
-                    $lines[-1] .= ' {}';
-                    next;
-                }
-                push @lines, $self->_dump_hash( $cursor, $indent, {} );
-
-            } else {
-                die \("Cannot serialize " . ref($cursor));
-            }
-        }
-    };
-    if ( ref $@ eq 'SCALAR' ) {
-        $self->_error(${$@});
-    } elsif ( $@ ) {
-        $self->_error($@);
-    }
-
-    join '', map { "$_\n" } @lines;
-}
-
-sub _has_internal_string_value {
-    my $value = shift;
-    my $b_obj = B::svref_2object(\$value);  # for round trip problem
-    return $b_obj->FLAGS & B::SVf_POK();
-}
-
-sub _dump_scalar {
-    my $string = $_[1];
-    my $is_key = $_[2];
-    # Check this before checking length or it winds up looking like a string!
-    my $has_string_flag = _has_internal_string_value($string);
-    return '~'  unless defined $string;
-    return "''" unless length  $string;
-    if (Scalar::Util::looks_like_number($string)) {
-        # keys and values that have been used as strings get quoted
-        if ( $is_key || $has_string_flag ) {
-            return qq['$string'];
-        }
-        else {
-            return $string;
-        }
-    }
-    if ( $string =~ /[\x00-\x09\x0b-\x0d\x0e-\x1f\x7f-\x9f\'\n]/ ) {
-        $string =~ s/\\/\\\\/g;
-        $string =~ s/"/\\"/g;
-        $string =~ s/\n/\\n/g;
-        $string =~ s/[\x85]/\\N/g;
-        $string =~ s/([\x00-\x1f])/\\$UNPRINTABLE[ord($1)]/g;
-        $string =~ s/([\x7f-\x9f])/'\x' . sprintf("%X",ord($1))/ge;
-        return qq|"$string"|;
-    }
-    if ( $string =~ /(?:^[~!@#%&*|>?:,'"`{}\[\]]|^-+$|\s|:\z)/ or
-        $QUOTE{$string}
-    ) {
-        return "'$string'";
-    }
-    return $string;
-}
-
-sub _dump_array {
-    my ($self, $array, $indent, $seen) = @_;
-    if ( $seen->{refaddr($array)}++ ) {
-        die \"YAML::Tiny does not support circular references";
-    }
-    my @lines  = ();
-    foreach my $el ( @$array ) {
-        my $line = ('  ' x $indent) . '-';
-        my $type = ref $el;
-        if ( ! $type ) {
-            $line .= ' ' . $self->_dump_scalar( $el );
-            push @lines, $line;
-
-        } elsif ( $type eq 'ARRAY' ) {
-            if ( @$el ) {
-                push @lines, $line;
-                push @lines, $self->_dump_array( $el, $indent + 1, $seen );
-            } else {
-                $line .= ' []';
-                push @lines, $line;
-            }
-
-        } elsif ( $type eq 'HASH' ) {
-            if ( keys %$el ) {
-                push @lines, $line;
-                push @lines, $self->_dump_hash( $el, $indent + 1, $seen );
-            } else {
-                $line .= ' {}';
-                push @lines, $line;
-            }
-
-        } else {
-            die \"YAML::Tiny does not support $type references";
-        }
-    }
-
-    @lines;
-}
-
-sub _dump_hash {
-    my ($self, $hash, $indent, $seen) = @_;
-    if ( $seen->{refaddr($hash)}++ ) {
-        die \"YAML::Tiny does not support circular references";
-    }
-    my @lines  = ();
-    foreach my $name ( sort keys %$hash ) {
-        my $el   = $hash->{$name};
-        my $line = ('  ' x $indent) . $self->_dump_scalar($name, 1) . ":";
-        my $type = ref $el;
-        if ( ! $type ) {
-            $line .= ' ' . $self->_dump_scalar( $el );
-            push @lines, $line;
-
-        } elsif ( $type eq 'ARRAY' ) {
-            if ( @$el ) {
-                push @lines, $line;
-                push @lines, $self->_dump_array( $el, $indent + 1, $seen );
-            } else {
-                $line .= ' []';
-                push @lines, $line;
-            }
-
-        } elsif ( $type eq 'HASH' ) {
-            if ( keys %$el ) {
-                push @lines, $line;
-                push @lines, $self->_dump_hash( $el, $indent + 1, $seen );
-            } else {
-                $line .= ' {}';
-                push @lines, $line;
-            }
-
-        } else {
-            die \"YAML::Tiny does not support $type references";
-        }
-    }
-
-    @lines;
-}
-
-
-
-#####################################################################
-# DEPRECATED API methods:
-
-# Error storage (DEPRECATED as of 1.57)
-our $errstr    = '';
-
-# Set error
-sub _error {
-    require Carp;
-    $errstr = $_[1];
-    $errstr =~ s/ at \S+ line \d+.*//;
-    Carp::croak( $errstr );
-}
-
-# Retrieve error
-my $errstr_warned;
-sub errstr {
-    require Carp;
-    Carp::carp( "YAML::Tiny->errstr and \$YAML::Tiny::errstr is deprecated" )
-        unless $errstr_warned++;
-    $errstr;
-}
-
-
-
-
-#####################################################################
-# Helper functions. Possibly not needed.
-
-
-# Use to detect nv or iv
-use B;
-
-# XXX-INGY Is flock YAML::Tiny's responsibility?
-# Some platforms can't flock :-(
-# XXX-XDG I think it is.  When reading and writing files, we ought
-# to be locking whenever possible.  People (foolishly) use YAML
-# files for things like session storage, which has race issues.
-my $HAS_FLOCK;
-sub _can_flock {
-    if ( defined $HAS_FLOCK ) {
-        return $HAS_FLOCK;
-    }
-    else {
-        require Config;
-        my $c = \%Config::Config;
-        $HAS_FLOCK = grep { $c->{$_} } qw/d_flock d_fcntl_can_lock d_lockf/;
-        require Fcntl if $HAS_FLOCK;
-        return $HAS_FLOCK;
-    }
-}
-
-
-# XXX-INGY Is this core in 5.8.1? Can we remove this?
-# XXX-XDG Scalar::Util 1.18 didn't land until 5.8.8, so we need this
-#####################################################################
-# Use Scalar::Util if possible, otherwise emulate it
-
-use Scalar::Util ();
-BEGIN {
-    local $@;
-    if ( eval { Scalar::Util->VERSION(1.18); } ) {
-        *refaddr = *Scalar::Util::refaddr;
-    }
-    else {
-        eval <<'END_PERL';
-# Scalar::Util failed to load or too old
-sub refaddr {
-    my $pkg = ref($_[0]) or return undef;
-    if ( !! UNIVERSAL::can($_[0], 'can') ) {
-        bless $_[0], 'Scalar::Util::Fake';
-    } else {
-        $pkg = undef;
-    }
-    "$_[0]" =~ /0x(\w+)/;
-    my $i = do { no warnings 'portable'; hex $1 };
-    bless $_[0], $pkg if defined $pkg;
-    $i;
-}
-END_PERL
-    }
-}
-
-delete $YAML::Tiny::{refaddr};
-
-1;
-
-# XXX-INGY Doc notes I'm putting up here. Changing the doc when it's wrong
-# but leaving grey area stuff up here.
-#
-# I would like to change Read/Write to Load/Dump below without
-# changing the actual API names.
-#
-# It might be better to put Load/Dump API in the SYNOPSIS instead of the
-# dubious OO API.
-#
-# null and bool explanations may be outdated.
-
-__END__
-
-#line 1487

commit e6b0fd62b2809459b52115b85ffbf30a37bd5469
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Fri Jan 21 14:25:30 2022 -0500

    Update MANIFEST

diff --git a/MANIFEST b/MANIFEST
index 9e47ce4..2280c25 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -1,7 +1,8 @@
 bin/wsgetmail.in
 Changes
-inc/Module/Install.pm
 inc/Module/AutoInstall.pm
+inc/Module/Install.pm
+inc/Module/Install/AutoInstall.pm
 inc/Module/Install/Base.pm
 inc/Module/Install/Can.pm
 inc/Module/Install/Fetch.pm
@@ -9,12 +10,10 @@ inc/Module/Install/Include.pm
 inc/Module/Install/Makefile.pm
 inc/Module/Install/Metadata.pm
 inc/Module/Install/ReadmeFromPod.pm
-inc/Module/Install/Substitute.pm
 inc/Module/Install/Scripts.pm
+inc/Module/Install/Substitute.pm
 inc/Module/Install/Win32.pm
 inc/Module/Install/WriteAll.pm
-inc/Module/Install/AutoInstall.pm
-inc/YAML/Tiny.pm
 INSTALL.SKIP
 lib/App/wsgetmail.pm
 lib/App/wsgetmail/MDA.pm
@@ -28,4 +27,7 @@ META.yml
 README.md
 t/00-load.t
 t/basic.t
+t/manifest.t
 t/mock_responses/messages.json
+t/pod-coverage.t
+t/pod.t

commit 3a623cc8a373386c13329cd5d1487ec3aff0c08b
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Fri Jan 21 14:06:50 2022 -0500

    Add a few more links to MS documentation

diff --git a/README.md b/README.md
index 4ff01dc..2704063 100644
--- a/README.md
+++ b/README.md
@@ -27,12 +27,12 @@ where `wsgetmail.json` looks like:
 wsgetmail retrieves mail from a folder available through a web services API
 and delivers it to another system. Currently, it only knows how to retrieve
 mail from the Microsoft Graph API, and deliver it by running another command
-on the local system. It may grow to support other systems in the future.
+on the local system.
 
 # INSTALLATION
 
     perl Makefile.PL
-    make PERL_CANARY_STABILITY_NOPROMPT=1
+    make
     make test
     sudo make install
 
@@ -75,9 +75,21 @@ system Perl, or in the same directory as `perl` if you built your own.
 
 ## Configuring Microsoft 365 Client Access
 
-To use wsgetmail, first you need to set up the app in Microsoft 365. This
-section walks you through each piece of configuration wsgetmail needs, and
-how to obtain it.
+To use wsgetmail, first you need to set up the app in Microsoft 365.
+Two authentication methods are supported:
+
+- Client Credentials
+
+    This method uses shared secrets and is preferred method by Microsoft.
+    (See [Client credentials](https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-authentication-flows#client-credentials))
+
+- Username/password
+
+    This method is more like previous connections via IMAP. It is currently
+    supported by Microsoft, but not recommended. (See [Username/password](https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-authentication-flows#usernamepassword))
+
+This section walks you through each piece of configuration wsgetmail needs,
+and how to obtain it.
 
 - tenant\_id
 
@@ -149,9 +161,9 @@ a completely random string, not a UUID/GUID.
 ### Configuring user+password authentication
 
 If you do not want to use a client secret, you can also configure wsgetmail
-to authenticate with a traditional username+password combination. This is
-easier to set up initially, but harder to manage in the long run, because
-the password needs to be kept in sync across applications.
+to authenticate with a traditional username+password combination. As noted
+above, this method is not recommended by Microsoft. It also does not work
+for systems with federated authentication enabled.
 
 - global\_access
 
@@ -170,32 +182,32 @@ the password needs to be kept in sync across applications.
 ## Configuring the mail delivery command
 
 Now that you've configured wsgetmail to access a mail account, all that's
-left is configuring delivery.
+left is configuring delivery. Set the following in your wsgetmail
+configuration file.
 
 - folder
 
-    Set this to the name string of a mail folder to read in your wsgetmail
-    configuration file.
+    Set this to the name string of a mail folder to read.
 
 - command
 
-    Set this to executable command string in your wsgetmail configuration
-    file. You can specify an absolute path, or a plain command name which will
-    be found from `$PATH`. For each email wsgetmail retrieves, it will run this
-    command and pass the message data to it via standard input.
+    Set this to an executable command. You can specify an absolute path,
+    or a plain command name which will be found from `$PATH`. For each
+    email wsgetmail retrieves, it will run this command and pass the
+    message data to it via standard input.
 
 - command\_args
 
-    Set this to a string with additional arguments to call `command` with in
-    your wsgetmail configuration file. These arguments follow shell quoting
-    rules: you can escape characters with a backslash, and denote a single
-    string argument with single or double quotes.
+    Set this to a string with additional arguments to pass to `command`.
+    These arguments follow shell quoting rules: you can escape characters
+    with a backslash, and denote a single string argument with single or
+    double quotes.
 
 - action\_on\_fetched
 
-    Set this to a literal string `"mark_as_read"` or `"delete"` in your
-    wsgetmail configuration file. For each email wsgetmail retrieves, after the
-    configured delivery command succeeds, it will take this action on the message.
+    Set this to a literal string `"mark_as_read"` or `"delete"`.
+    For each email wsgetmail retrieves, after the configured delivery
+    command succeeds, it will take this action on the message.
 
     If you set this to `"mark_as_read"`, wsgetmail will only retrieve and
     deliver messages that are marked unread in the configured folder, so it does
@@ -217,6 +229,8 @@ periodically through cron or a systemd service on a timer.
 
 # LIMITATIONS
 
+## Fetching from Multiple Accounts
+
 wsgetmail can only read from a single folder each time it runs. If you need
 to read multiple folders (possibly spanning different accounts), then you
 need to run it multiple times with different configuration.
@@ -236,6 +250,13 @@ configuration, just run wsgetmail with different configurations:
     wsgetmail --config=account01.json
     wsgetmail --config=account02.json
 
+## Office 365 API Limits
+
+Microsoft applies some limits to the amount of API requests allowed as
+documented in their [Microsoft Graph throttling guidance](https://docs.microsoft.com/en-us/graph/throttling).
+If you reach a limit, requests to the API will start failing for a period
+of time.
+
 # AUTHOR
 
 Best Practical Solutions, LLC <modules at bestpractical.com>
diff --git a/bin/wsgetmail.in b/bin/wsgetmail.in
index b7ea893..8db9b0b 100755
--- a/bin/wsgetmail.in
+++ b/bin/wsgetmail.in
@@ -79,12 +79,12 @@ where C<wsgetmail.json> looks like:
 wsgetmail retrieves mail from a folder available through a web services API
 and delivers it to another system. Currently, it only knows how to retrieve
 mail from the Microsoft Graph API, and deliver it by running another command
-on the local system. It may grow to support other systems in the future.
+on the local system.
 
 =head1 INSTALLATION
 
     perl Makefile.PL
-    make PERL_CANARY_STABILITY_NOPROMPT=1
+    make
     make test
     sudo make install
 
@@ -131,9 +131,25 @@ Show this help documentation.
 
 =head2 Configuring Microsoft 365 Client Access
 
-To use wsgetmail, first you need to set up the app in Microsoft 365. This
-section walks you through each piece of configuration wsgetmail needs, and
-how to obtain it.
+To use wsgetmail, first you need to set up the app in Microsoft 365.
+Two authentication methods are supported:
+
+=over
+
+=item Client Credentials
+
+This method uses shared secrets and is preferred method by Microsoft.
+(See L<Client credentials|https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-authentication-flows#client-credentials>)
+
+=item Username/password
+
+This method is more like previous connections via IMAP. It is currently
+supported by Microsoft, but not recommended. (See L<Username/password|https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-authentication-flows#usernamepassword>)
+
+=back
+
+This section walks you through each piece of configuration wsgetmail needs,
+and how to obtain it.
 
 =over 4
 
@@ -222,9 +238,9 @@ address string in your wsgetmail configuration file.
 =head3 Configuring user+password authentication
 
 If you do not want to use a client secret, you can also configure wsgetmail
-to authenticate with a traditional username+password combination. This is
-easier to set up initially, but harder to manage in the long run, because
-the password needs to be kept in sync across applications.
+to authenticate with a traditional username+password combination. As noted
+above, this method is not recommended by Microsoft. It also does not work
+for systems with federated authentication enabled.
 
 =over 4
 
@@ -247,34 +263,34 @@ configuration file.
 =head2 Configuring the mail delivery command
 
 Now that you've configured wsgetmail to access a mail account, all that's
-left is configuring delivery.
+left is configuring delivery. Set the following in your wsgetmail
+configuration file.
 
 =over 4
 
 =item folder
 
-Set this to the name string of a mail folder to read in your wsgetmail
-configuration file.
+Set this to the name string of a mail folder to read.
 
 =item command
 
-Set this to executable command string in your wsgetmail configuration
-file. You can specify an absolute path, or a plain command name which will
-be found from C<$PATH>. For each email wsgetmail retrieves, it will run this
-command and pass the message data to it via standard input.
+Set this to an executable command. You can specify an absolute path,
+or a plain command name which will be found from C<$PATH>. For each
+email wsgetmail retrieves, it will run this command and pass the
+message data to it via standard input.
 
 =item command_args
 
-Set this to a string with additional arguments to call C<command> with in
-your wsgetmail configuration file. These arguments follow shell quoting
-rules: you can escape characters with a backslash, and denote a single
-string argument with single or double quotes.
+Set this to a string with additional arguments to pass to C<command>.
+These arguments follow shell quoting rules: you can escape characters
+with a backslash, and denote a single string argument with single or
+double quotes.
 
 =item action_on_fetched
 
-Set this to a literal string C<"mark_as_read"> or C<"delete"> in your
-wsgetmail configuration file. For each email wsgetmail retrieves, after the
-configured delivery command succeeds, it will take this action on the message.
+Set this to a literal string C<"mark_as_read"> or C<"delete">.
+For each email wsgetmail retrieves, after the configured delivery
+command succeeds, it will take this action on the message.
 
 If you set this to C<"mark_as_read">, wsgetmail will only retrieve and
 deliver messages that are marked unread in the configured folder, so it does
@@ -298,6 +314,8 @@ periodically through cron or a systemd service on a timer.
 
 =head1 LIMITATIONS
 
+=head2 Fetching from Multiple Accounts
+
 wsgetmail can only read from a single folder each time it runs. If you need
 to read multiple folders (possibly spanning different accounts), then you
 need to run it multiple times with different configuration.
@@ -317,6 +335,13 @@ configuration, just run wsgetmail with different configurations:
     wsgetmail --config=account01.json
     wsgetmail --config=account02.json
 
+=head2 Office 365 API Limits
+
+Microsoft applies some limits to the amount of API requests allowed as
+documented in their L<Microsoft Graph throttling guidance|https://docs.microsoft.com/en-us/graph/throttling>.
+If you reach a limit, requests to the API will start failing for a period
+of time.
+
 =head1 AUTHOR
 
 Best Practical Solutions, LLC <modules at bestpractical.com>

commit 735fbb927544e25707db657b1d47a6ae274f1cbe
Author: Brett Smith <brett at bestpractical.com>
Date:   Fri Jan 28 15:36:02 2022 -0500

    Remove boilerplate test
    
    At this point, we've changed enough that this test is unlikely to fail
    ever again, and it lets us get rid of the xt directory completely.

diff --git a/xt/boilerplate.t b/xt/boilerplate.t
deleted file mode 100644
index 5040b18..0000000
--- a/xt/boilerplate.t
+++ /dev/null
@@ -1,57 +0,0 @@
-#!perl -T
-use 5.006;
-use strict;
-use warnings;
-use Test::More;
-
-plan tests => 3;
-
-sub not_in_file_ok {
-    my ($filename, %regex) = @_;
-    open( my $fh, '<', $filename )
-        or die "couldn't open $filename for reading: $!";
-
-    my %violated;
-
-    while (my $line = <$fh>) {
-        while (my ($desc, $regex) = each %regex) {
-            if ($line =~ $regex) {
-                push @{$violated{$desc}||=[]}, $.;
-            }
-        }
-    }
-
-    if (%violated) {
-        fail("$filename contains boilerplate text");
-        diag "$_ appears on lines @{$violated{$_}}" for keys %violated;
-    } else {
-        pass("$filename contains no boilerplate text");
-    }
-}
-
-sub module_boilerplate_ok {
-    my ($module) = @_;
-    not_in_file_ok($module =>
-        'the great new $MODULENAME'   => qr/ - The great new /,
-        'boilerplate description'     => qr/Quick summary of what the module/,
-        'stub function definition'    => qr/function[12]/,
-    );
-}
-
-TODO: {
-  local $TODO = "Need to replace the boilerplate text";
-
-  not_in_file_ok(README =>
-    "The README is used..."       => qr/The README is used/,
-    "'version information here'"  => qr/to provide version information/,
-  );
-
-  not_in_file_ok(Changes =>
-    "placeholder date/time"       => qr(Date/time)
-  );
-
-  module_boilerplate_ok('lib/App/wsgetmail.pm');
-
-
-}
-

commit 9363b24c3081c80b6bb0632fab612510668da555
Author: Brett Smith <brett at bestpractical.com>
Date:   Fri Jan 28 15:30:24 2022 -0500

    Generate README.md from bin/wsgetmail
    
    We use the script as the README source, rather than App::wsgetmail,
    because we expect it to be the primary tool most users are interested
    in.
    
    We generate Markdown because it'll look nicer in GitHub; and Pod::Text
    breaks the documentation links in the pod, with no option to prevent
    that.

diff --git a/MANIFEST b/MANIFEST
index 5ff0692..9e47ce4 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -25,7 +25,7 @@ Makefile.PL
 MANIFEST			This list of files
 MANIFEST.SKIP
 META.yml
-README
+README.md
 t/00-load.t
 t/basic.t
 t/mock_responses/messages.json
diff --git a/Makefile.PL b/Makefile.PL
index 69100ac..052b635 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -1,6 +1,7 @@
 use lib qw(.);
 use Config;
 use inc::Module::Install;
+readme_from 'bin/wsgetmail.in', 0, 'md';
 all_from 'lib/App/wsgetmail.pm';
 license 'gpl_2';
 
diff --git a/README b/README
deleted file mode 100644
index e41697d..0000000
--- a/README
+++ /dev/null
@@ -1,55 +0,0 @@
-NAME
-    App::wsgetmail - Fetch mail from the cloud using webservices
-
-DESCRIPTION
-    A simple command line application/script to fetch mail from the cloud
-    using webservices instead of IMAP and POP.
-
-    Configurable to mark fetched mail as read, or to delete it, and with
-    configurable action with the fetched email.
-
-SYNOPSIS
-    wsgetmail365 --configuration path/to/file.json [--debug]
-
-CONFIGURATION
-    Configuration of the wsgetmail tool needs the following fields specific
-    to the ms365 application: Application (client) ID, Directory (tenant) ID
-
-    For access to the email account you need: Account email address Account
-    password Folder (defaults to inbox, currently only one folder is
-    supported)
-
-    Example configuration :
-
-    {
-       "command": "/path/to/rt/bin/rt-mailgate",
-       "command_args": "--url http://rt.example.tld/ --queue general --action correspond",
-       "command_timeout": 15,
-       "recipient":"rt at example.tld",
-       "action_on_fetched":"mark_as_read",
-       "username":"rt at example.tld",
-       "user_password":"password",
-       "tenant_id":"abcd1234-xxxx-xxxx-xxxx-123abcde1234",
-       "client_id":"abcd1234-xxxx-xxxx-xxxx-1234abcdef99",
-       "folder":"Inbox"
-    }
-
-
-    an example configuration file is included in the docs/ directory of this
-    package
-
-SEE ALSO
-    App::wsgetmail::MS365
-    wsgemail365
-
-AUTHOR
-    Best Practical Solutions, LLC <modules at bestpractical.com>
-
-LICENSE AND COPYRIGHT
-    This software is Copyright (c) 2015-2020 by Best Practical Solutions,
-    LLC.
-
-    This is free software, licensed under:
-
-    The GNU General Public License, Version 2, June 1991
-
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..4ff01dc
--- /dev/null
+++ b/README.md
@@ -0,0 +1,249 @@
+# NAME
+
+wsgetmail - get mail from cloud webservices
+
+# SYNOPSIS
+
+Run:
+
+    wsgetmail [options] --config=wsgetmail.json
+
+where `wsgetmail.json` looks like:
+
+    {
+    "client_id": "abcd1234-xxxx-xxxx-xxxx-1234abcdef99",
+    "tenant_id": "abcd1234-xxxx-xxxx-xxxx-123abcde1234",
+    "secret": "abcde1fghij2klmno3pqrst4uvwxy5~0",
+    "global_access": 1,
+    "username": "rt-comment at example.com",
+    "folder": "Inbox",
+    "command": "/opt/rt5/bin/rt-mailgate",
+    "command_args": "--url=http://rt.example.com/ --queue=General --action=comment",
+    "action_on_fetched": "mark_as_read"
+    }
+
+# DESCRIPTION
+
+wsgetmail retrieves mail from a folder available through a web services API
+and delivers it to another system. Currently, it only knows how to retrieve
+mail from the Microsoft Graph API, and deliver it by running another command
+on the local system. It may grow to support other systems in the future.
+
+# INSTALLATION
+
+    perl Makefile.PL
+    make PERL_CANARY_STABILITY_NOPROMPT=1
+    make test
+    sudo make install
+
+`wsgetmail` will be installed under `/usr/local/bin` if you're using the
+system Perl, or in the same directory as `perl` if you built your own.
+
+# ARGUMENTS
+
+- --config, --configuration, -c
+
+    Path of the primary wsgetmail JSON configuration file to read. This argument
+    is required. The configuration file is documented in the next section.
+
+- --options
+
+    A string with a JSON object in the same format as the configuration
+    file. Configuration in this object will override the configuration file. You
+    can use this to extend a base configuration. For example, given the
+    configuration in the synopsis above, you can process a second folder the
+    same way by running:
+
+        wsgetmail --config=wsgetmail.json --options='{"folder": "Other Folder"}'
+
+- --verbose, --debug, -v
+
+    Log additional information about each mail API request and any problems
+    delivering mail.
+
+- --dry-run
+
+    Read mail and deliver it to the configured command, but don't run the
+    configured `action_on_fetched` like deleting messages or marking them as
+    read.
+
+- --help, -h
+
+    Show this help documentation.
+
+# CONFIGURATION
+
+## Configuring Microsoft 365 Client Access
+
+To use wsgetmail, first you need to set up the app in Microsoft 365. This
+section walks you through each piece of configuration wsgetmail needs, and
+how to obtain it.
+
+- tenant\_id
+
+    wsgetmail authenticates to an Azure Active Directory (AD) tenant. This
+    tenant is identified by an identifier that looks like a UUID/GUID: it should
+    be mostly alphanumeric, with dividing dashes in the same places as shown in
+    the example configuration above. Microsoft documents how to find your tenant
+    ID, and create a tenant if needed, in the ["Set up a tenant"
+    quickstart](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-create-new-tenant). Save
+    this as the `tenant_id` string in your wsgetmail configuration file.
+
+- client\_id
+
+    You need to register wsgetmail as an application in your Azure Active
+    Directory tenant. Microsoft documents how to do this in the ["Register an
+    application with the Microsoft identity platform"
+    quickstart](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app#register-an-application),
+    under the section "Register an application." When asked who can use this
+    application, you can leave that at the default "Accounts in this
+    organizational directory only (Single tenant)."
+
+    After you successfully register the wsgetmail application, its information
+    page in your Azure account will display an "Application (client) ID" in the
+    same UUID/GUID format as your tenant ID. Save this as the `client_id`
+    string in your configuration file.
+
+    After that is done, you need to grant wsgetmail permission to access the
+    Microsoft Graph mail APIs. Microsoft documents how to do this in the
+    ["Configure a client application to access a web API"
+    quickstart](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-configure-app-access-web-apis#add-permissions-to-access-microsoft-graph),
+    under the section "Add permissions to access Microsoft Graph." When prompted
+    to select permissions, select all of the following:
+
+    - Mail.Read
+    - Mail.Read.Shared
+    - Mail.ReadWrite
+    - Mail.ReadWrite.Shared
+    - openid
+    - User.Read
+
+### Configuring client secret authentication
+
+We recommend you deploy wsgetmail by configuring it with a client
+secret. Client secrets can be granted limited access to only the mailboxes
+you choose. You can adjust or revoke wsgetmail's access without interfering
+with other applications.
+
+Microsoft documents how to create a client secret in the ["Register an
+application with the Microsoft identity platform"
+quickstart](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app#add-a-client-secret),
+under the section "Add a client secret." Take care to record the secret
+token when it appears; it will never be displayed again. It should look like
+a completely random string, not a UUID/GUID.
+
+- global\_access
+
+    Set this to `1` in your wsgetmail configuration file.
+
+- secret
+
+    Set this to the secret token string you recorded earlier in your wsgetmail
+    configuration file.
+
+- username
+
+    wsgetmail will fetch mail from this user's account. Set this to an email
+    address string in your wsgetmail configuration file.
+
+### Configuring user+password authentication
+
+If you do not want to use a client secret, you can also configure wsgetmail
+to authenticate with a traditional username+password combination. This is
+easier to set up initially, but harder to manage in the long run, because
+the password needs to be kept in sync across applications.
+
+- global\_access
+
+    Set this to `0` in your wsgetmail configuration file.
+
+- username
+
+    wsgetmail will authenticate as this user. Set this to an email address
+    string in your wsgetmail configuration file.
+
+- user\_password
+
+    Set this to the password string for `username` in your wsgetmail
+    configuration file.
+
+## Configuring the mail delivery command
+
+Now that you've configured wsgetmail to access a mail account, all that's
+left is configuring delivery.
+
+- folder
+
+    Set this to the name string of a mail folder to read in your wsgetmail
+    configuration file.
+
+- command
+
+    Set this to executable command string in your wsgetmail configuration
+    file. You can specify an absolute path, or a plain command name which will
+    be found from `$PATH`. For each email wsgetmail retrieves, it will run this
+    command and pass the message data to it via standard input.
+
+- command\_args
+
+    Set this to a string with additional arguments to call `command` with in
+    your wsgetmail configuration file. These arguments follow shell quoting
+    rules: you can escape characters with a backslash, and denote a single
+    string argument with single or double quotes.
+
+- action\_on\_fetched
+
+    Set this to a literal string `"mark_as_read"` or `"delete"` in your
+    wsgetmail configuration file. For each email wsgetmail retrieves, after the
+    configured delivery command succeeds, it will take this action on the message.
+
+    If you set this to `"mark_as_read"`, wsgetmail will only retrieve and
+    deliver messages that are marked unread in the configured folder, so it does
+    not try to deliver the same email multiple times.
+
+# TESTING AND DEPLOYMENT
+
+After you write your wsgetmail configuration file, you can test it by running:
+
+    wsgetmail --debug --dry-run --config=wsgetmail.json
+
+This will read and deliver messages, but will not mark them as read or
+delete them. If there are any problems, those will be reported in the error
+output. You can update your configuration file and try again until wsgetmail
+runs successfully.
+
+Once your configuration is stable, you can configure wsgetmail to run
+periodically through cron or a systemd service on a timer.
+
+# LIMITATIONS
+
+wsgetmail can only read from a single folder each time it runs. If you need
+to read multiple folders (possibly spanning different accounts), then you
+need to run it multiple times with different configuration.
+
+If you only need to change a couple small configuration settings like the
+folder name, you can use the `--options` argument to override those from a
+base configuration file. For example:
+
+    wsgetmail --config=wsgetmail.json --options='{"folder": "Inbox"}'
+    wsgetmail --config=wsgetmail.json --options='{"folder": "Other Folder"}'
+
+NOTE: Setting `secret` or `user_password` with `--options` is not secure
+and may expose your credentials to other users on the local system. If you
+need to set these options, or just change a lot of settings in your
+configuration, just run wsgetmail with different configurations:
+
+    wsgetmail --config=account01.json
+    wsgetmail --config=account02.json
+
+# AUTHOR
+
+Best Practical Solutions, LLC <modules at bestpractical.com>
+
+# LICENSE AND COPYRIGHT
+
+This software is Copyright (c) 2015-2020 by Best Practical Solutions, LLC.
+
+This is free software, licensed under:
+
+The GNU General Public License, Version 2, June 1991

commit fc343fb2bc4bca86e010f80225b1264b532ef6c3
Author: Brett Smith <brett at bestpractical.com>
Date:   Fri Jan 28 15:39:10 2022 -0500

    Support Perl interpreter substitution in wsgetmail
    
    Ensure the tool runs with the same interpreter used for the install.

diff --git a/ignore.txt b/.gitignore
similarity index 93%
rename from ignore.txt
rename to .gitignore
index 91d3464..ce17930 100644
--- a/ignore.txt
+++ b/.gitignore
@@ -16,3 +16,4 @@ pod2htm*.tmp
 pm_to_blib
 App-wsgetmail-*
 App-wsgetmail-*.tar.gz
+bin/wsgetmail
diff --git a/INSTALL.SKIP b/INSTALL.SKIP
new file mode 100644
index 0000000..191f64e
--- /dev/null
+++ b/INSTALL.SKIP
@@ -0,0 +1 @@
+\.in$
diff --git a/MANIFEST b/MANIFEST
index c495736..5ff0692 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -1,4 +1,4 @@
-bin/wsgetmail
+bin/wsgetmail.in
 Changes
 inc/Module/Install.pm
 inc/Module/AutoInstall.pm
@@ -15,6 +15,7 @@ inc/Module/Install/Win32.pm
 inc/Module/Install/WriteAll.pm
 inc/Module/Install/AutoInstall.pm
 inc/YAML/Tiny.pm
+INSTALL.SKIP
 lib/App/wsgetmail.pm
 lib/App/wsgetmail/MDA.pm
 lib/App/wsgetmail/MS365.pm
diff --git a/MANIFEST.SKIP b/MANIFEST.SKIP
index 504fc9d..fd1f2eb 100644
--- a/MANIFEST.SKIP
+++ b/MANIFEST.SKIP
@@ -2,5 +2,6 @@
 .gitignore
 Makefile$
 MYMETA.*
+bin/wsgetmail
 blib/*
 pm_to_blib
diff --git a/Makefile.PL b/Makefile.PL
index ed804e5..69100ac 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -1,4 +1,5 @@
 use lib qw(.);
+use Config;
 use inc::Module::Install;
 all_from 'lib/App/wsgetmail.pm';
 license 'gpl_2';
@@ -22,6 +23,19 @@ requires 'URI::Escape';
 requires 'URI';
 requires 'warnings';
 
+my $secure_perl_path = $Config{perlpath};
+if ($^O ne 'VMS' and $secure_perl_path !~ /$Config{_exe}$/i) {
+    $secure_perl_path .= $Config{_exe};
+}
+
+substitute(
+    {
+        PERL => $ENV{PERL} || $secure_perl_path,
+    },
+    { sufix => '.in' },
+    'bin/wsgetmail',
+);
+
 install_script('bin/wsgetmail');
 auto_install();
 sign;
diff --git a/bin/wsgetmail b/bin/wsgetmail.in
similarity index 100%
rename from bin/wsgetmail
rename to bin/wsgetmail.in

commit accaef5ad9bdaad3d2198c5eb261ae117cdac19b
Author: Brett Smith <brett at bestpractical.com>
Date:   Wed Jan 19 10:34:43 2022 -0500

    Remove doc directory
    
    The tool pod covers this better now, with links to Microsoft's
    documentation as appropriate. Their pages will stay up-to-date better
    than anything we can write. The tool pod documents the configuration
    file in more detail as well.

diff --git a/MANIFEST b/MANIFEST
index 0e3f5a9..c495736 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -1,12 +1,5 @@
 bin/wsgetmail
 Changes
-doc/activedirectory_setup.md
-doc/example.conf
-doc/register_api_app1.png
-doc/register_api_app2.png
-doc/register_api_app3.png
-doc/register_api_app4.png
-doc/register_api_app5.png
 inc/Module/Install.pm
 inc/Module/AutoInstall.pm
 inc/Module/Install/Base.pm
diff --git a/doc/activedirectory_setup.md b/doc/activedirectory_setup.md
deleted file mode 100644
index 4c4f187..0000000
--- a/doc/activedirectory_setup.md
+++ /dev/null
@@ -1,62 +0,0 @@
-# Setting up mail API integration in microsoft365
-
-## Active Directory application configuration
-
-From Azure Active directory admin center..
-1. Go to App Registrations and then "New registration", select single tenant and register.
-![screenshot](register_api_app1.png)
-2. Go to certificates and secrets, add a new client secret. 
-![screenshot](register_api_app3.png)
-3. Go to API permissions and add the following delegated rights :
-  * Microsoft Graph :
-        * Mail.Read Delegated right
-        * Mail.Read.Shared Delegated right
-        * Mail.ReadWrite Delegated right
-        * Mail.ReadWrite.Shared Delegated right
-        * openid  Delegated right
-        * User.Read  Delegated right
-![screenshot](register_api_app4.png)
-4. Once the rights have been added, grant admin consent to allow the API client to use them.
-5. Then go to authentication, and change "Treat application as a public client." to "yes".
-![screenshot](register_api_app5.png)
-
-## Further microsoft 365 documentation
-
-* https://docs.microsoft.com/en-gb/azure/active-directory/develop/quickstart-register-app
-
-
-## wsgetmail tool configuration
-
-Configuration of the wsgetmail tool needs the following fields specific to the ms365 application:
-* Application (client) ID - client_id in the configuration file, a UUID string, i.e. abcd1234-xxxx-xxxx-xxxx-123abcde1234
-* Directory (tenant) ID - tenant_ud in the configuration file, a UUID string, i.e.  abcd1234-xxxx-xxxx-xxxx-1234abcdef99
-
-You can get these details from the overview of the registered application
-![screenshot](register_api_app2.png)
-
-For access to the email account you need:
-* Account email address - username in configuration file
-* Account password - user_password in configuration file
-* Folder (defaults to Inbox, currently only one folder is supported) - folder in configuration file
-
-## Example configuration
-
-``` Javascript
-{
-  "handler_options":{
-     "url":"http://rt.example.tld/",
-     "debug":"1",
-     "class":"Mailgate",
-     "command_path":"/path/to/rt/bin",
-     "recipient":"rt at example.tld",
-     "action_on_fetched":"mark_as_read"
-   },
-   "username":"rt at example.tld",
-   "user_password":"password",
-   "tenant_id":"abcd1234-xxxx-xxxx-xxxx-123abcde1234",
-   "client_id":"abcd1234-xxxx-xxxx-xxxx-1234abcdef99",
-   "action_on_fetched":"mark_as_read",
-   "folder":"Inbox"
-}
-```
-
diff --git a/doc/example.conf b/doc/example.conf
deleted file mode 100644
index f15d022..0000000
--- a/doc/example.conf
+++ /dev/null
@@ -1,12 +0,0 @@
-{
-   "command": "/path/to/rt/bin/rt-mailgate",
-   "command_args": "--url http://rt.example.tld/ --queue general --action correspond",
-   "command_timeout": 15,
-   "recipient":"rt at example.tld",
-   "action_on_fetched":"mark_as_read",
-   "username":"rt at example.tld",
-   "user_password":"password",
-   "tenant_id":"abcd1234-xxxx-xxxx-xxxx-123abcde1234",
-   "client_id":"abcd1234-xxxx-xxxx-xxxx-1234abcdef99",
-   "folder":"Inbox"
-}
diff --git a/doc/register_api_app1.png b/doc/register_api_app1.png
deleted file mode 100644
index ba16ae6..0000000
Binary files a/doc/register_api_app1.png and /dev/null differ
diff --git a/doc/register_api_app2.png b/doc/register_api_app2.png
deleted file mode 100644
index affed24..0000000
Binary files a/doc/register_api_app2.png and /dev/null differ
diff --git a/doc/register_api_app3.png b/doc/register_api_app3.png
deleted file mode 100644
index 94bca17..0000000
Binary files a/doc/register_api_app3.png and /dev/null differ
diff --git a/doc/register_api_app4.png b/doc/register_api_app4.png
deleted file mode 100644
index a2f5f73..0000000
Binary files a/doc/register_api_app4.png and /dev/null differ
diff --git a/doc/register_api_app5.png b/doc/register_api_app5.png
deleted file mode 100644
index 82ed701..0000000
Binary files a/doc/register_api_app5.png and /dev/null differ

commit 3c14164727f5c72482f3327e37eb1c131f7dcea0
Author: Brett Smith <brett at bestpractical.com>
Date:   Fri Jan 28 15:24:32 2022 -0500

    Add pod to App::wsgetmail
    
    Because we expect most users to be interested in running the tool, and
    we've written complete setup documentation there, we refer them to it
    very early. After that, add library reference material to match the rest
    of the pod.

diff --git a/lib/App/wsgetmail.pm b/lib/App/wsgetmail.pm
index cea9ca0..1e4023b 100644
--- a/lib/App/wsgetmail.pm
+++ b/lib/App/wsgetmail.pm
@@ -12,50 +12,52 @@ App::wsgetmail - Fetch mail from the cloud using webservices
 
 0.03
 
-=head1 DESCRIPTION
+=head1 SYNOPSIS
 
-A simple command line application/script to fetch mail from the cloud
-using webservices instead of IMAP and POP.
+If you just want to run wsgetmail on the command line, the L<wsgetmail>
+documentation page provides full documentation for how to configure and run
+it, including how to configure the app in your cloud environment. Run:
 
-Configurable to mark fetched mail as read, or to delete it, and with
-configurable action with the fetched email.
+    wsgetmail [options] --config=wsgetmail.json
 
-=head1 SYNOPSIS
+where C<wsgetmail.json> looks like:
 
-wsgetmail365 --configuration path/to/file.json [--debug] [ --dry-run]
-
-=head1 CONFIGURATION
-
-Configuration of the wsgetmail tool needs the following fields specific to the ms365 application:
-Application (client) ID,
-Directory (tenant) ID
-
-For access to the email account you need:
-Account email address
-Account password
-Folder (defaults to inbox, currently only one folder is supported)
-
-For forwarding to RT via rt-mailgate you need :
-RT URL
-Path to rt-mailgate
-Recipient address (usually same as account email address, could be a shared mailbox or alias)
-action on fetching mail : either "mark_as_read" or "delete"
-
-example configuration :
-{
-   "command": "/path/to/rt/bin/rt-mailgate",
-   "command_args": "--url http://rt.example.tld/ --queue general --action correspond",
-   "command_timeout": 15,
-   "recipient":"rt at example.tld",
-   "action_on_fetched":"mark_as_read",
-   "username":"rt at example.tld",
-   "user_password":"password",
-   "tenant_id":"abcd1234-xxxx-xxxx-xxxx-123abcde1234",
-   "client_id":"abcd1234-xxxx-xxxx-xxxx-1234abcdef99",
-   "folder":"Inbox"
-}
+    {
+    "client_id": "abcd1234-xxxx-xxxx-xxxx-1234abcdef99",
+    "tenant_id": "abcd1234-xxxx-xxxx-xxxx-123abcde1234",
+    "secret": "abcde1fghij2klmno3pqrst4uvwxy5~0",
+    "global_access": 1,
+    "username": "rt-comment at example.com",
+    "folder": "Inbox",
+    "command": "/opt/rt5/bin/rt-mailgate",
+    "command_args": "--url=http://rt.example.com/ --queue=General --action=comment",
+    "action_on_fetched": "mark_as_read"
+    }
+
+Using App::wsgetmail as a library looks like:
+
+    my $getmail = App::wsgetmail->new({config => {
+      # The config hashref takes all the same keys and values as the
+      # command line tool configuration JSON.
+    }});
+    while (my $message = $getmail->get_next_message()) {
+        $getmail->process_message($message)
+          or warn "could not process $message->id";
+    }
+
+=head1 DESCRIPTION
+
+wsgetmail retrieves mail from a folder available through a web services API
+and delivers it to another system. Currently, it only knows how to retrieve
+mail from the Microsoft Graph API, and deliver it by running another command
+on the local system. It may grow to support other systems in the future.
 
-an example configuration file is included in the docs/ directory of this package
+=head1 INSTALLATION
+
+    perl Makefile.PL
+    make PERL_CANARY_STABILITY_NOPROMPT=1
+    make test
+    sudo make install
 
 =cut
 
@@ -63,11 +65,25 @@ use Clone 'clone';
 use Module::Load;
 use App::wsgetmail::MDA;
 
+=head1 ATTRIBUTES
+
+=head2 config
+
+A hash ref that is passed to construct the C<mda> and C<client> (see below).
+
+=cut
+
 has config => (
     is => 'ro',
     required => 1
 );
 
+=head2 mda
+
+An instance of L<App::wsgetmail::MDA> created from our C<config> object.
+
+=cut
+
 has mda => (
     is => 'rw',
     lazy => 1,
@@ -75,12 +91,24 @@ has mda => (
     builder => '_build_mda'
 );
 
+=head2 client_class
+
+The name of the App::wsgetmail package used to construct the
+C<client>. Default C<MS365>.
+
+=cut
 
 has client_class => (
     is => 'ro',
     default => sub { 'MS365' }
 );
 
+=head2 client
+
+An instance of the C<client_class> created from our C<config> object.
+
+=cut
+
 has client => (
     is => 'ro',
     lazy => 1,
@@ -115,6 +143,15 @@ sub _build__post_fetch_action {
     return $fetched_action_method;
 }
 
+=head1 METHODS
+
+=head2 process_message($message)
+
+Given a Message object, retrieves the full message content, delivers it
+using the C<mda>, and then executes the configured post-fetch
+action. Returns a boolean indicating success.
+
+=cut
 
 sub process_message {
     my ($self, $message) = @_;
@@ -137,6 +174,13 @@ sub process_message {
     return $ok;
 }
 
+=head2 post_fetch_action($message)
+
+Given a Message object, executes the configured post-fetch action. Returns a
+boolean indicating success.
+
+=cut
+
 sub post_fetch_action {
     my ($self, $message) = @_;
     my $method = $self->_post_fetch_action;
@@ -173,20 +217,17 @@ sub _build_mda {
     return App::wsgetmail::MDA->new($config);
 }
 
-
-
-##
-
-
 =head1 SEE ALSO
 
 =over 4
 
-=item App::wsgetmail::MDA
+=item * L<wsgetmail>
+
+=item * L<App::wsgetmail::MDA>
 
-=item App::wsgetmail::MS365
+=item * L<App::wsgetmail::MS365>
 
-=item wsgemail365
+=item * L<App::wsgetmail::MS365::Message>
 
 =back
 

commit bae3464da04f589a52221bb10aa4dfab3f15431f
Author: Brett Smith <brett at bestpractical.com>
Date:   Tue Jan 18 15:26:47 2022 -0500

    Clean up pod in App::wsgetmail::MS365::Message
    
    Make formatting and legal information more consistent with other
    modules.

diff --git a/lib/App/wsgetmail/MS365/Message.pm b/lib/App/wsgetmail/MS365/Message.pm
index 331632d..5741cf6 100644
--- a/lib/App/wsgetmail/MS365/Message.pm
+++ b/lib/App/wsgetmail/MS365/Message.pm
@@ -7,17 +7,19 @@ App::wsgetmail::MS365::Message
 
 =head2 DESCRIPTION
 
-Simple Moo class representing an microsoft/outlook 365 message.
+Simple Moo class representing an Microsoft/Outlook 365 message.
 
-=head2 ACCESSORS
+=head2 ATTRIBUTES
+
+All attributes are read-only.
 
 =over 4
 
-=item id
+=item * id
 
-=item status
+=item * status
 
-=item recipients
+=item * recipients
 
 =back
 
@@ -61,13 +63,13 @@ around BUILDARGS => sub {
 
 =over 4
 
-=item App::wsgetmail::MS365
+=item * L<App::wsgetmail::MS365>
 
 =back
 
 =head1 AUTHOR
 
-Aaron Trevena, C<< <ast at bestpractical.com> >>
+Best Practical Solutions, LLC <modules at bestpractical.com>
 
 =head1 LICENSE AND COPYRIGHT
 
@@ -75,9 +77,8 @@ This software is Copyright (c) 2020 by Best Practical Solutions, LLC
 
 This is free software, licensed under:
 
-  The Artistic License 2.0 (GPL Compatible)
+  The GNU General Public License, Version 2, June 1991
 
 =cut
 
-
 1;

commit 0e501ede3ccbdb2e4ddcb0a0888cc1a5de649999
Author: Brett Smith <brett at bestpractical.com>
Date:   Tue Jan 18 15:12:22 2022 -0500

    Add pod to App::wsgetmail::MS365::Client
    
    Describe the class' purpose, attributes, and methods in more detail.

diff --git a/lib/App/wsgetmail/MS365/Client.pm b/lib/App/wsgetmail/MS365/Client.pm
index fd826e9..0abba71 100644
--- a/lib/App/wsgetmail/MS365/Client.pm
+++ b/lib/App/wsgetmail/MS365/Client.pm
@@ -2,7 +2,7 @@ package App::wsgetmail::MS365::Client;
 
 =head1 NAME
 
-App::wsgetmail::MS365 - Fetch mail from Microsoft 365
+App::wsgetmail::MS365::Client - Low-level client to the Microsoft Graph API
 
 =cut
 
@@ -15,25 +15,17 @@ use Azure::AD::ClientCredentials;
 
 =head1 DESCRIPTION
 
-Fetch mail from Microsoft 365 mailboxes using the Graph REST API
+This class performs the actual REST requests to support
+L<App::wsgetmail::MS365>.
 
 =head1 ATTRIBUTES
 
-=over 4
-
-=item secret
-
-=item client_id
-
-=item tenant_id
-
-=item username
-
-=item user_password
+The following attributes are received from L<App::wsgetmail::MS365> and have
+the same meaning:
 
-=item global_access
+=over 4
 
-=back
+=item * secret
 
 =cut
 
@@ -42,36 +34,72 @@ has secret  => (
     required => 0,
 );
 
+=item * client_id
+
+=cut
+
 has client_id => (
     is => 'ro',
     required => 1,
 );
 
+=item * tenant_id
+
+=cut
+
 has tenant_id => (
     is => 'ro',
     required => 1,
 );
 
+=item * username
+
+=cut
+
 has username => (
     is => 'ro',
     required => 0
 );
 
+=item * user_password
+
+=cut
+
 has user_password => (
     is => 'ro',
     required => 0
 );
 
+=item * global_access
+
+=item * debug
+
+=cut
+
 has global_access => (
     is => 'ro',
     default => sub { return 0 }
 );
 
+=back
+
+=head2 resource_url
+
+A string with the URL for the overall API endpoint.
+
+=cut
+
 has resource_url => (
     is => 'ro',
     default => sub { return 'https://graph.microsoft.com/' }
 );
 
+=head2 resource_path
+
+A string with the REST API endpoint URL path.
+
+=cut
+
 has resource_path => (
     is => 'ro',
     default => sub { return 'v1.0' }
@@ -118,7 +146,10 @@ sub BUILD {
 
 =head1 METHODS
 
-=head2 build_rest_uir
+=head2 build_rest_uri(@endpoint_parts)
+
+Given a list of URL component strings, returns a complete URL string to
+reach that endpoint from this object's C<resource_url> and C<resource_path>.
 
 =cut
 
@@ -128,7 +159,11 @@ sub build_rest_uri {
     return join('/', $base_url, @endpoint_parts);
 }
 
-=head2 get_request
+=head2 get_request($parts, $params)
+
+Makes a GET request to the API. C<$parts> is an arrayref of URL endpoint
+strings with the specific endpoint to request. C<$params> is a hashref of
+query parameters to send with the request.
 
 =cut
 
@@ -141,7 +176,9 @@ sub get_request {
     return $self->_ua->get($uri);
 }
 
-=head2 get_request_by_url
+=head2 get_request_by_url($url)
+
+Makes a GET request to the URL in the C<$url> string.
 
 =cut
 
@@ -151,7 +188,10 @@ sub get_request_by_url {
     return $self->_ua->get($url);
 }
 
-=head2 delete_request
+=head2 delete_request($parts, $params)
+
+Makes a DELETE request to the API. C<$parts> is an arrayref of URL endpoint
+strings with the specific endpoint to request. C<$params> is unused.
 
 =cut
 
@@ -162,7 +202,11 @@ sub delete_request {
     return $self->_ua->delete($url);
 }
 
-=head2 post_request
+=head2 post_request($path_parts, $post_data)
+
+Makes a POST request to the API. C<$path_parts> is an arrayref of URL
+endpoint strings with the specific endpoint to request. C<$post_data> is a
+reference to an array or hash of data to include in the POST request body.
 
 =cut
 
@@ -173,7 +217,11 @@ sub post_request {
     return $self->_ua->post($url,$post_data);
 }
 
-=head2 patch_request
+=head2 patch_request($path_parts, $patch_params)
+
+Makes a PATCH request to the API. C<$path_parts> is an arrayref of URL
+endpoint strings with the specific endpoint to request. C<$patch_params> is
+a hashref of data to include in the PATCH request body.
 
 =cut
 
@@ -255,20 +303,22 @@ sub _new_useragent {
 
 =over 4
 
-=item App::wsgetmail::MS365
+=item * L<App::wsgetmail::MS365>
 
 =back
 
+=head1 AUTHOR
+
+Best Practical Solutions, LLC <modules at bestpractical.com>
+
 =head1 LICENSE AND COPYRIGHT
 
 This software is Copyright (c) 2020 by Best Practical Solutions, LLC
 
 This is free software, licensed under:
 
-  The Artistic License 2.0 (GPL Compatible)
-
+  The GNU General Public License, Version 2, June 1991
 
 =cut
 
-
 1;

commit 89ed5bdb7ebd04ae08d2f285309f80d51912f5cb
Author: Brett Smith <brett at bestpractical.com>
Date:   Tue Jan 18 14:53:47 2022 -0500

    Add pod to App::wsgetmail::MS365
    
    Add detailed explanations of each attribute.
    
    Remove the documentation about configuring Microsoft 365 now that that's
    consolidated in the tool pod.

diff --git a/lib/App/wsgetmail/MS365.pm b/lib/App/wsgetmail/MS365.pm
index a781a3d..17d7c43 100644
--- a/lib/App/wsgetmail/MS365.pm
+++ b/lib/App/wsgetmail/MS365.pm
@@ -13,6 +13,18 @@ use App::wsgetmail::MS365::Client;
 use App::wsgetmail::MS365::Message;
 use File::Temp;
 
+=head1 SYNOPSIS
+
+    my $ms365 = App::wsgetmail::MS365->new({
+      client_id => "client UUID",
+      tenant_id => "tenant UUID",
+      secret => "random secret token",
+      global_access => 1,
+      folder => "Inbox",
+      post_fetch_action => "mark_message_as_read",
+      debug => 0,
+    })
+
 =head1 DESCRIPTION
 
 Moo class providing methods to connect to and fetch mail from Microsoft 365
@@ -20,23 +32,14 @@ Moo class providing methods to connect to and fetch mail from Microsoft 365
 
 =head1 ATTRIBUTES
 
-=over 4
-
-=item client_id
-
-=item tenant_id
-
-=item username
-
-=item user_password
+You must provide C<client_id>, C<tenant_id>, C<post_fetch_action>, and
+authentication details. If C<global_access> is false (the default), you must
+provide C<username> and C<user_password>. If you set C<global_access> to a
+true value, you must provide C<secret>.
 
-=item global_access
+=head2 client_id
 
-=item secret
-
-=item folder
-
-=back
+A string with the UUID of the client application to use for authentication.
 
 =cut
 
@@ -45,42 +48,101 @@ has client_id => (
     required => 1,
 );
 
+=head2 tenant_id
+
+A string with the UUID of your Microsoft 365 tenant to use for authentication.
+
+=cut
+
 has tenant_id => (
     is => 'ro',
     required => 1,
 );
 
+=head2 username
+
+A string with a username email address. If C<global_access> is false (the
+default), the client authenticates with this username. If C<global_access>
+is true, the client accesses this user's mailboxes.
+
+=cut
+
 has username => (
     is => 'ro',
     required => 0
 );
 
+=head2 user_password
+
+A string with the user password to use for authentication without global
+access.
+
+=cut
+
 has user_password => (
     is => 'ro',
     required => 0
 );
 
+=head2 folder
+
+A string with the name of the email folder to read. Default "Inbox".
+
+=cut
+
 has folder => (
     is => 'ro',
     required => 0,
     default => sub { 'Inbox' }
 );
 
+=head2 global_access
+
+A boolean. If false (the default), the client will authenticate using
+C<username> and C<user_password>. If true, the client will authenticate
+using its C<secret> token.
+
+=cut
+
 has global_access => (
     is => 'ro',
     default => sub { return 0 }
 );
 
+=head2 secret
+
+A string with the client secret to use for global authentication. This
+should look like a long string of completely random characters, not a UUID
+or other recognizable format.
+
+=cut
+
 has secret => (
     is => 'ro',
     required => 0,
 );
 
+=head2 post_fetch_action
+
+A string with the name of a method to call after reading a message. You
+probably want to pass either "mark_message_as_read" or "delete_message". In
+principle, you can pass the name of any method that accepts a message ID
+string argument.
+
+=cut
+
 has post_fetch_action => (
     is => 'ro',
     required => 1
 );
 
+=head2 debug
+
+A boolean. If true, the object will issue a warning with details about each
+request it issues.
+
+=cut
+
 has debug => (
     is => 'rw',
     default => sub { return 0 }
@@ -332,76 +394,18 @@ sub _build_client {
 
 }
 
-=head1 CONFIGURATION
-
-=head2 Setting up mail API integration in microsoft365
-
-Active Directory application configuration
-
-From Azure Active directory admin center.
-
-=over 4
-
-=item 1.
-
-Go to App Registrations and then "New registration", select single tenant and register.
-
-=item 2.
-
-Go to certificates and secrets, add a new client secret.
-
-=item 3.
-
-Go to API permissions and add the following delegated rights for Microsoft Graph:
-
-=over 6
-
-=item * Mail.Read Delegated right
-
-=item * Mail.Read.Shared Delegated right
+=head1 AUTHOR
 
-=item * Mail.ReadWrite Delegated right
-
-=item * Mail.ReadWrite.Shared Delegated right
-
-=item * openid  Delegated right
-
-=item * User.Read  Delegated right
-
-=back
-
-=item 4.
-
-Once the rights have been added, grant admin consent to allow the API client to use them.
-
-=item 5.
-
-Then go to authentication, and change "Treat application as a public client." to "yes".
-
-=back
-
-=head1 SEE ALSO
-
-=over 4
-
-=item App::wsgetmail::MS365::Client
-
-=item App::wsgetmail::MS365::Message
-
-=item L<https://docs.microsoft.com/en-gb/azure/active-directory/develop/quickstart-register-app>
-
-=back
+Best Practical Solutions, LLC <modules at bestpractical.com>
 
 =head1 LICENSE AND COPYRIGHT
 
-This software is Copyright (c) 2020 by Best Practical Solutions, LLC
+This software is Copyright (c) 2020 by Best Practical Solutions, LLC.
 
 This is free software, licensed under:
 
-  The Artistic License 2.0 (GPL Compatible)
-
+The GNU General Public License, Version 2, June 1991
 
 =cut
 
-
 1;

commit 05f9253ee2d7747e5cbb6f340e7fd091c30f4b9e
Author: Brett Smith <brett at bestpractical.com>
Date:   Fri Jan 14 16:59:18 2022 -0500

    Add pod to App::wsgetmail::MDA

diff --git a/lib/App/wsgetmail/MDA.pm b/lib/App/wsgetmail/MDA.pm
index 4031e94..32620a5 100644
--- a/lib/App/wsgetmail/MDA.pm
+++ b/lib/App/wsgetmail/MDA.pm
@@ -1,25 +1,73 @@
+=head1 NAME
+
+App::wsgetmail::MDA - Deliver mail to another command's standard input
+
+=head1 SYNOPSIS
+
+    my $mda = App::wsgetmail::MDA->new({
+      command => "/opt/rt5/bin/rt-mailgate",
+      command_args => "--url https://rt.example.com --queue General --action correspond",
+      command_timeout => 15,
+      debug => 0,
+    })
+    $mda->forward($message, $message_path);
+
+=head1 DESCRIPTION
+
+App::wsgetmail::MDA takes mail fetched from web services and routes it to
+another command via standard input.
+
+=cut
+
 package App::wsgetmail::MDA;
 use Moo;
 
 use IPC::Run qw( run timeout );
 
+=head1 ATTRIBUTES
+
+You can initialize a new App::wsgetmail::MDA object with the attributes
+below. C<command> and C<command_args> are required; the rest are
+optional. All attributes are read-only.
+
+=head2 command
+
+A string with the executable to run. You can specify an absolute path, or a
+plain command name which will be found from C<$PATH>.
+
+=cut
+
 has command => (
     is => 'ro',
     required => 1,
 );
+
+=head2 command_args
+
+A string with additional arguments to call C<command> with. These arguments
+follow shell quoting rules: you can escape characters with a backslash, and
+denote a single string argument with single or double quotes.
+
+=cut
+
 has command_args => (
     is => 'ro',
     required => 1,
 );
 
+=head2 command_timeout
+
+A number. The run command will be terminated if it takes longer than this many
+seconds.
+
+=cut
+
 has command_timeout => (
     is => 'ro',
     default => sub { 30; }
 );
 
-# extension and recipient were used in previous versions, but the code was
-# buggy and has since been removed. They're only here for backwards API
-# compatibility.
+# extension and recipient are currently unused. See pod below.
 has extension => (
     is => 'ro',
     required => 0
@@ -30,6 +78,26 @@ has recipient => (
     required => 0,
 );
 
+=head2 debug
+
+A boolean. If true, the object will issue additional diagnostic warnings if it
+encounters any trouble.
+
+=head2 Unused Attributes
+
+These attributes were used in previous versions of the module. They are
+currently unimplemented and always return undef. You cannot initialize them.
+
+=over 4
+
+=item * extension
+
+=item * recipient
+
+=back
+
+=cut
+
 has debug => (
     is => 'ro',
     default => sub { 0 }
@@ -45,7 +113,15 @@ around BUILDARGS => sub {
 };
 
 
-###
+=head1 METHODS
+
+=head2 forward($message, $filename)
+
+Invokes the configured command to deliver the given message. C<$message> is
+an object like L<App::wsgetmail::MS365::Message>. C<$filename> is the path
+to a file with the raw message content.
+
+=cut
 
 sub forward {
     my ($self, $message, $filename) = @_;
@@ -131,4 +207,18 @@ sub _split_command_args {
     return @args;
 }
 
+=head1 AUTHOR
+
+Best Practical Solutions, LLC <modules at bestpractical.com>
+
+=head1 LICENSE AND COPYRIGHT
+
+This software is Copyright (c) 2015-2020 by Best Practical Solutions, LLC.
+
+This is free software, licensed under:
+
+The GNU General Public License, Version 2, June 1991
+
+=cut
+
 1;

commit dd38525989fb8fa880a7301f87af7445657b16dc
Author: Brett Smith <brett at bestpractical.com>
Date:   Fri Jan 14 16:16:53 2022 -0500

    Remove extension support from MDA
    
    I believe this was intended to act like other MDAs which set an
    EXTENSION environment variable from the address for
    convenience. However, it was never documented, and the implementation
    has bugs that mean it never worked.
    
    Note that in `last if ($recipient = $self->recipient)`, the condition
    uses the assignment operator. This means if $self->recipient was any
    truthy value, the loop would immediately abort. Otherwise, $recipient
    would get to a falsy value (probably either the empty string or undef),
    and there's no way we could get past the later check
    `next unless (defined $extension && ( $extension =~ /\S/ ))`.
    
    I can guess at a few ways this *should* work, but it's not clear which
    one was intended. Just remove it for now until we have a real use case.

diff --git a/lib/App/wsgetmail/MDA.pm b/lib/App/wsgetmail/MDA.pm
index a76c423..4031e94 100644
--- a/lib/App/wsgetmail/MDA.pm
+++ b/lib/App/wsgetmail/MDA.pm
@@ -17,6 +17,9 @@ has command_timeout => (
     default => sub { 30; }
 );
 
+# extension and recipient were used in previous versions, but the code was
+# buggy and has since been removed. They're only here for backwards API
+# compatibility.
 has extension => (
     is => 'ro',
     required => 0
@@ -34,7 +37,7 @@ has debug => (
 
 
 
-my @config_fields = qw( command command_args command_timeout extension recipient debug);
+my @config_fields = qw( command command_args command_timeout debug );
 around BUILDARGS => sub {
     my ( $orig, $class, $config ) = @_;
     my $attributes = { map { $_ => $config->{$_} } @config_fields };
@@ -46,22 +49,6 @@ around BUILDARGS => sub {
 
 sub forward {
     my ($self, $message, $filename) = @_;
-    # build arguments
-    if ($self->extension) {
-        # get relevent recipients for extension check
-        foreach my $recipient (@{$message->recipients}) {
-            last if ($recipient = $self->recipient);
-            my ($username, $domain) = split(/\@/, $recipient);
-            my $extension;
-            ($username, $extension) = split(/\+/, $username);
-            next unless (defined $extension && ( $extension =~ /\S/ ));
-            if (sprintf('%s@%s',$username,$domain) eq $self->recipient) {
-                $ENV{EXTENSION} = $extension;
-            }
-        }
-    }
-
-    # run command
     return $self->_run_command($filename);
 }
 

commit cf9f2a546ff07b5298766161e4b5029f1845a8bb
Author: Brett Smith <brett at bestpractical.com>
Date:   Tue Jan 18 17:23:16 2022 -0500

    Expand pod for bin/wsgetmail
    
    Expand documentation of arguments and the configuration file format. Add
    documentation with Microsoft links about how to set up the client
    application.
    
    We expect most wsgetmail users want to run the command line tool. With
    that, we are working to make the tool pod complete documentation for
    them to get up and running. This is nicer for those users, and frees up
    space for the library pod to become more of a developer-oriented
    reference.

diff --git a/bin/wsgetmail b/bin/wsgetmail
index 3e2f8b5..b7ea893 100755
--- a/bin/wsgetmail
+++ b/bin/wsgetmail
@@ -54,54 +54,279 @@ __END__
 
 wsgetmail - get mail from cloud webservices
 
+=head1 SYNOPSIS
+
+Run:
+
+    wsgetmail [options] --config=wsgetmail.json
+
+where C<wsgetmail.json> looks like:
+
+    {
+    "client_id": "abcd1234-xxxx-xxxx-xxxx-1234abcdef99",
+    "tenant_id": "abcd1234-xxxx-xxxx-xxxx-123abcde1234",
+    "secret": "abcde1fghij2klmno3pqrst4uvwxy5~0",
+    "global_access": 1,
+    "username": "rt-comment at example.com",
+    "folder": "Inbox",
+    "command": "/opt/rt5/bin/rt-mailgate",
+    "command_args": "--url=http://rt.example.com/ --queue=General --action=comment",
+    "action_on_fetched": "mark_as_read"
+    }
+
 =head1 DESCRIPTION
 
-get mail from cloud webservices
+wsgetmail retrieves mail from a folder available through a web services API
+and delivers it to another system. Currently, it only knows how to retrieve
+mail from the Microsoft Graph API, and deliver it by running another command
+on the local system. It may grow to support other systems in the future.
 
-=head1 SYNOPSIS
+=head1 INSTALLATION
+
+    perl Makefile.PL
+    make PERL_CANARY_STABILITY_NOPROMPT=1
+    make test
+    sudo make install
 
-wsgetmail --config[uration] path/to/file.json [--options "{..}"] [--debug]
+C<wsgetmail> will be installed under C</usr/local/bin> if you're using the
+system Perl, or in the same directory as C<perl> if you built your own.
 
 =head1 ARGUMENTS
 
 =over 4
 
-=item config - configuration file to be used
+=item --config, --configuration, -c
+
+Path of the primary wsgetmail JSON configuration file to read. This argument
+is required. The configuration file is documented in the next section.
+
+=item --options
+
+A string with a JSON object in the same format as the configuration
+file. Configuration in this object will override the configuration file. You
+can use this to extend a base configuration. For example, given the
+configuration in the synopsis above, you can process a second folder the
+same way by running:
 
-=item options - json string of options over-riding or adding to configuration from filename (optional)
+    wsgetmail --config=wsgetmail.json --options='{"folder": "Other Folder"}'
 
-=item debug - flag indicating that debug warnings should be output (optional)
+=item --verbose, --debug, -v
 
-=item dry-run - fetch mail and deliver it but don't delete or mark as read (optional)
+Log additional information about each mail API request and any problems
+delivering mail.
+
+=item --dry-run
+
+Read mail and deliver it to the configured command, but don't run the
+configured C<action_on_fetched> like deleting messages or marking them as
+read.
+
+=item --help, -h
+
+Show this help documentation.
 
 =back
 
 =head1 CONFIGURATION
 
-Configuration of the wsgetmail tool needs the following fields specific to the ms365 application:
-Application (client) ID,
-Directory (tenant) ID
-
-For access to the email account you need:
-Account email address
-Account password
-Folder (defaults to inbox, currently only one folder is supported)
-
-example configuration :
-
-{
-   "command": "/path/to/rt/bin/rt-mailgate",
-   "command_args": "--url http://rt.example.tld/ --queue general --action correspond",
-   "command_timeout": 15,
-   "recipient":"rt at example.tld",
-   "action_on_fetched":"mark_as_read",
-   "username":"rt at example.tld",
-   "user_password":"password",
-   "tenant_id":"abcd1234-xxxx-xxxx-xxxx-123abcde1234",
-   "client_id":"abcd1234-xxxx-xxxx-xxxx-1234abcdef99",
-   "folder":"Inbox"
-}
+=head2 Configuring Microsoft 365 Client Access
+
+To use wsgetmail, first you need to set up the app in Microsoft 365. This
+section walks you through each piece of configuration wsgetmail needs, and
+how to obtain it.
+
+=over 4
+
+=item tenant_id
+
+wsgetmail authenticates to an Azure Active Directory (AD) tenant. This
+tenant is identified by an identifier that looks like a UUID/GUID: it should
+be mostly alphanumeric, with dividing dashes in the same places as shown in
+the example configuration above. Microsoft documents how to find your tenant
+ID, and create a tenant if needed, in the L<"Set up a tenant"
+quickstart|https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-create-new-tenant>. Save
+this as the C<tenant_id> string in your wsgetmail configuration file.
+
+=item client_id
+
+You need to register wsgetmail as an application in your Azure Active
+Directory tenant. Microsoft documents how to do this in the L<"Register an
+application with the Microsoft identity platform"
+quickstart|https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app#register-an-application>,
+under the section "Register an application." When asked who can use this
+application, you can leave that at the default "Accounts in this
+organizational directory only (Single tenant)."
+
+After you successfully register the wsgetmail application, its information
+page in your Azure account will display an "Application (client) ID" in the
+same UUID/GUID format as your tenant ID. Save this as the C<client_id>
+string in your configuration file.
+
+After that is done, you need to grant wsgetmail permission to access the
+Microsoft Graph mail APIs. Microsoft documents how to do this in the
+L<"Configure a client application to access a web API"
+quickstart|https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-configure-app-access-web-apis#add-permissions-to-access-microsoft-graph>,
+under the section "Add permissions to access Microsoft Graph." When prompted
+to select permissions, select all of the following:
+
+=over 4
+
+=item * Mail.Read
+
+=item * Mail.Read.Shared
+
+=item * Mail.ReadWrite
+
+=item * Mail.ReadWrite.Shared
+
+=item * openid
+
+=item * User.Read
+
+=back
+
+=back
+
+=head3 Configuring client secret authentication
+
+We recommend you deploy wsgetmail by configuring it with a client
+secret. Client secrets can be granted limited access to only the mailboxes
+you choose. You can adjust or revoke wsgetmail's access without interfering
+with other applications.
+
+Microsoft documents how to create a client secret in the L<"Register an
+application with the Microsoft identity platform"
+quickstart|https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app#add-a-client-secret>,
+under the section "Add a client secret." Take care to record the secret
+token when it appears; it will never be displayed again. It should look like
+a completely random string, not a UUID/GUID.
+
+=over 4
+
+=item global_access
+
+Set this to C<1> in your wsgetmail configuration file.
+
+=item secret
+
+Set this to the secret token string you recorded earlier in your wsgetmail
+configuration file.
+
+=item username
+
+wsgetmail will fetch mail from this user's account. Set this to an email
+address string in your wsgetmail configuration file.
+
+=back
+
+=head3 Configuring user+password authentication
+
+If you do not want to use a client secret, you can also configure wsgetmail
+to authenticate with a traditional username+password combination. This is
+easier to set up initially, but harder to manage in the long run, because
+the password needs to be kept in sync across applications.
+
+=over 4
+
+=item global_access
+
+Set this to C<0> in your wsgetmail configuration file.
+
+=item username
+
+wsgetmail will authenticate as this user. Set this to an email address
+string in your wsgetmail configuration file.
+
+=item user_password
+
+Set this to the password string for C<username> in your wsgetmail
+configuration file.
+
+=back
+
+=head2 Configuring the mail delivery command
+
+Now that you've configured wsgetmail to access a mail account, all that's
+left is configuring delivery.
+
+=over 4
+
+=item folder
+
+Set this to the name string of a mail folder to read in your wsgetmail
+configuration file.
+
+=item command
+
+Set this to executable command string in your wsgetmail configuration
+file. You can specify an absolute path, or a plain command name which will
+be found from C<$PATH>. For each email wsgetmail retrieves, it will run this
+command and pass the message data to it via standard input.
+
+=item command_args
+
+Set this to a string with additional arguments to call C<command> with in
+your wsgetmail configuration file. These arguments follow shell quoting
+rules: you can escape characters with a backslash, and denote a single
+string argument with single or double quotes.
+
+=item action_on_fetched
+
+Set this to a literal string C<"mark_as_read"> or C<"delete"> in your
+wsgetmail configuration file. For each email wsgetmail retrieves, after the
+configured delivery command succeeds, it will take this action on the message.
+
+If you set this to C<"mark_as_read">, wsgetmail will only retrieve and
+deliver messages that are marked unread in the configured folder, so it does
+not try to deliver the same email multiple times.
+
+=back
+
+=head1 TESTING AND DEPLOYMENT
+
+After you write your wsgetmail configuration file, you can test it by running:
+
+    wsgetmail --debug --dry-run --config=wsgetmail.json
+
+This will read and deliver messages, but will not mark them as read or
+delete them. If there are any problems, those will be reported in the error
+output. You can update your configuration file and try again until wsgetmail
+runs successfully.
+
+Once your configuration is stable, you can configure wsgetmail to run
+periodically through cron or a systemd service on a timer.
+
+=head1 LIMITATIONS
+
+wsgetmail can only read from a single folder each time it runs. If you need
+to read multiple folders (possibly spanning different accounts), then you
+need to run it multiple times with different configuration.
+
+If you only need to change a couple small configuration settings like the
+folder name, you can use the C<--options> argument to override those from a
+base configuration file. For example:
+
+    wsgetmail --config=wsgetmail.json --options='{"folder": "Inbox"}'
+    wsgetmail --config=wsgetmail.json --options='{"folder": "Other Folder"}'
+
+NOTE: Setting C<secret> or C<user_password> with C<--options> is not secure
+and may expose your credentials to other users on the local system. If you
+need to set these options, or just change a lot of settings in your
+configuration, just run wsgetmail with different configurations:
+
+    wsgetmail --config=account01.json
+    wsgetmail --config=account02.json
+
+=head1 AUTHOR
+
+Best Practical Solutions, LLC <modules at bestpractical.com>
+
+=head1 LICENSE AND COPYRIGHT
+
+This software is Copyright (c) 2015-2020 by Best Practical Solutions, LLC.
+
+This is free software, licensed under:
 
-an example configuration file is included in the docs/ directory of this package
+The GNU General Public License, Version 2, June 1991
 
 =cut

commit 6643055eb616c362ad296e142f3613ca8e98fb98
Author: Brett Smith <brett at bestpractical.com>
Date:   Wed Jan 19 14:33:30 2022 -0500

    Exclude Moo constructor from pod coverage tests

diff --git a/t/pod-coverage.t b/t/pod-coverage.t
index f5728a5..8b3e645 100644
--- a/t/pod-coverage.t
+++ b/t/pod-coverage.t
@@ -21,4 +21,6 @@ eval "use Pod::Coverage $min_pc";
 plan skip_all => "Pod::Coverage $min_pc required for testing POD coverage"
     if $@;
 
-all_pod_coverage_ok();
+all_pod_coverage_ok({
+    also_private => [qr/^BUILD(ARGS)?$/],
+});

commit 129967686f2d658ee105945d0b4e2a1d65e61c8f
Author: Brett Smith <brett at bestpractical.com>
Date:   Wed Jan 19 14:24:40 2022 -0500

    Add MANIFEST.SKIP to support MANIFEST check

diff --git a/MANIFEST b/MANIFEST
index 83caf7e..0e3f5a9 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -29,6 +29,7 @@ lib/App/wsgetmail/MS365/Client.pm
 lib/App/wsgetmail/MS365/Message.pm
 Makefile.PL
 MANIFEST			This list of files
+MANIFEST.SKIP
 META.yml
 README
 t/00-load.t
diff --git a/MANIFEST.SKIP b/MANIFEST.SKIP
new file mode 100644
index 0000000..504fc9d
--- /dev/null
+++ b/MANIFEST.SKIP
@@ -0,0 +1,6 @@
+.git/*
+.gitignore
+Makefile$
+MYMETA.*
+blib/*
+pm_to_blib

commit 199a699c1cf457e127f2c83acf3a7ceeab25d80a
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Thu Jan 6 18:42:05 2022 -0600

    Add missing requirements to Makefile.PL

diff --git a/Makefile.PL b/Makefile.PL
index e5429f9..ed804e5 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -4,14 +4,23 @@ all_from 'lib/App/wsgetmail.pm';
 license 'gpl_2';
 
 requires 'Azure::AD::ClientCredentials';
+requires 'Clone';
+requires 'FindBin';
+requires 'File::Slurp';
+requires 'File::Temp';
+requires 'Getopt::Long';
+requires 'IPC::Run';
+requires 'JSON';
+requires 'LWP::UserAgent' => '6.42';
+requires 'Module::Load';
+requires 'Moo';
+requires 'Pod::Usage';
+requires 'strict';
 requires 'Test::LWP::UserAgent';
 requires 'Test::More';
-requires 'LWP::UserAgent' => '6.42';
 requires 'URI::Escape';
 requires 'URI';
-requires 'Moo';
-requires 'JSON';
-requires 'IPC::Run';
+requires 'warnings';
 
 install_script('bin/wsgetmail');
 auto_install();

-----------------------------------------------------------------------


hooks/post-receive
-- 
app-wsgetmail


More information about the Bps-public-commit mailing list