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

BPS Git Server git at git.bestpractical.com
Wed Jan 19 20:32:54 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  1c85a79e64a04eae3744907ed1c64ed28c8d9996 (commit)

- Log -----------------------------------------------------------------
commit 1c85a79e64a04eae3744907ed1c64ed28c8d9996
Author: Brett Smith <brett at bestpractical.com>
Date:   Wed Jan 19 12:26:34 2022 -0500

    Release version 0.05

diff --git a/Changes b/Changes
index 344cc5c..7103b91 100644
--- a/Changes
+++ b/Changes
@@ -1,5 +1,15 @@
 Revision history for App-wsgetmail
 
+0.05    19/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
 
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 d881de7..3d9c13a 100644
--- a/lib/App/wsgetmail.pm
+++ b/lib/App/wsgetmail.pm
@@ -2,7 +2,7 @@ package App::wsgetmail;
 
 use Moo;
 
-our $VERSION = '0.04';
+our $VERSION = '0.05';
 
 =head1 NAME
 
@@ -10,7 +10,7 @@ App::wsgetmail - Fetch mail from the cloud using webservices
 
 =head1 VERSION
 
-0.04
+0.05
 
 =head1 SYNOPSIS
 

commit 18d6e583297a94188c7868e5a8967b4e3d956830
Author: Brett Smith <brett at bestpractical.com>
Date:   Wed Jan 19 15:20:44 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 a963c4c..f92f319 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/manifest.t
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 5102254..0000000
--- a/README
+++ /dev/null
@@ -1,64 +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]
-
-INSTALLATION
-
-    perl Makefile.PL
-    sudo PERL_CANARY_STABILITY_NOPROMPT=1 make
-    make test
-    sudo make install
-
-    The tool will be installed in /usr/local/bin or binary path for your system perl configuration
-
-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 9ab610b5c8a10adb852635c8f361921de5b698d0
Author: Brett Smith <brett at bestpractical.com>
Date:   Wed Jan 19 11:58:58 2022 -0500

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

diff --git a/.gitignore b/.gitignore
index 4851496..bdda478 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,5 +8,6 @@ pod2htm*.tmp
 *.bak
 *.swp
 /MYMETA.*
+/bin/wsgetmail
 /t/tmp
 /xt/tmp
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 7bb9dc9..a963c4c 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 bf89abd..41acf44 100644
--- a/MANIFEST.SKIP
+++ b/MANIFEST.SKIP
@@ -1,2 +1,3 @@
 /.git/
 /.gitignore
+/bin/wsgetmail
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 7687eb9e43f4c1338dd0c7f7ef02881154246207
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 f1ae1f6..7bb9dc9 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 edaea3a2fc4432b887a2729cea90ba29d6ee69f1
Author: Brett Smith <brett at bestpractical.com>
Date:   Wed Jan 19 10:26:22 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 c1685b0..d881de7 100644
--- a/lib/App/wsgetmail.pm
+++ b/lib/App/wsgetmail.pm
@@ -12,72 +12,78 @@ App::wsgetmail - Fetch mail from the cloud using webservices
 
 0.04
 
-=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]
+    {
+    "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.
 
 =head1 INSTALLATION
 
     perl Makefile.PL
-    sudo PERL_CANARY_STABILITY_NOPROMPT=1 make
+    make PERL_CANARY_STABILITY_NOPROMPT=1
     make test
     sudo make install
 
-The tool will be installed in /usr/local/bin or binary path for your system perl configuration
-
-=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"
-}
-
-an example configuration file is included in the docs/ directory of this package
-
 =cut
 
 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,
@@ -85,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,
@@ -125,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) = @_;
@@ -147,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;
@@ -183,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 6587f9edb043f64bf9c696b8aa4b7af126b1f69d
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 d7fe8afe34c7cd0ae8c987dc6eaaf6625cc2633c
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 7f77db94f451ba396fee3d39ed4e0b9d1060d5d6
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 abafdc4e9aaa6aba3d9cc5b5f42b9391384b31c2
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 3f084d6f6928cf56fd85e26d5c856248430a740c
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 ee1dda34ac7c0dbbb0e7dae88e64688795f8751b
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 d56180a3f57f4593165908e499e7cf31cffb3ed6
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 c40f1870de8f1a425dd1bcb745c36a6fbfc3874d
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 dce0607..f1ae1f6 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..bf89abd
--- /dev/null
+++ b/MANIFEST.SKIP
@@ -0,0 +1,2 @@
+/.git/
+/.gitignore

commit cc02ad6fcb3a6028a1449b56f5c8a8729e3ce455
Author: Brett Smith <brett at bestpractical.com>
Date:   Wed Jan 19 14:16:19 2022 -0500

    Add release tests to the repository
    
    These were added to the original public repository but not the original
    source repository.

diff --git a/MANIFEST b/MANIFEST
index 83caf7e..dce0607 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -33,4 +33,7 @@ META.yml
 README
 t/00-load.t
 t/basic.t
+t/manifest.t
 t/mock_responses/messages.json
+t/pod-coverage.t
+t/pod.t
diff --git a/t/manifest.t b/t/manifest.t
new file mode 100644
index 0000000..e0b558e
--- /dev/null
+++ b/t/manifest.t
@@ -0,0 +1,15 @@
+#!perl -T
+use 5.006;
+use strict;
+use warnings;
+use Test::More;
+
+unless ( $ENV{RELEASE_TESTING} ) {
+    plan( skip_all => "Author tests not required for installation" );
+}
+
+my $min_tcm = 0.9;
+eval "use Test::CheckManifest $min_tcm";
+plan skip_all => "Test::CheckManifest $min_tcm required" if $@;
+
+ok_manifest();
diff --git a/t/pod-coverage.t b/t/pod-coverage.t
new file mode 100644
index 0000000..f5728a5
--- /dev/null
+++ b/t/pod-coverage.t
@@ -0,0 +1,24 @@
+#!perl -T
+use 5.006;
+use strict;
+use warnings;
+use Test::More;
+
+unless ( $ENV{RELEASE_TESTING} ) {
+    plan( skip_all => "Author tests not required for installation" );
+}
+
+# Ensure a recent version of Test::Pod::Coverage
+my $min_tpc = 1.08;
+eval "use Test::Pod::Coverage $min_tpc";
+plan skip_all => "Test::Pod::Coverage $min_tpc required for testing POD coverage"
+    if $@;
+
+# Test::Pod::Coverage doesn't require a minimum Pod::Coverage version,
+# but older versions don't recognize some common documentation styles
+my $min_pc = 0.18;
+eval "use Pod::Coverage $min_pc";
+plan skip_all => "Pod::Coverage $min_pc required for testing POD coverage"
+    if $@;
+
+all_pod_coverage_ok();
diff --git a/t/pod.t b/t/pod.t
new file mode 100644
index 0000000..4d3a0ce
--- /dev/null
+++ b/t/pod.t
@@ -0,0 +1,16 @@
+#!perl -T
+use 5.006;
+use strict;
+use warnings;
+use Test::More;
+
+unless ( $ENV{RELEASE_TESTING} ) {
+    plan( skip_all => "Author tests not required for installation" );
+}
+
+# Ensure a recent version of Test::Pod
+my $min_tp = 1.22;
+eval "use Test::Pod $min_tp";
+plan skip_all => "Test::Pod $min_tp required for testing POD" if $@;
+
+all_pod_files_ok();

commit 4e00c9dacba6db86ae06761177eece0b3a4949d0
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();

commit 14a2a39f714cf61c6a2e42266132fd039db62a9e
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Apr 9 04:07:58 2021 +0800

    Skip the misleading $! as it is not set by IPC::Run::run

diff --git a/lib/App/wsgetmail/MDA.pm b/lib/App/wsgetmail/MDA.pm
index 7258b7b..a76c423 100644
--- a/lib/App/wsgetmail/MDA.pm
+++ b/lib/App/wsgetmail/MDA.pm
@@ -76,10 +76,10 @@ sub _run_command {
     }
     my $ok = run ([ $self->command, _split_command_args($self->command_args, 1)], $fh, \$output, \$error, timeout( $self->command_timeout + 5 ) );
     unless ($ok) {
-        warn sprintf('failed to run command "%s %s" for file %s : %s : %s',
+        warn sprintf('failed to run command "%s %s" for file %s : %s',
                      $self->command,
                      ($self->debug ? join(' ', _split_command_args($self->command_args)) : '' ),
-                     $filename, $!, $?);
+                     $filename, $?);
         warn "output : $output\nerror:$error\n" if ($self->debug);
     }
     close $fh;

commit 6f8ab28bf2c84065182d9c4a95d5acb6b8a7b6ca
Merge: 0ed5b63 b2990d4
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Mon Apr 5 21:13:04 2021 +0800

    Merge branch 'move-close-after-warn'


commit b2990d41ebc12020c63d27163c7d995e6d6dacf9
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Tue Mar 30 14:43:18 2021 -0500

    Move close fh after warn for correct error strings
    
    Reading the exit and error string values should be immediately
    after the syscall we're wanting values for, else risks another
    syscall setting them to something else.
    
    In the case of _run_command we want to read $! and $? for the
    command being run through IPC run, not the values from close.

diff --git a/lib/App/wsgetmail/MDA.pm b/lib/App/wsgetmail/MDA.pm
index 78964c1..7258b7b 100644
--- a/lib/App/wsgetmail/MDA.pm
+++ b/lib/App/wsgetmail/MDA.pm
@@ -75,8 +75,6 @@ sub _run_command {
         return 1;
     }
     my $ok = run ([ $self->command, _split_command_args($self->command_args, 1)], $fh, \$output, \$error, timeout( $self->command_timeout + 5 ) );
-    close $fh;
-
     unless ($ok) {
         warn sprintf('failed to run command "%s %s" for file %s : %s : %s',
                      $self->command,
@@ -84,7 +82,7 @@ sub _run_command {
                      $filename, $!, $?);
         warn "output : $output\nerror:$error\n" if ($self->debug);
     }
-
+    close $fh;
     return $ok;
 }
 

commit 0ed5b63bb7b5b7c2d5841397c9b21134bd285e34
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Wed Aug 19 13:42:17 2020 +0100

    Update version/changelog of wsgetmail for version 0.04

diff --git a/Changes b/Changes
index 27a51fc..344cc5c 100644
--- a/Changes
+++ b/Changes
@@ -1,5 +1,8 @@
 Revision history for App-wsgetmail
 
+0.04    18/8/20
+        Documentation improvements
+
 0.03    4/8/20
         Improved debugging
 
diff --git a/lib/App/wsgetmail.pm b/lib/App/wsgetmail.pm
index f5fa83e..c1685b0 100644
--- a/lib/App/wsgetmail.pm
+++ b/lib/App/wsgetmail.pm
@@ -2,7 +2,7 @@ package App::wsgetmail;
 
 use Moo;
 
-our $VERSION = '0.03';
+our $VERSION = '0.04';
 
 =head1 NAME
 
@@ -10,7 +10,7 @@ App::wsgetmail - Fetch mail from the cloud using webservices
 
 =head1 VERSION
 
-0.03
+0.04
 
 =head1 DESCRIPTION
 

commit f83782e30a99b5435562e538efbb40add7f49456
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Wed Aug 19 13:40:36 2020 +0100

    Add INSTALLATION to wsgetmail README and pod

diff --git a/README b/README
index e41697d..5102254 100644
--- a/README
+++ b/README
@@ -11,6 +11,15 @@ DESCRIPTION
 SYNOPSIS
     wsgetmail365 --configuration path/to/file.json [--debug]
 
+INSTALLATION
+
+    perl Makefile.PL
+    sudo PERL_CANARY_STABILITY_NOPROMPT=1 make
+    make test
+    sudo make install
+
+    The tool will be installed in /usr/local/bin or binary path for your system perl configuration
+
 CONFIGURATION
     Configuration of the wsgetmail tool needs the following fields specific
     to the ms365 application: Application (client) ID, Directory (tenant) ID
diff --git a/lib/App/wsgetmail.pm b/lib/App/wsgetmail.pm
index cea9ca0..f5fa83e 100644
--- a/lib/App/wsgetmail.pm
+++ b/lib/App/wsgetmail.pm
@@ -24,6 +24,16 @@ configurable action with the fetched email.
 
 wsgetmail365 --configuration path/to/file.json [--debug] [ --dry-run]
 
+
+=head1 INSTALLATION
+
+    perl Makefile.PL
+    sudo PERL_CANARY_STABILITY_NOPROMPT=1 make
+    make test
+    sudo make install
+
+The tool will be installed in /usr/local/bin or binary path for your system perl configuration
+
 =head1 CONFIGURATION
 
 Configuration of the wsgetmail tool needs the following fields specific to the ms365 application:

commit c3696e4f0a1dbed2c4bcd08442094f092fdaf107
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Tue Aug 4 12:46:49 2020 +0100

    Bumped version for next release of wsgetmail

diff --git a/Changes b/Changes
index 48bddb5..27a51fc 100644
--- a/Changes
+++ b/Changes
@@ -1,5 +1,8 @@
 Revision history for App-wsgetmail
 
+0.03    4/8/20
+        Improved debugging
+
 0.02    17/07/20
         Reworked mail delivery
 
diff --git a/lib/App/wsgetmail.pm b/lib/App/wsgetmail.pm
index 827a2a3..cea9ca0 100644
--- a/lib/App/wsgetmail.pm
+++ b/lib/App/wsgetmail.pm
@@ -2,7 +2,7 @@ package App::wsgetmail;
 
 use Moo;
 
-our $VERSION = '0.02';
+our $VERSION = '0.03';
 
 =head1 NAME
 
@@ -10,7 +10,7 @@ App::wsgetmail - Fetch mail from the cloud using webservices
 
 =head1 VERSION
 
-0.02
+0.03
 
 =head1 DESCRIPTION
 

commit 7d2d9fec1b67d0aa34d5eb4e0fdacf6223e42edd
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Tue Aug 4 12:45:02 2020 +0100

    Increase detail of debug output in wsgetmail

diff --git a/lib/App/wsgetmail/MS365.pm b/lib/App/wsgetmail/MS365.pm
index ee4e261..a781a3d 100644
--- a/lib/App/wsgetmail/MS365.pm
+++ b/lib/App/wsgetmail/MS365.pm
@@ -81,6 +81,11 @@ has post_fetch_action => (
     required => 1
 );
 
+has debug => (
+    is => 'rw',
+    default => sub { return 0 }
+);
+
 ###
 
 has _client => (
@@ -108,7 +113,7 @@ has _next_fetch_url => (
 );
 
 
-my @config_fields = qw(client_id tenant_id username user_password global_access secret folder post_fetch_action);
+my @config_fields = qw(client_id tenant_id username user_password global_access secret folder post_fetch_action debug);
 around BUILDARGS => sub {
   my ( $orig, $class, $config ) = @_;
 
@@ -303,7 +308,7 @@ sub _get_message_filters {
     my $self = shift;
     #TODO: handle filtering multiple folders
     my $filters = [ ];
-    if ( $self->post_fetch_action eq 'mark_message_as_read') {
+    if ( $self->post_fetch_action && ($self->post_fetch_action eq 'mark_message_as_read')) {
         push(@$filters, 'isRead eq false');
     }
 
@@ -320,7 +325,8 @@ sub _build_client {
         secret => $self->secret,
         client_id => $self->client_id,
         tenant_id => $self->tenant_id,
-        global_access => $self->global_access
+        global_access => $self->global_access,
+        debug => $self->debug,
     } );
     return $client;
 
diff --git a/lib/App/wsgetmail/MS365/Client.pm b/lib/App/wsgetmail/MS365/Client.pm
index 441d9dc..fd826e9 100644
--- a/lib/App/wsgetmail/MS365/Client.pm
+++ b/lib/App/wsgetmail/MS365/Client.pm
@@ -77,6 +77,11 @@ has resource_path => (
     default => sub { return 'v1.0' }
 );
 
+has debug => (
+    is => 'rw',
+    default => sub { return 0 }
+);
+
 has _ua => (
     builder   => '_build_authorised_ua',
     is => 'ro',
@@ -131,6 +136,7 @@ sub get_request {
     my ($self, $parts, $params) = @_;
     # add error handling!
     my $uri = URI->new($self->build_rest_uri(@$parts));
+    warn "making GET request to url $uri" if ($self->debug);
     $uri->query_form($params) if ($params);
     return $self->_ua->get($uri);
 }
@@ -141,6 +147,7 @@ sub get_request {
 
 sub get_request_by_url {
     my ($self, $url) = @_;
+    warn "making GET request to url $url" if ($self->debug);
     return $self->_ua->get($url);
 }
 
@@ -151,6 +158,7 @@ sub get_request_by_url {
 sub delete_request {
     my ($self, $parts, $params) = @_;
     my $url = $self->build_rest_uri(@$parts);
+    warn "making DELETE request to url $url" if ($self->debug);
     return $self->_ua->delete($url);
 }
 
@@ -161,6 +169,7 @@ sub delete_request {
 sub post_request {
     my ($self, $path_parts, $post_data) = @_;
     my $url = $self->build_rest_uri(@$path_parts);
+    warn "making POST request to url $url" if ($self->debug);
     return $self->_ua->post($url,$post_data);
 }
 
@@ -171,6 +180,7 @@ sub post_request {
 sub patch_request {
      my ($self, $path_parts, $patch_params) = @_;
      my $url = $self->build_rest_uri(@$path_parts);
+     warn "making PATCH request to url $url" if ($self->debug);
      return $self->_ua->patch($url,%$patch_params);
  }
 
@@ -179,6 +189,7 @@ sub patch_request {
 sub _build_authorised_ua {
     my $self = shift;
     my $ua = $self->_new_useragent;
+    warn "getting system access token" if ($self->debug);
     $ua->default_header( Authorization => $self->_access_token() );
     return $ua;
 }
@@ -199,6 +210,7 @@ sub _get_user_access_token {
     my $self = shift;
     my $ua = $self->_new_useragent;
     my $access_token;
+    warn "getting user access token" if ($self->debug);
     my $oauth_login_url = sprintf('https://login.windows.net/%s/oauth2/token', $self->tenant_id);
     my $response = $ua->post( $oauth_login_url,
                               {
@@ -218,6 +230,7 @@ sub _get_user_access_token {
     }
     else {
         # throw error
+        warn "auth response from server : $raw_message" if ($self->debug);
         die sprintf('unable to get user access token for user %s request failed with status %s ', $self->username, $response->status_line);
     }
     return $access_token;

commit 42a12de599613e0496653ccfad5026136d2ee252
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Tue Aug 4 12:44:22 2020 +0100

    Prevent warnings with no action-on-fetch setting for wsgetmail

diff --git a/lib/App/wsgetmail.pm b/lib/App/wsgetmail.pm
index cd15566..827a2a3 100644
--- a/lib/App/wsgetmail.pm
+++ b/lib/App/wsgetmail.pm
@@ -103,11 +103,13 @@ sub _build__post_fetch_action {
     my $self = shift;
     my $fetched_action_method;
     my $action = $self->config->{action_on_fetched};
+    return undef unless (defined $action);
     if (lc($action) eq 'mark_as_read') {
         $fetched_action_method = 'mark_message_as_read';
     } elsif ( lc($action) eq "delete" ) {
         $fetched_action_method = 'delete_message';
     } else {
+        $fetched_action_method = undef;
         warn "no recognised action for fetched mail, mailbox not updated";
     }
     return $fetched_action_method;

commit 412ab863cdbe0d55cf2d9f0d36f25acfe4398488
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Fri Jul 17 19:57:12 2020 +0100

    Updated Makefile.PL and Module::Install for wsgetmail release

diff --git a/MANIFEST b/MANIFEST
index 9ebc6d3..83caf7e 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -7,8 +7,8 @@ doc/register_api_app2.png
 doc/register_api_app3.png
 doc/register_api_app4.png
 doc/register_api_app5.png
-ignore.txt
 inc/Module/Install.pm
+inc/Module/AutoInstall.pm
 inc/Module/Install/Base.pm
 inc/Module/Install/Can.pm
 inc/Module/Install/Fetch.pm
@@ -17,8 +17,10 @@ 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/Win32.pm
 inc/Module/Install/WriteAll.pm
+inc/Module/Install/AutoInstall.pm
 inc/YAML/Tiny.pm
 lib/App/wsgetmail.pm
 lib/App/wsgetmail/MDA.pm
diff --git a/Makefile.PL b/Makefile.PL
index c96ef27..e5429f9 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -13,5 +13,7 @@ requires 'Moo';
 requires 'JSON';
 requires 'IPC::Run';
 
+install_script('bin/wsgetmail');
+auto_install();
 sign;
 WriteAll;
diff --git a/ignore.txt b/ignore.txt
deleted file mode 100644
index 91d3464..0000000
--- a/ignore.txt
+++ /dev/null
@@ -1,18 +0,0 @@
-Makefile
-Makefile.old
-Build
-Build.bat
-META.*
-MYMETA.*
-.build/
-_build/
-cover_db/
-blib/
-inc/
-.lwpcookies
-.last_cover_stats
-nytprof.out
-pod2htm*.tmp
-pm_to_blib
-App-wsgetmail-*
-App-wsgetmail-*.tar.gz
diff --git a/inc/Module/AutoInstall.pm b/inc/Module/AutoInstall.pm
new file mode 100644
index 0000000..8852e0b
--- /dev/null
+++ b/inc/Module/AutoInstall.pm
@@ -0,0 +1,934 @@
+#line 1
+package Module::AutoInstall;
+
+use strict;
+use Cwd                 ();
+use File::Spec          ();
+use ExtUtils::MakeMaker ();
+
+use vars qw{$VERSION};
+BEGIN {
+	$VERSION = '1.19';
+}
+
+# special map on pre-defined feature sets
+my %FeatureMap = (
+    ''      => 'Core Features',    # XXX: deprecated
+    '-core' => 'Core Features',
+);
+
+# various lexical flags
+my ( @Missing, @Existing,  %DisabledTests, $UnderCPAN, $InstallDepsTarget, $HasCPANPLUS );
+my (
+    $Config, $CheckOnly, $SkipInstall, $AcceptDefault, $TestOnly, $AllDeps,
+    $UpgradeDeps
+);
+my ( $PostambleActions, $PostambleActionsNoTest, $PostambleActionsUpgradeDeps,
+    $PostambleActionsUpgradeDepsNoTest, $PostambleActionsListDeps,
+    $PostambleActionsListAllDeps, $PostambleUsed, $NoTest);
+
+# See if it's a testing or non-interactive session
+_accept_default( $ENV{AUTOMATED_TESTING} or ! -t STDIN ); 
+_init();
+
+sub _accept_default {
+    $AcceptDefault = shift;
+}
+
+sub _installdeps_target {
+    $InstallDepsTarget = shift;
+}
+
+sub missing_modules {
+    return @Missing;
+}
+
+sub do_install {
+    __PACKAGE__->install(
+        [
+            $Config
+            ? ( UNIVERSAL::isa( $Config, 'HASH' ) ? %{$Config} : @{$Config} )
+            : ()
+        ],
+        @Missing,
+    );
+}
+
+# initialize various flags, and/or perform install
+sub _init {
+    foreach my $arg (
+        @ARGV,
+        split(
+            /[\s\t]+/,
+            $ENV{PERL_AUTOINSTALL} || $ENV{PERL_EXTUTILS_AUTOINSTALL} || ''
+        )
+      )
+    {
+        if ( $arg =~ /^--config=(.*)$/ ) {
+            $Config = [ split( ',', $1 ) ];
+        }
+        elsif ( $arg =~ /^--installdeps=(.*)$/ ) {
+            __PACKAGE__->install( $Config, @Missing = split( /,/, $1 ) );
+            exit 0;
+        }
+	elsif ( $arg =~ /^--upgradedeps=(.*)$/ ) {
+	    $UpgradeDeps = 1;
+	    __PACKAGE__->install( $Config, @Missing = split( /,/, $1 ) );
+	    exit 0;
+	}
+        elsif ( $arg =~ /^--default(?:deps)?$/ ) {
+            $AcceptDefault = 1;
+        }
+        elsif ( $arg =~ /^--check(?:deps)?$/ ) {
+            $CheckOnly = 1;
+        }
+        elsif ( $arg =~ /^--skip(?:deps)?$/ ) {
+            $SkipInstall = 1;
+        }
+        elsif ( $arg =~ /^--test(?:only)?$/ ) {
+            $TestOnly = 1;
+        }
+        elsif ( $arg =~ /^--all(?:deps)?$/ ) {
+            $AllDeps = 1;
+        }
+    }
+}
+
+# overrides MakeMaker's prompt() to automatically accept the default choice
+sub _prompt {
+    goto &ExtUtils::MakeMaker::prompt unless $AcceptDefault;
+
+    my ( $prompt, $default ) = @_;
+    my $y = ( $default =~ /^[Yy]/ );
+
+    print $prompt, ' [', ( $y ? 'Y' : 'y' ), '/', ( $y ? 'n' : 'N' ), '] ';
+    print "$default\n";
+    return $default;
+}
+
+# the workhorse
+sub import {
+    my $class = shift;
+    my @args  = @_ or return;
+    my $core_all;
+
+    print "*** $class version " . $class->VERSION . "\n";
+    print "*** Checking for Perl dependencies...\n";
+
+    my $cwd = Cwd::getcwd();
+
+    $Config = [];
+
+    my $maxlen = length(
+        (
+            sort   { length($b) <=> length($a) }
+              grep { /^[^\-]/ }
+              map  {
+                ref($_)
+                  ? ( ( ref($_) eq 'HASH' ) ? keys(%$_) : @{$_} )
+                  : ''
+              }
+              map { +{@args}->{$_} }
+              grep { /^[^\-]/ or /^-core$/i } keys %{ +{@args} }
+        )[0]
+    );
+
+    # We want to know if we're under CPAN early to avoid prompting, but
+    # if we aren't going to try and install anything anyway then skip the
+    # check entirely since we don't want to have to load (and configure)
+    # an old CPAN just for a cosmetic message
+
+    $UnderCPAN = _check_lock(1) unless $SkipInstall || $InstallDepsTarget;
+
+    while ( my ( $feature, $modules ) = splice( @args, 0, 2 ) ) {
+        my ( @required, @tests, @skiptests );
+        my $default  = 1;
+        my $conflict = 0;
+
+        if ( $feature =~ m/^-(\w+)$/ ) {
+            my $option = lc($1);
+
+            # check for a newer version of myself
+            _update_to( $modules, @_ ) and return if $option eq 'version';
+
+            # sets CPAN configuration options
+            $Config = $modules if $option eq 'config';
+
+            # promote every features to core status
+            $core_all = ( $modules =~ /^all$/i ) and next
+              if $option eq 'core';
+
+            next unless $option eq 'core';
+        }
+
+        print "[" . ( $FeatureMap{ lc($feature) } || $feature ) . "]\n";
+
+        $modules = [ %{$modules} ] if UNIVERSAL::isa( $modules, 'HASH' );
+
+        unshift @$modules, -default => &{ shift(@$modules) }
+          if ( ref( $modules->[0] ) eq 'CODE' );    # XXX: bugward compatibility
+
+        while ( my ( $mod, $arg ) = splice( @$modules, 0, 2 ) ) {
+            if ( $mod =~ m/^-(\w+)$/ ) {
+                my $option = lc($1);
+
+                $default   = $arg    if ( $option eq 'default' );
+                $conflict  = $arg    if ( $option eq 'conflict' );
+                @tests     = @{$arg} if ( $option eq 'tests' );
+                @skiptests = @{$arg} if ( $option eq 'skiptests' );
+
+                next;
+            }
+
+            printf( "- %-${maxlen}s ...", $mod );
+
+            if ( $arg and $arg =~ /^\D/ ) {
+                unshift @$modules, $arg;
+                $arg = 0;
+            }
+
+            # XXX: check for conflicts and uninstalls(!) them.
+            my $cur = _version_of($mod);
+            if (_version_cmp ($cur, $arg) >= 0)
+            {
+                print "loaded. ($cur" . ( $arg ? " >= $arg" : '' ) . ")\n";
+                push @Existing, $mod => $arg;
+                $DisabledTests{$_} = 1 for map { glob($_) } @skiptests;
+            }
+            else {
+                if (not defined $cur)   # indeed missing
+                {
+                    print "missing." . ( $arg ? " (would need $arg)" : '' ) . "\n";
+                }
+                else
+                {
+                    # no need to check $arg as _version_cmp ($cur, undef) would satisfy >= above
+                    print "too old. ($cur < $arg)\n";
+                }
+
+                push @required, $mod => $arg;
+            }
+        }
+
+        next unless @required;
+
+        my $mandatory = ( $feature eq '-core' or $core_all );
+
+        if (
+            !$SkipInstall
+            and (
+                $CheckOnly
+                or ($mandatory and $UnderCPAN)
+                or $AllDeps
+                or $InstallDepsTarget
+                or _prompt(
+                    qq{==> Auto-install the }
+                      . ( @required / 2 )
+                      . ( $mandatory ? ' mandatory' : ' optional' )
+                      . qq{ module(s) from CPAN?},
+                    $default ? 'y' : 'n',
+                ) =~ /^[Yy]/
+            )
+          )
+        {
+            push( @Missing, @required );
+            $DisabledTests{$_} = 1 for map { glob($_) } @skiptests;
+        }
+
+        elsif ( !$SkipInstall
+            and $default
+            and $mandatory
+            and
+            _prompt( qq{==> The module(s) are mandatory! Really skip?}, 'n', )
+            =~ /^[Nn]/ )
+        {
+            push( @Missing, @required );
+            $DisabledTests{$_} = 1 for map { glob($_) } @skiptests;
+        }
+
+        else {
+            $DisabledTests{$_} = 1 for map { glob($_) } @tests;
+        }
+    }
+
+    if ( @Missing and not( $CheckOnly or $UnderCPAN) ) {
+        require Config;
+        my $make = $Config::Config{make};
+        if ($InstallDepsTarget) {
+            print
+"*** To install dependencies type '$make installdeps' or '$make installdeps_notest'.\n";
+        }
+        else {
+            print
+"*** Dependencies will be installed the next time you type '$make'.\n";
+        }
+
+        # make an educated guess of whether we'll need root permission.
+        print "    (You may need to do that as the 'root' user.)\n"
+          if eval '$>';
+    }
+    print "*** $class configuration finished.\n";
+
+    chdir $cwd;
+
+    # import to main::
+    no strict 'refs';
+    *{'main::WriteMakefile'} = \&Write if caller(0) eq 'main';
+
+    return (@Existing, @Missing);
+}
+
+sub _running_under {
+    my $thing = shift;
+    print <<"END_MESSAGE";
+*** Since we're running under ${thing}, I'll just let it take care
+    of the dependency's installation later.
+END_MESSAGE
+    return 1;
+}
+
+# Check to see if we are currently running under CPAN.pm and/or CPANPLUS;
+# if we are, then we simply let it taking care of our dependencies
+sub _check_lock {
+    return unless @Missing or @_;
+
+    if ($ENV{PERL5_CPANM_IS_RUNNING}) {
+        return _running_under('cpanminus');
+    }
+
+    my $cpan_env = $ENV{PERL5_CPAN_IS_RUNNING};
+
+    if ($ENV{PERL5_CPANPLUS_IS_RUNNING}) {
+        return _running_under($cpan_env ? 'CPAN' : 'CPANPLUS');
+    }
+
+    require CPAN;
+
+    if ($CPAN::VERSION > '1.89') {
+        if ($cpan_env) {
+            return _running_under('CPAN');
+        }
+        return; # CPAN.pm new enough, don't need to check further
+    }
+
+    # last ditch attempt, this -will- configure CPAN, very sorry
+
+    _load_cpan(1); # force initialize even though it's already loaded
+
+    # Find the CPAN lock-file
+    my $lock = MM->catfile( $CPAN::Config->{cpan_home}, ".lock" );
+    return unless -f $lock;
+
+    # Check the lock
+    local *LOCK;
+    return unless open(LOCK, $lock);
+
+    if (
+            ( $^O eq 'MSWin32' ? _under_cpan() : <LOCK> == getppid() )
+        and ( $CPAN::Config->{prerequisites_policy} || '' ) ne 'ignore'
+    ) {
+        print <<'END_MESSAGE';
+
+*** Since we're running under CPAN, I'll just let it take care
+    of the dependency's installation later.
+END_MESSAGE
+        return 1;
+    }
+
+    close LOCK;
+    return;
+}
+
+sub install {
+    my $class = shift;
+
+    my $i;    # used below to strip leading '-' from config keys
+    my @config = ( map { s/^-// if ++$i; $_ } @{ +shift } );
+
+	my ( @modules, @installed, @modules_to_upgrade );
+	while (my ($pkg, $ver) = splice(@_, 0, 2)) {
+
+		# grep out those already installed
+		if (_version_cmp(_version_of($pkg), $ver) >= 0) {
+			push @installed, $pkg;
+			if ($UpgradeDeps) {
+				push @modules_to_upgrade, $pkg, $ver;
+			}
+		}
+		else {
+			push @modules, $pkg, $ver;
+		}
+	}
+
+	if ($UpgradeDeps) {
+		push @modules, @modules_to_upgrade;
+		@installed          = ();
+		@modules_to_upgrade = ();
+	}
+
+    return @installed unless @modules;  # nothing to do
+    return @installed if _check_lock(); # defer to the CPAN shell
+
+    print "*** Installing dependencies...\n";
+
+    return unless _connected_to('cpan.org');
+
+    my %args = @config;
+    my %failed;
+    local *FAILED;
+    if ( $args{do_once} and open( FAILED, '.#autoinstall.failed' ) ) {
+        while (<FAILED>) { chomp; $failed{$_}++ }
+        close FAILED;
+
+        my @newmod;
+        while ( my ( $k, $v ) = splice( @modules, 0, 2 ) ) {
+            push @newmod, ( $k => $v ) unless $failed{$k};
+        }
+        @modules = @newmod;
+    }
+
+    if ( _has_cpanplus() and not $ENV{PERL_AUTOINSTALL_PREFER_CPAN} ) {
+        _install_cpanplus( \@modules, \@config );
+    } else {
+        _install_cpan( \@modules, \@config );
+    }
+
+    print "*** $class installation finished.\n";
+
+    # see if we have successfully installed them
+    while ( my ( $pkg, $ver ) = splice( @modules, 0, 2 ) ) {
+        if ( _version_cmp( _version_of($pkg), $ver ) >= 0 ) {
+            push @installed, $pkg;
+        }
+        elsif ( $args{do_once} and open( FAILED, '>> .#autoinstall.failed' ) ) {
+            print FAILED "$pkg\n";
+        }
+    }
+
+    close FAILED if $args{do_once};
+
+    return @installed;
+}
+
+sub _install_cpanplus {
+    my @modules   = @{ +shift };
+    my @config    = _cpanplus_config( @{ +shift } );
+    my $installed = 0;
+
+    require CPANPLUS::Backend;
+    my $cp   = CPANPLUS::Backend->new;
+    my $conf = $cp->configure_object;
+
+    return unless $conf->can('conf') # 0.05x+ with "sudo" support
+               or _can_write($conf->_get_build('base'));  # 0.04x
+
+    # if we're root, set UNINST=1 to avoid trouble unless user asked for it.
+    my $makeflags = $conf->get_conf('makeflags') || '';
+    if ( UNIVERSAL::isa( $makeflags, 'HASH' ) ) {
+        # 0.03+ uses a hashref here
+        $makeflags->{UNINST} = 1 unless exists $makeflags->{UNINST};
+
+    } else {
+        # 0.02 and below uses a scalar
+        $makeflags = join( ' ', split( ' ', $makeflags ), 'UNINST=1' )
+          if ( $makeflags !~ /\bUNINST\b/ and eval qq{ $> eq '0' } );
+
+    }
+    $conf->set_conf( makeflags => $makeflags );
+    $conf->set_conf( prereqs   => 1 );
+
+    
+
+    while ( my ( $key, $val ) = splice( @config, 0, 2 ) ) {
+        $conf->set_conf( $key, $val );
+    }
+
+    my $modtree = $cp->module_tree;
+    while ( my ( $pkg, $ver ) = splice( @modules, 0, 2 ) ) {
+        print "*** Installing $pkg...\n";
+
+        MY::preinstall( $pkg, $ver ) or next if defined &MY::preinstall;
+
+        my $success;
+        my $obj = $modtree->{$pkg};
+
+        if ( $obj and _version_cmp( $obj->{version}, $ver ) >= 0 ) {
+            my $pathname = $pkg;
+            $pathname =~ s/::/\\W/;
+
+            foreach my $inc ( grep { m/$pathname.pm/i } keys(%INC) ) {
+                delete $INC{$inc};
+            }
+
+            my $rv = $cp->install( modules => [ $obj->{module} ] );
+
+            if ( $rv and ( $rv->{ $obj->{module} } or $rv->{ok} ) ) {
+                print "*** $pkg successfully installed.\n";
+                $success = 1;
+            } else {
+                print "*** $pkg installation cancelled.\n";
+                $success = 0;
+            }
+
+            $installed += $success;
+        } else {
+            print << ".";
+*** Could not find a version $ver or above for $pkg; skipping.
+.
+        }
+
+        MY::postinstall( $pkg, $ver, $success ) if defined &MY::postinstall;
+    }
+
+    return $installed;
+}
+
+sub _cpanplus_config {
+	my @config = ();
+	while ( @_ ) {
+		my ($key, $value) = (shift(), shift());
+		if ( $key eq 'prerequisites_policy' ) {
+			if ( $value eq 'follow' ) {
+				$value = CPANPLUS::Internals::Constants::PREREQ_INSTALL();
+			} elsif ( $value eq 'ask' ) {
+				$value = CPANPLUS::Internals::Constants::PREREQ_ASK();
+			} elsif ( $value eq 'ignore' ) {
+				$value = CPANPLUS::Internals::Constants::PREREQ_IGNORE();
+			} else {
+				die "*** Cannot convert option $key = '$value' to CPANPLUS version.\n";
+			}
+			push @config, 'prereqs', $value;
+		} elsif ( $key eq 'force' ) {
+		    push @config, $key, $value;
+		} elsif ( $key eq 'notest' ) {
+		    push @config, 'skiptest', $value;
+		} else {
+			die "*** Cannot convert option $key to CPANPLUS version.\n";
+		}
+	}
+	return @config;
+}
+
+sub _install_cpan {
+    my @modules   = @{ +shift };
+    my @config    = @{ +shift };
+    my $installed = 0;
+    my %args;
+
+    _load_cpan();
+    require Config;
+
+    if (CPAN->VERSION < 1.80) {
+        # no "sudo" support, probe for writableness
+        return unless _can_write( MM->catfile( $CPAN::Config->{cpan_home}, 'sources' ) )
+                  and _can_write( $Config::Config{sitelib} );
+    }
+
+    # if we're root, set UNINST=1 to avoid trouble unless user asked for it.
+    my $makeflags = $CPAN::Config->{make_install_arg} || '';
+    $CPAN::Config->{make_install_arg} =
+      join( ' ', split( ' ', $makeflags ), 'UNINST=1' )
+      if ( $makeflags !~ /\bUNINST\b/ and eval qq{ $> eq '0' } );
+
+    # don't show start-up info
+    $CPAN::Config->{inhibit_startup_message} = 1;
+
+    # set additional options
+    while ( my ( $opt, $arg ) = splice( @config, 0, 2 ) ) {
+        ( $args{$opt} = $arg, next )
+          if $opt =~ /^(?:force|notest)$/;    # pseudo-option
+        $CPAN::Config->{$opt} = $opt eq 'urllist' ? [$arg] : $arg;
+    }
+
+    if ($args{notest} && (not CPAN::Shell->can('notest'))) {
+	die "Your version of CPAN is too old to support the 'notest' pragma";
+    }
+
+    local $CPAN::Config->{prerequisites_policy} = 'follow';
+
+    while ( my ( $pkg, $ver ) = splice( @modules, 0, 2 ) ) {
+        MY::preinstall( $pkg, $ver ) or next if defined &MY::preinstall;
+
+        print "*** Installing $pkg...\n";
+
+        my $obj     = CPAN::Shell->expand( Module => $pkg );
+        my $success = 0;
+
+        if ( $obj and _version_cmp( $obj->cpan_version, $ver ) >= 0 ) {
+            my $pathname = $pkg;
+            $pathname =~ s/::/\\W/;
+
+            foreach my $inc ( grep { m/$pathname.pm/i } keys(%INC) ) {
+                delete $INC{$inc};
+            }
+
+            my $rv = do {
+		if ($args{force}) {
+		    CPAN::Shell->force( install => $pkg )
+		} elsif ($args{notest}) {
+		    CPAN::Shell->notest( install => $pkg )
+		} else {
+		    CPAN::Shell->install($pkg)
+		}
+	    };
+
+            $rv ||= eval {
+                $CPAN::META->instance( 'CPAN::Distribution', $obj->cpan_file, )
+                  ->{install}
+                  if $CPAN::META;
+            };
+
+            if ( $rv eq 'YES' ) {
+                print "*** $pkg successfully installed.\n";
+                $success = 1;
+            }
+            else {
+                print "*** $pkg installation failed.\n";
+                $success = 0;
+            }
+
+            $installed += $success;
+        }
+        else {
+            print << ".";
+*** Could not find a version $ver or above for $pkg; skipping.
+.
+        }
+
+        MY::postinstall( $pkg, $ver, $success ) if defined &MY::postinstall;
+    }
+
+    return $installed;
+}
+
+sub _has_cpanplus {
+    return (
+        $HasCPANPLUS = (
+            $INC{'CPANPLUS/Config.pm'}
+              or _load('CPANPLUS::Shell::Default')
+        )
+    );
+}
+
+# make guesses on whether we're under the CPAN installation directory
+sub _under_cpan {
+    require Cwd;
+    require File::Spec;
+
+    my $cwd  = File::Spec->canonpath( Cwd::getcwd() );
+    my $cpan = File::Spec->canonpath( $CPAN::Config->{cpan_home} );
+
+    return ( index( $cwd, $cpan ) > -1 );
+}
+
+sub _update_to {
+    my $class = __PACKAGE__;
+    my $ver   = shift;
+
+    return
+      if _version_cmp( _version_of($class), $ver ) >= 0;  # no need to upgrade
+
+    if (
+        _prompt( "==> A newer version of $class ($ver) is required. Install?",
+            'y' ) =~ /^[Nn]/
+      )
+    {
+        die "*** Please install $class $ver manually.\n";
+    }
+
+    print << ".";
+*** Trying to fetch it from CPAN...
+.
+
+    # install ourselves
+    _load($class) and return $class->import(@_)
+      if $class->install( [], $class, $ver );
+
+    print << '.'; exit 1;
+
+*** Cannot bootstrap myself. :-( Installation terminated.
+.
+}
+
+# check if we're connected to some host, using inet_aton
+sub _connected_to {
+    my $site = shift;
+
+    return (
+        ( _load('Socket') and Socket::inet_aton($site) ) or _prompt(
+            qq(
+*** Your host cannot resolve the domain name '$site', which
+    probably means the Internet connections are unavailable.
+==> Should we try to install the required module(s) anyway?), 'n'
+          ) =~ /^[Yy]/
+    );
+}
+
+# check if a directory is writable; may create it on demand
+sub _can_write {
+    my $path = shift;
+    mkdir( $path, 0755 ) unless -e $path;
+
+    return 1 if -w $path;
+
+    print << ".";
+*** You are not allowed to write to the directory '$path';
+    the installation may fail due to insufficient permissions.
+.
+
+    if (
+        eval '$>' and lc(`sudo -V`) =~ /version/ and _prompt(
+            qq(
+==> Should we try to re-execute the autoinstall process with 'sudo'?),
+            ((-t STDIN) ? 'y' : 'n')
+        ) =~ /^[Yy]/
+      )
+    {
+
+        # try to bootstrap ourselves from sudo
+        print << ".";
+*** Trying to re-execute the autoinstall process with 'sudo'...
+.
+        my $missing = join( ',', @Missing );
+        my $config = join( ',',
+            UNIVERSAL::isa( $Config, 'HASH' ) ? %{$Config} : @{$Config} )
+          if $Config;
+
+        return
+          unless system( 'sudo', $^X, $0, "--config=$config",
+            "--installdeps=$missing" );
+
+        print << ".";
+*** The 'sudo' command exited with error!  Resuming...
+.
+    }
+
+    return _prompt(
+        qq(
+==> Should we try to install the required module(s) anyway?), 'n'
+    ) =~ /^[Yy]/;
+}
+
+# load a module and return the version it reports
+sub _load {
+    my $mod  = pop; # method/function doesn't matter
+    my $file = $mod;
+    $file =~ s|::|/|g;
+    $file .= '.pm';
+    local $@;
+    return eval { require $file; $mod->VERSION } || ( $@ ? undef: 0 );
+}
+
+# report version without loading a module
+sub _version_of {
+    my $mod = pop; # method/function doesn't matter
+    my $file = $mod;
+    $file =~ s|::|/|g;
+    $file .= '.pm';
+    foreach my $dir ( @INC ) {
+        next if ref $dir;
+        my $path = File::Spec->catfile($dir, $file);
+        next unless -e $path;
+        require ExtUtils::MM_Unix;
+        return ExtUtils::MM_Unix->parse_version($path);
+    }
+    return undef;
+}
+
+# Load CPAN.pm and it's configuration
+sub _load_cpan {
+    return if $CPAN::VERSION and $CPAN::Config and not @_;
+    require CPAN;
+
+    # CPAN-1.82+ adds CPAN::Config::AUTOLOAD to redirect to
+    #    CPAN::HandleConfig->load. CPAN reports that the redirection
+    #    is deprecated in a warning printed at the user.
+
+    # CPAN-1.81 expects CPAN::HandleConfig->load, does not have
+    #   $CPAN::HandleConfig::VERSION but cannot handle
+    #   CPAN::Config->load
+
+    # Which "versions expect CPAN::Config->load?
+
+    if ( $CPAN::HandleConfig::VERSION
+        || CPAN::HandleConfig->can('load')
+    ) {
+        # Newer versions of CPAN have a HandleConfig module
+        CPAN::HandleConfig->load;
+    } else {
+    	# Older versions had the load method in Config directly
+        CPAN::Config->load;
+    }
+}
+
+# compare two versions, either use Sort::Versions or plain comparison
+# return values same as <=>
+sub _version_cmp {
+    my ( $cur, $min ) = @_;
+    return -1 unless defined $cur;  # if 0 keep comparing
+    return 1 unless $min;
+
+    $cur =~ s/\s+$//;
+
+    # check for version numbers that are not in decimal format
+    if ( ref($cur) or ref($min) or $cur =~ /v|\..*\./ or $min =~ /v|\..*\./ ) {
+        if ( ( $version::VERSION or defined( _load('version') )) and
+             version->can('new') 
+            ) {
+
+            # use version.pm if it is installed.
+            return version->new($cur) <=> version->new($min);
+        }
+        elsif ( $Sort::Versions::VERSION or defined( _load('Sort::Versions') ) )
+        {
+
+            # use Sort::Versions as the sorting algorithm for a.b.c versions
+            return Sort::Versions::versioncmp( $cur, $min );
+        }
+
+        warn "Cannot reliably compare non-decimal formatted versions.\n"
+          . "Please install version.pm or Sort::Versions.\n";
+    }
+
+    # plain comparison
+    local $^W = 0;    # shuts off 'not numeric' bugs
+    return $cur <=> $min;
+}
+
+# nothing; this usage is deprecated.
+sub main::PREREQ_PM { return {}; }
+
+sub _make_args {
+    my %args = @_;
+
+    $args{PREREQ_PM} = { %{ $args{PREREQ_PM} || {} }, @Existing, @Missing }
+      if $UnderCPAN or $TestOnly;
+
+    if ( $args{EXE_FILES} and -e 'MANIFEST' ) {
+        require ExtUtils::Manifest;
+        my $manifest = ExtUtils::Manifest::maniread('MANIFEST');
+
+        $args{EXE_FILES} =
+          [ grep { exists $manifest->{$_} } @{ $args{EXE_FILES} } ];
+    }
+
+    $args{test}{TESTS} ||= 't/*.t';
+    $args{test}{TESTS} = join( ' ',
+        grep { !exists( $DisabledTests{$_} ) }
+          map { glob($_) } split( /\s+/, $args{test}{TESTS} ) );
+
+    my $missing = join( ',', @Missing );
+    my $config =
+      join( ',', UNIVERSAL::isa( $Config, 'HASH' ) ? %{$Config} : @{$Config} )
+      if $Config;
+
+    $PostambleActions = (
+        ($missing and not $UnderCPAN)
+        ? "\$(PERL) $0 --config=$config --installdeps=$missing"
+        : "\$(NOECHO) \$(NOOP)"
+    );
+
+    my $deps_list = join( ',', @Missing, @Existing );
+
+    $PostambleActionsUpgradeDeps =
+        "\$(PERL) $0 --config=$config --upgradedeps=$deps_list";
+
+    my $config_notest =
+      join( ',', (UNIVERSAL::isa( $Config, 'HASH' ) ? %{$Config} : @{$Config}),
+	  'notest', 1 )
+      if $Config;
+
+    $PostambleActionsNoTest = (
+        ($missing and not $UnderCPAN)
+        ? "\$(PERL) $0 --config=$config_notest --installdeps=$missing"
+        : "\$(NOECHO) \$(NOOP)"
+    );
+
+    $PostambleActionsUpgradeDepsNoTest =
+        "\$(PERL) $0 --config=$config_notest --upgradedeps=$deps_list";
+
+    $PostambleActionsListDeps =
+        '@$(PERL) -le "print for @ARGV" '
+            . join(' ', map $Missing[$_], grep $_ % 2 == 0, 0..$#Missing);
+
+    my @all = (@Missing, @Existing);
+
+    $PostambleActionsListAllDeps =
+        '@$(PERL) -le "print for @ARGV" '
+            . join(' ', map $all[$_], grep $_ % 2 == 0, 0..$#all);
+
+    return %args;
+}
+
+# a wrapper to ExtUtils::MakeMaker::WriteMakefile
+sub Write {
+    require Carp;
+    Carp::croak "WriteMakefile: Need even number of args" if @_ % 2;
+
+    if ($CheckOnly) {
+        print << ".";
+*** Makefile not written in check-only mode.
+.
+        return;
+    }
+
+    my %args = _make_args(@_);
+
+    no strict 'refs';
+
+    $PostambleUsed = 0;
+    local *MY::postamble = \&postamble unless defined &MY::postamble;
+    ExtUtils::MakeMaker::WriteMakefile(%args);
+
+    print << "." unless $PostambleUsed;
+*** WARNING: Makefile written with customized MY::postamble() without
+    including contents from Module::AutoInstall::postamble() --
+    auto installation features disabled.  Please contact the author.
+.
+
+    return 1;
+}
+
+sub postamble {
+    $PostambleUsed = 1;
+    my $fragment;
+
+    $fragment .= <<"AUTO_INSTALL" if !$InstallDepsTarget;
+
+config :: installdeps
+\t\$(NOECHO) \$(NOOP)
+AUTO_INSTALL
+
+    $fragment .= <<"END_MAKE";
+
+checkdeps ::
+\t\$(PERL) $0 --checkdeps
+
+installdeps ::
+\t$PostambleActions
+
+installdeps_notest ::
+\t$PostambleActionsNoTest
+
+upgradedeps ::
+\t$PostambleActionsUpgradeDeps
+
+upgradedeps_notest ::
+\t$PostambleActionsUpgradeDepsNoTest
+
+listdeps ::
+\t$PostambleActionsListDeps
+
+listalldeps ::
+\t$PostambleActionsListAllDeps
+
+END_MAKE
+
+    return $fragment;
+}
+
+1;
+
+__END__
+
+#line 1197
diff --git a/inc/Module/Install/AutoInstall.pm b/inc/Module/Install/AutoInstall.pm
new file mode 100644
index 0000000..0e3dada
--- /dev/null
+++ b/inc/Module/Install/AutoInstall.pm
@@ -0,0 +1,93 @@
+#line 1
+package Module::Install::AutoInstall;
+
+use strict;
+use Module::Install::Base ();
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+	$VERSION = '1.19';
+	@ISA     = 'Module::Install::Base';
+	$ISCORE  = 1;
+}
+
+sub AutoInstall { $_[0] }
+
+sub run {
+    my $self = shift;
+    $self->auto_install_now(@_);
+}
+
+sub write {
+    my $self = shift;
+    $self->auto_install(@_);
+}
+
+sub auto_install {
+    my $self = shift;
+    return if $self->{done}++;
+
+    # Flatten array of arrays into a single array
+    my @core = map @$_, map @$_, grep ref,
+               $self->build_requires, $self->requires;
+
+    my @config = @_;
+
+    # We'll need Module::AutoInstall
+    $self->include('Module::AutoInstall');
+    require Module::AutoInstall;
+
+    my @features_require = Module::AutoInstall->import(
+        (@config ? (-config => \@config) : ()),
+        (@core   ? (-core   => \@core)   : ()),
+        $self->features,
+    );
+
+    my %seen;
+    my @requires = map @$_, map @$_, grep ref, $self->requires;
+    while (my ($mod, $ver) = splice(@requires, 0, 2)) {
+        $seen{$mod}{$ver}++;
+    }
+    my @build_requires = map @$_, map @$_, grep ref, $self->build_requires;
+    while (my ($mod, $ver) = splice(@build_requires, 0, 2)) {
+        $seen{$mod}{$ver}++;
+    }
+    my @configure_requires = map @$_, map @$_, grep ref, $self->configure_requires;
+    while (my ($mod, $ver) = splice(@configure_requires, 0, 2)) {
+        $seen{$mod}{$ver}++;
+    }
+
+    my @deduped;
+    while (my ($mod, $ver) = splice(@features_require, 0, 2)) {
+        push @deduped, $mod => $ver unless $seen{$mod}{$ver}++;
+    }
+
+    $self->requires(@deduped);
+
+    $self->makemaker_args( Module::AutoInstall::_make_args() );
+
+    my $class = ref($self);
+    $self->postamble(
+        "# --- $class section:\n" .
+        Module::AutoInstall::postamble()
+    );
+}
+
+sub installdeps_target {
+    my ($self, @args) = @_;
+
+    $self->include('Module::AutoInstall');
+    require Module::AutoInstall;
+
+    Module::AutoInstall::_installdeps_target(1);
+
+    $self->auto_install(@args);
+}
+
+sub auto_install_now {
+    my $self = shift;
+    $self->auto_install(@_);
+    Module::AutoInstall::do_install();
+}
+
+1;
diff --git a/inc/Module/Install/Scripts.pm b/inc/Module/Install/Scripts.pm
new file mode 100644
index 0000000..2fc8f32
--- /dev/null
+++ b/inc/Module/Install/Scripts.pm
@@ -0,0 +1,28 @@
+package Module::Install::Scripts;
+
+use strict 'vars';
+use Module::Install::Base ();
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+	$VERSION = '1.19';
+	@ISA     = 'Module::Install::Base';
+	$ISCORE  = 1;
+}
+
+sub install_script {
+	my $self = shift;
+	my $args = $self->makemaker_args;
+	my $exe  = $args->{EXE_FILES} ||= [];
+        foreach ( @_ ) {
+		if ( -f $_ ) {
+			push @$exe, $_;
+		} elsif ( -d 'script' and -f "script/$_" ) {
+			push @$exe, "script/$_";
+		} else {
+			die("Cannot find script '$_'");
+		}
+	}
+}
+
+1;

commit c78d160c77432bd716b2c5cb4fb093e7923eba16
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Fri Jul 17 19:36:10 2020 +0100

    bumped version for wsgetmail to v0.02 for new package

diff --git a/Changes b/Changes
index c8fb631..48bddb5 100644
--- a/Changes
+++ b/Changes
@@ -1,5 +1,8 @@
 Revision history for App-wsgetmail
 
+0.02    17/07/20
+        Reworked mail delivery
+
 0.01    20/05/20
         Initial Implementation
 
diff --git a/MANIFEST b/MANIFEST
index 3740938..9ebc6d3 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -16,8 +16,6 @@ inc/Module/Install/Include.pm
 inc/Module/Install/Makefile.pm
 inc/Module/Install/Metadata.pm
 inc/Module/Install/ReadmeFromPod.pm
-inc/Module/Install/RTx.pm
-inc/Module/Install/RTx/Runtime.pm
 inc/Module/Install/Substitute.pm
 inc/Module/Install/Win32.pm
 inc/Module/Install/WriteAll.pm
diff --git a/Makefile.PL b/Makefile.PL
index 34c82d7..c96ef27 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -1,7 +1,6 @@
 use lib qw(.);
 use inc::Module::Install;
-
-RTx 'App-wsgetmail';
+all_from 'lib/App/wsgetmail.pm';
 license 'gpl_2';
 
 requires 'Azure::AD::ClientCredentials';
diff --git a/inc/Module/Install/RTx.pm b/inc/Module/Install/RTx.pm
deleted file mode 100644
index d371af9..0000000
--- a/inc/Module/Install/RTx.pm
+++ /dev/null
@@ -1,300 +0,0 @@
-#line 1
-package Module::Install::RTx;
-
-use 5.008;
-use strict;
-use warnings;
-no warnings 'once';
-
-use Module::Install::Base;
-use base 'Module::Install::Base';
-our $VERSION = '0.41';
-
-use FindBin;
-use File::Glob     ();
-use File::Basename ();
-
-my @DIRS = qw(etc lib html static bin sbin po var);
-my @INDEX_DIRS = qw(lib bin sbin);
-
-sub RTx {
-    my ( $self, $name, $extra_args ) = @_;
-    $extra_args ||= {};
-
-    # Set up names
-    my $fname = $name;
-    $fname =~ s!-!/!g;
-
-    $self->name( $name )
-        unless $self->name;
-    $self->all_from( "lib/$fname.pm" )
-        unless $self->version;
-    $self->abstract("$name Extension")
-        unless $self->abstract;
-    unless ( $extra_args->{no_readme_generation} ) {
-        $self->readme_from( "lib/$fname.pm",
-                            { options => [ quotes => "none" ] } );
-    }
-    $self->add_metadata("x_module_install_rtx_version", $VERSION );
-
-    my $installdirs = $ENV{INSTALLDIRS};
-    for ( @ARGV ) {
-        if ( /INSTALLDIRS=(.*)/ ) {
-            $installdirs = $1;
-        }
-    }
-
-    # Try to find RT.pm
-    my @prefixes = qw( /opt /usr/local /home /usr /sw /usr/share/request-tracker4);
-    $ENV{RTHOME} =~ s{/RT\.pm$}{} if defined $ENV{RTHOME};
-    $ENV{RTHOME} =~ s{/lib/?$}{}  if defined $ENV{RTHOME};
-    my @try = $ENV{RTHOME} ? ($ENV{RTHOME}, "$ENV{RTHOME}/lib") : ();
-    while (1) {
-        my @look = @INC;
-        unshift @look, grep {defined and -d $_} @try;
-        push @look, grep {defined and -d $_}
-            map { ( "$_/rt5/lib", "$_/lib/rt5", "$_/rt4/lib", "$_/lib/rt4", "$_/lib" ) } @prefixes;
-        last if eval {local @INC = @look; require RT; $RT::LocalLibPath};
-
-        warn
-            "Cannot find the location of RT.pm that defines \$RT::LocalPath in: @look\n";
-        my $given = $self->prompt("Path to directory containing your RT.pm:") or exit;
-        $given =~ s{/RT\.pm$}{};
-        $given =~ s{/lib/?$}{};
-        @try = ($given, "$given/lib");
-    }
-
-    print "Using RT configuration from $INC{'RT.pm'}:\n";
-
-    my $local_lib_path = $RT::LocalLibPath;
-    unshift @INC, $local_lib_path;
-    my $lib_path = File::Basename::dirname( $INC{'RT.pm'} );
-    unshift @INC, $lib_path;
-
-    # Set a baseline minimum version
-    unless ( $extra_args->{deprecated_rt} ) {
-        $self->requires_rt('4.0.0');
-    }
-
-    # Installation locations
-    my %path;
-    my $plugin_path;
-    if ( $installdirs && $installdirs eq 'vendor' ) {
-        $plugin_path = $RT::PluginPath;
-    } else {
-        $plugin_path = $RT::LocalPluginPath;
-    }
-    $path{$_} = $plugin_path . "/$name/$_"
-        foreach @DIRS;
-
-    # Copy RT 4.2.0 static files into NoAuth; insufficient for
-    # images, but good enough for css and js.
-    $path{static} = "$path{html}/NoAuth/"
-        unless $RT::StaticPath;
-
-    # Delete the ones we don't need
-    delete $path{$_} for grep {not -d "$FindBin::Bin/$_"} keys %path;
-
-    my %index = map { $_ => 1 } @INDEX_DIRS;
-    $self->no_index( directory => $_ ) foreach grep !$index{$_}, @DIRS;
-
-    my $args = join ', ', map "q($_)", map { ($_, "\$(DESTDIR)$path{$_}") }
-        sort keys %path;
-
-    printf "%-10s => %s\n", $_, $path{$_} for sort keys %path;
-
-    if ( my @dirs = map { ( -D => $_ ) } grep $path{$_}, qw(bin html sbin etc) ) {
-        my @po = map { ( -o => $_ ) }
-            grep -f,
-            File::Glob::bsd_glob("po/*.po");
-        $self->postamble(<< ".") if @po;
-lexicons ::
-\t\$(NOECHO) \$(PERL) -MLocale::Maketext::Extract::Run=xgettext -e \"xgettext(qw(@dirs @po))\"
-.
-    }
-
-    my $remove_files;
-    if( $extra_args->{'remove_files'} ){
-        $self->include('Module::Install::RTx::Remove');
-        our @remove_files;
-        eval { require "etc/upgrade/remove_files" }
-          or print "No remove file located, no files to remove\n";
-        $remove_files = join ",", map {"q(\$(DESTDIR)$plugin_path/$name/$_)"} @remove_files;
-    }
-
-    $self->include('Module::Install::RTx::Runtime') if $self->admin;
-    $self->include_deps( 'YAML::Tiny', 0 ) if $self->admin;
-    my $postamble = << ".";
-install ::
-\t\$(NOECHO) \$(PERL) -Ilib -I"$local_lib_path" -I"$lib_path" -Iinc -MModule::Install::RTx::Runtime -e"RTxPlugin()"
-.
-
-    if( $remove_files ){
-        $postamble .= << ".";
-\t\$(NOECHO) \$(PERL) -MModule::Install::RTx::Remove -e \"RTxRemove([$remove_files])\"
-.
-    }
-
-    $postamble .= << ".";
-\t\$(NOECHO) \$(PERL) -MExtUtils::Install -e \"install({$args})\"
-.
-
-    if ( $path{var} and -d $RT::MasonDataDir ) {
-        my ( $uid, $gid ) = ( stat($RT::MasonDataDir) )[ 4, 5 ];
-        $postamble .= << ".";
-\t\$(NOECHO) chown -R $uid:$gid $path{var}
-.
-    }
-
-    my %has_etc;
-    if ( File::Glob::bsd_glob("$FindBin::Bin/etc/schema.*") ) {
-        $has_etc{schema}++;
-    }
-    if ( File::Glob::bsd_glob("$FindBin::Bin/etc/acl.*") ) {
-        $has_etc{acl}++;
-    }
-    if ( -e 'etc/initialdata' ) { $has_etc{initialdata}++; }
-    if ( grep { /\d+\.\d+\.\d+.*$/ } glob('etc/upgrade/*.*.*') ) {
-        $has_etc{upgrade}++;
-    }
-
-    $self->postamble("$postamble\n");
-    if ( $path{lib} ) {
-        $self->makemaker_args( INSTALLSITELIB => $path{'lib'} );
-        $self->makemaker_args( INSTALLARCHLIB => $path{'lib'} );
-        $self->makemaker_args( INSTALLVENDORLIB => $path{'lib'} )
-    } else {
-        $self->makemaker_args( PM => { "" => "" }, );
-    }
-
-    $self->makemaker_args( INSTALLSITEMAN1DIR => "$RT::LocalPath/man/man1" );
-    $self->makemaker_args( INSTALLSITEMAN3DIR => "$RT::LocalPath/man/man3" );
-    $self->makemaker_args( INSTALLSITEARCH => "$RT::LocalPath/man" );
-
-    # INSTALLDIRS=vendor should install manpages into /usr/share/man.
-    # That is the default path in most distributions. Need input from
-    # Redhat, Centos etc.
-    $self->makemaker_args( INSTALLVENDORMAN1DIR => "/usr/share/man/man1" );
-    $self->makemaker_args( INSTALLVENDORMAN3DIR => "/usr/share/man/man3" );
-    $self->makemaker_args( INSTALLVENDORARCH => "/usr/share/man" );
-
-    if (%has_etc) {
-        print "For first-time installation, type 'make initdb'.\n";
-        my $initdb = '';
-        $initdb .= <<"." if $has_etc{schema};
-\t\$(NOECHO) \$(PERL) -Ilib -I"$local_lib_path" -I"$lib_path" -Iinc -MModule::Install::RTx::Runtime -e"RTxDatabase(qw(schema \$(NAME) \$(VERSION)))"
-.
-        $initdb .= <<"." if $has_etc{acl};
-\t\$(NOECHO) \$(PERL) -Ilib -I"$local_lib_path" -I"$lib_path" -Iinc -MModule::Install::RTx::Runtime -e"RTxDatabase(qw(acl \$(NAME) \$(VERSION)))"
-.
-        $initdb .= <<"." if $has_etc{initialdata};
-\t\$(NOECHO) \$(PERL) -Ilib -I"$local_lib_path" -I"$lib_path" -Iinc -MModule::Install::RTx::Runtime -e"RTxDatabase(qw(insert \$(NAME) \$(VERSION)))"
-.
-        $self->postamble("initdb ::\n$initdb\n");
-        $self->postamble("initialize-database ::\n$initdb\n");
-        if ($has_etc{upgrade}) {
-            print "To upgrade from a previous version of this extension, use 'make upgrade-database'\n";
-            my $upgradedb = qq|\t\$(NOECHO) \$(PERL) -Ilib -I"$local_lib_path" -I"$lib_path" -Iinc -MModule::Install::RTx::Runtime -e"RTxDatabase(qw(upgrade \$(NAME) \$(VERSION)))"\n|;
-            $self->postamble("upgrade-database ::\n$upgradedb\n");
-            $self->postamble("upgradedb ::\n$upgradedb\n");
-        }
-    }
-
-}
-
-sub requires_rt {
-    my ($self,$version) = @_;
-
-    _load_rt_handle();
-
-    if ($self->is_admin) {
-        $self->add_metadata("x_requires_rt", $version);
-        my @sorted = sort RT::Handle::cmp_version $version,'4.0.0';
-        $self->perl_version('5.008003') if $sorted[0] eq '4.0.0'
-            and (not $self->perl_version or '5.008003' > $self->perl_version);
-        @sorted = sort RT::Handle::cmp_version $version,'4.2.0';
-        $self->perl_version('5.010001') if $sorted[0] eq '4.2.0'
-            and (not $self->perl_version or '5.010001' > $self->perl_version);
-    }
-
-    # if we're exactly the same version as what we want, silently return
-    return if ($version eq $RT::VERSION);
-
-    my @sorted = sort RT::Handle::cmp_version $version,$RT::VERSION;
-
-    if ($sorted[-1] eq $version) {
-        die <<"EOT";
-
-**** Error: This extension requires RT $version. Your installed version
-            of RT ($RT::VERSION) is too old.
-
-EOT
-    }
-}
-
-sub requires_rt_plugin {
-    my $self = shift;
-    my ( $plugin ) = @_;
-
-    if ($self->is_admin) {
-        my $plugins = $self->Meta->{values}{"x_requires_rt_plugins"} || [];
-        push @{$plugins}, $plugin;
-        $self->add_metadata("x_requires_rt_plugins", $plugins);
-    }
-
-    my $path = $plugin;
-    $path =~ s{\:\:}{-}g;
-    $path = "$RT::LocalPluginPath/$path/lib";
-    if ( -e $path ) {
-        unshift @INC, $path;
-    } else {
-        my $name = $self->name;
-        warn <<"EOT";
-
-**** Warning: $name requires that the $plugin plugin be installed and
-              enabled; it does not appear to be installed.
-
-EOT
-    }
-    $self->requires(@_);
-}
-
-sub rt_too_new {
-    my ($self,$version,$msg) = @_;
-    my $name = $self->name;
-    $msg ||= <<EOT;
-
-**** Error: Your installed version of RT (%s) is too new; this extension
-            only works with versions older than %s.
-
-EOT
-    $self->add_metadata("x_rt_too_new", $version) if $self->is_admin;
-
-    _load_rt_handle();
-    my @sorted = sort RT::Handle::cmp_version $version,$RT::VERSION;
-
-    if ($sorted[0] eq $version) {
-        die sprintf($msg,$RT::VERSION,$version);
-    }
-}
-
-# RT::Handle runs FinalizeDatabaseType which calls RT->Config->Get
-# On 3.8, this dies.  On 4.0/4.2 ->Config transparently runs LoadConfig.
-# LoadConfig requires being able to read RT_SiteConfig.pm (root) so we'd
-# like to avoid pushing that on users.
-# Fake up just enough Config to let FinalizeDatabaseType finish, and
-# anyone later calling LoadConfig will overwrite our shenanigans.
-sub _load_rt_handle {
-    unless ($RT::Config) {
-        require RT::Config;
-        $RT::Config = RT::Config->new;
-        RT->Config->Set('DatabaseType','mysql');
-    }
-    require RT::Handle;
-}
-
-1;
-
-__END__
-
-#line 468
diff --git a/inc/Module/Install/RTx/Runtime.pm b/inc/Module/Install/RTx/Runtime.pm
deleted file mode 100644
index ae07502..0000000
--- a/inc/Module/Install/RTx/Runtime.pm
+++ /dev/null
@@ -1,80 +0,0 @@
-#line 1
-package Module::Install::RTx::Runtime;
-
-use base 'Exporter';
-our @EXPORT = qw/RTxDatabase RTxPlugin/;
-
-use strict;
-use File::Basename ();
-
-sub _rt_runtime_load {
-    require RT;
-
-    eval { RT::LoadConfig(); };
-    if (my $err = $@) {
-        die $err unless $err =~ /^RT couldn't load RT config file/m;
-        my $warn = <<EOT;
-This usually means that your current user cannot read the file.  You
-will likely need to run this installation step as root, or some user
-with more permissions.
-EOT
-        $err =~ s/This usually means.*/$warn/s;
-        die $err;
-    }
-}
-
-sub RTxDatabase {
-    my ($action, $name, $version) = @_;
-
-    _rt_runtime_load();
-
-    require RT::System;
-    my $has_upgrade = RT::System->can('AddUpgradeHistory');
-
-    my $lib_path = File::Basename::dirname($INC{'RT.pm'});
-    my @args = (
-        "-I.",
-        "-Ilib",
-        "-I$RT::LocalLibPath",
-        "-I$lib_path",
-        "$RT::SbinPath/rt-setup-database",
-        "--action"      => $action,
-        ($action eq 'upgrade' ? () : ("--datadir"     => "etc")),
-        (($action eq 'insert') ? ("--datafile"    => "etc/initialdata") : ()),
-        "--dba"         => $RT::DatabaseAdmin || $RT::DatabaseUser,
-        "--prompt-for-dba-password" => '',
-        ($has_upgrade ? ("--package" => $name, "--ext-version" => $version) : ()),
-    );
-    # If we're upgrading against an RT which isn't at least 4.2 (has
-    # AddUpgradeHistory) then pass --package.  Upgrades against later RT
-    # releases will pick up --package from AddUpgradeHistory.
-    if ($action eq 'upgrade' and not $has_upgrade) {
-        push @args, "--package" => $name;
-    }
-
-    print "$^X @args\n";
-    (system($^X, @args) == 0) or die "...returned with error: $?\n";
-}
-
-sub RTxPlugin {
-    my ($name) = @_;
-
-    _rt_runtime_load();
-    require YAML::Tiny;
-    my $data = YAML::Tiny::LoadFile('META.yml');
-    my $name = $data->{name};
-
-    my @enabled = RT->Config->Get('Plugins');
-    for my $required (@{$data->{x_requires_rt_plugins} || []}) {
-        next if grep {$required eq $_} @enabled;
-
-        warn <<"EOT";
-
-**** Warning: $name requires that the $required plugin be installed and
-              enabled; it is not currently in \@Plugins.
-
-EOT
-    }
-}
-
-1;
diff --git a/lib/App/wsgetmail.pm b/lib/App/wsgetmail.pm
index 96455c4..cd15566 100644
--- a/lib/App/wsgetmail.pm
+++ b/lib/App/wsgetmail.pm
@@ -2,12 +2,16 @@ package App::wsgetmail;
 
 use Moo;
 
-our $VERSION = '0.01';
+our $VERSION = '0.02';
 
 =head1 NAME
 
 App::wsgetmail - Fetch mail from the cloud using webservices
 
+=head1 VERSION
+
+0.02
+
 =head1 DESCRIPTION
 
 A simple command line application/script to fetch mail from the cloud

commit 24d7627595b76290d5591ed2ae0aa1e8ccf9c8b7
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Fri Jul 17 19:30:08 2020 +0100

    cleanup old test files in wsgetmail MANIFEST

diff --git a/MANIFEST b/MANIFEST
index 9f8ad86..3740938 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -1,4 +1,4 @@
-bin/wsgetmail.in
+bin/wsgetmail
 Changes
 doc/activedirectory_setup.md
 doc/example.conf
@@ -33,8 +33,4 @@ META.yml
 README
 t/00-load.t
 t/basic.t
-t/manifest.t
 t/mock_responses/messages.json
-t/pod-coverage.t
-t/pod.t
-xt/boilerplate.t

commit 926888c878153e78e4c8bcf010ab32557a1d7a88
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Fri Jul 17 19:29:31 2020 +0100

    No need for subsitutions in wsgetmail script

diff --git a/Makefile.PL b/Makefile.PL
index 71c9ef0..34c82d7 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -14,20 +14,5 @@ requires 'Moo';
 requires 'JSON';
 requires 'IPC::Run';
 
-use Config;
-my $perl_path = $Config{perlpath};
-$perl_path .= $Config{_exe}
-    if $^O ne 'VMS' and $perl_path !~ m/$Config{_exe}$/i;
-
-for my $file ( 'bin/wsgetmail' ) {
-    substitute(
-        {
-            PERL => $perl_path,
-        },
-        { sufix => ".in" },
-        $file,
-    );
-}
-
 sign;
 WriteAll;
diff --git a/bin/wsgetmail.in b/bin/wsgetmail
old mode 100644
new mode 100755
similarity index 100%
rename from bin/wsgetmail.in
rename to bin/wsgetmail

commit 90cc127256bd8b403f5eb4488dd718bc80d50ec1
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Fri Jul 17 17:34:39 2020 +0100

    updated MANIFEST for wsgetmail package

diff --git a/MANIFEST b/MANIFEST
index 44331f2..9f8ad86 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -1,5 +1,4 @@
-bin/wsgetmail365
-bin/wsgetmail365.in
+bin/wsgetmail.in
 Changes
 doc/activedirectory_setup.md
 doc/example.conf
@@ -24,7 +23,7 @@ inc/Module/Install/Win32.pm
 inc/Module/Install/WriteAll.pm
 inc/YAML/Tiny.pm
 lib/App/wsgetmail.pm
-lib/App/wsgetmail/Handler/RT/Mailgate.pm
+lib/App/wsgetmail/MDA.pm
 lib/App/wsgetmail/MS365.pm
 lib/App/wsgetmail/MS365/Client.pm
 lib/App/wsgetmail/MS365/Message.pm

commit f39ed4c3f527fbbade3c86728a3848d74bd7161f
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Fri Jul 17 17:33:10 2020 +0100

    clean up build files for wsgetmail

diff --git a/MYMETA.json b/MYMETA.json
deleted file mode 100644
index 4123c8f..0000000
--- a/MYMETA.json
+++ /dev/null
@@ -1,59 +0,0 @@
-{
-   "abstract" : "Fetch mail from the cloud using webservices",
-   "author" : [
-      "Best Practical Solutions, LLC <modules at bestpractical.com>"
-   ],
-   "dynamic_config" : 0,
-   "generated_by" : "Module::Install version 1.19, CPAN::Meta::Converter version 2.150010",
-   "license" : [
-      "gpl_2"
-   ],
-   "meta-spec" : {
-      "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec",
-      "version" : "2"
-   },
-   "name" : "App-wsgetmail",
-   "no_index" : {
-      "directory" : [
-         "inc",
-         "t",
-         "xt"
-      ]
-   },
-   "prereqs" : {
-      "build" : {
-         "requires" : {
-            "ExtUtils::MakeMaker" : "6.59"
-         }
-      },
-      "configure" : {
-         "requires" : {
-            "ExtUtils::MakeMaker" : "0"
-         }
-      },
-      "runtime" : {
-         "requires" : {
-            "Azure::AD::ClientCredentials" : "0",
-            "IPC::Run" : "0",
-            "JSON" : "0",
-            "LWP::UserAgent" : "6.42",
-            "Moo" : "0",
-            "Test::LWP::UserAgent" : "0",
-            "Test::More" : "0",
-            "URI" : "0",
-            "URI::Escape" : "0",
-            "perl" : "5.010001"
-         }
-      }
-   },
-   "release_status" : "stable",
-   "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",
-   "x_serialization_backend" : "JSON::PP version 2.27400_02"
-}
diff --git a/MYMETA.yml b/MYMETA.yml
deleted file mode 100644
index 5e984ba..0000000
--- a/MYMETA.yml
+++ /dev/null
@@ -1,37 +0,0 @@
----
-abstract: 'Fetch mail from the cloud using webservices'
-author:
-  - 'Best Practical Solutions, LLC <modules at bestpractical.com>'
-build_requires:
-  ExtUtils::MakeMaker: '6.59'
-configure_requires:
-  ExtUtils::MakeMaker: '0'
-dynamic_config: 0
-generated_by: 'Module::Install version 1.19, CPAN::Meta::Converter version 2.150010'
-license: gpl
-meta-spec:
-  url: http://module-build.sourceforge.net/META-spec-v1.4.html
-  version: '1.4'
-name: App-wsgetmail
-no_index:
-  directory:
-    - inc
-    - t
-    - xt
-requires:
-  Azure::AD::ClientCredentials: '0'
-  IPC::Run: '0'
-  JSON: '0'
-  LWP::UserAgent: '6.42'
-  Moo: '0'
-  Test::LWP::UserAgent: '0'
-  Test::More: '0'
-  URI: '0'
-  URI::Escape: '0'
-  perl: '5.010001'
-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
-x_serialization_backend: 'CPAN::Meta::YAML version 0.018'
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 9b8b1bf..0000000
--- a/Makefile
+++ /dev/null
@@ -1,898 +0,0 @@
-# This Makefile is for the App::wsgetmail extension to perl.
-#
-# It was generated automatically by MakeMaker version
-# 7.24 (Revision: 72400) from the contents of
-# Makefile.PL. Don't edit this file, edit Makefile.PL instead.
-#
-#       ANY CHANGES MADE HERE WILL BE LOST!
-#
-#   MakeMaker ARGV: ()
-#
-
-#   MakeMaker Parameters:
-
-#     ABSTRACT => q[Fetch mail from the cloud using webservices]
-#     AUTHOR => [q[Best Practical Solutions, LLC <modules at bestpractical.com>]]
-#     BUILD_REQUIRES => { ExtUtils::MakeMaker=>q[6.59] }
-#     CONFIGURE_REQUIRES => {  }
-#     DISTNAME => q[App-wsgetmail]
-#     INSTALLARCHLIB => q[/Users/craigkaiser/bps/rts/rt5/local/plugins/App-wsgetmail/lib]
-#     INSTALLSITEARCH => q[/Users/craigkaiser/bps/rts/rt5/local/man]
-#     INSTALLSITELIB => q[/Users/craigkaiser/bps/rts/rt5/local/plugins/App-wsgetmail/lib]
-#     INSTALLSITEMAN1DIR => q[/Users/craigkaiser/bps/rts/rt5/local/man/man1]
-#     INSTALLSITEMAN3DIR => q[/Users/craigkaiser/bps/rts/rt5/local/man/man3]
-#     INSTALLVENDORARCH => q[/usr/share/man]
-#     INSTALLVENDORLIB => q[/Users/craigkaiser/bps/rts/rt5/local/plugins/App-wsgetmail/lib]
-#     INSTALLVENDORMAN1DIR => q[/usr/share/man/man1]
-#     INSTALLVENDORMAN3DIR => q[/usr/share/man/man3]
-#     LICENSE => q[gpl_2]
-#     MIN_PERL_VERSION => q[5.010001]
-#     NAME => q[App::wsgetmail]
-#     NO_META => q[1]
-#     PREREQ_PM => { Azure::AD::ClientCredentials=>q[0], ExtUtils::MakeMaker=>q[6.59], IPC::Run=>q[0], JSON=>q[0], LWP::UserAgent=>q[6.42], Moo=>q[0], Test::LWP::UserAgent=>q[0], Test::More=>q[0], URI=>q[0], URI::Escape=>q[0] }
-#     SIGN => q[1]
-#     TEST_REQUIRES => {  }
-#     VERSION => q[0.01]
-#     VERSION_FROM => q[lib/App/wsgetmail.pm]
-#     dist => { PREOP=>q[$(PERL) -I. "-MModule::Install::Admin" -e "dist_preop(q($(DISTVNAME)))"] }
-#     realclean => { FILES=>q[MYMETA.yml] }
-#     test => { TESTS=>q[t/*.t xt/*.t] }
-
-# --- MakeMaker post_initialize section:
-
-
-# --- MakeMaker const_config section:
-
-# These definitions are from config.sh (via /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/lib/5.26.1/darwin-2level/Config.pm).
-# They may have been overridden via Makefile.PL or on the command line.
-AR = ar
-CC = cc
-CCCDLFLAGS =  
-CCDLFLAGS =  
-DLEXT = bundle
-DLSRC = dl_dlopen.xs
-EXE_EXT = 
-FULL_AR = /usr/bin/ar
-LD = cc
-LDDLFLAGS =  -mmacosx-version-min=10.14 -bundle -undefined dynamic_lookup -fstack-protector-strong
-LDFLAGS =  -mmacosx-version-min=10.14 -fstack-protector-strong
-LIBC = 
-LIB_EXT = .a
-OBJ_EXT = .o
-OSNAME = darwin
-OSVERS = 18.0.0
-RANLIB = ranlib
-SITELIBEXP = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/lib/site_perl/5.26.1
-SITEARCHEXP = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/lib/site_perl/5.26.1/darwin-2level
-SO = dylib
-VENDORARCHEXP = /usr/share/man
-VENDORLIBEXP = /Users/craigkaiser/bps/rts/rt5/local/plugins/App-wsgetmail/lib
-
-
-# --- MakeMaker constants section:
-AR_STATIC_ARGS = cr
-DIRFILESEP = /
-DFSEP = $(DIRFILESEP)
-NAME = App::wsgetmail
-NAME_SYM = App_wsgetmail
-VERSION = 0.01
-VERSION_MACRO = VERSION
-VERSION_SYM = 0_01
-DEFINE_VERSION = -D$(VERSION_MACRO)=\"$(VERSION)\"
-XS_VERSION = 0.01
-XS_VERSION_MACRO = XS_VERSION
-XS_DEFINE_VERSION = -D$(XS_VERSION_MACRO)=\"$(XS_VERSION)\"
-INST_ARCHLIB = blib/arch
-INST_SCRIPT = blib/script
-INST_BIN = blib/bin
-INST_LIB = blib/lib
-INST_MAN1DIR = blib/man1
-INST_MAN3DIR = blib/man3
-MAN1EXT = 1
-MAN3EXT = 3
-INSTALLDIRS = site
-DESTDIR = 
-PREFIX = $(SITEPREFIX)
-PERLPREFIX = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1
-SITEPREFIX = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1
-VENDORPREFIX = 
-INSTALLPRIVLIB = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/lib/5.26.1
-DESTINSTALLPRIVLIB = $(DESTDIR)$(INSTALLPRIVLIB)
-INSTALLSITELIB = /Users/craigkaiser/bps/rts/rt5/local/plugins/App-wsgetmail/lib
-DESTINSTALLSITELIB = $(DESTDIR)$(INSTALLSITELIB)
-INSTALLVENDORLIB = /Users/craigkaiser/bps/rts/rt5/local/plugins/App-wsgetmail/lib
-DESTINSTALLVENDORLIB = $(DESTDIR)$(INSTALLVENDORLIB)
-INSTALLARCHLIB = /Users/craigkaiser/bps/rts/rt5/local/plugins/App-wsgetmail/lib
-DESTINSTALLARCHLIB = $(DESTDIR)$(INSTALLARCHLIB)
-INSTALLSITEARCH = /Users/craigkaiser/bps/rts/rt5/local/man
-DESTINSTALLSITEARCH = $(DESTDIR)$(INSTALLSITEARCH)
-INSTALLVENDORARCH = /usr/share/man
-DESTINSTALLVENDORARCH = $(DESTDIR)$(INSTALLVENDORARCH)
-INSTALLBIN = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/bin
-DESTINSTALLBIN = $(DESTDIR)$(INSTALLBIN)
-INSTALLSITEBIN = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/bin
-DESTINSTALLSITEBIN = $(DESTDIR)$(INSTALLSITEBIN)
-INSTALLVENDORBIN = 
-DESTINSTALLVENDORBIN = $(DESTDIR)$(INSTALLVENDORBIN)
-INSTALLSCRIPT = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/bin
-DESTINSTALLSCRIPT = $(DESTDIR)$(INSTALLSCRIPT)
-INSTALLSITESCRIPT = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/bin
-DESTINSTALLSITESCRIPT = $(DESTDIR)$(INSTALLSITESCRIPT)
-INSTALLVENDORSCRIPT = 
-DESTINSTALLVENDORSCRIPT = $(DESTDIR)$(INSTALLVENDORSCRIPT)
-INSTALLMAN1DIR = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/man/man1
-DESTINSTALLMAN1DIR = $(DESTDIR)$(INSTALLMAN1DIR)
-INSTALLSITEMAN1DIR = /Users/craigkaiser/bps/rts/rt5/local/man/man1
-DESTINSTALLSITEMAN1DIR = $(DESTDIR)$(INSTALLSITEMAN1DIR)
-INSTALLVENDORMAN1DIR = /usr/share/man/man1
-DESTINSTALLVENDORMAN1DIR = $(DESTDIR)$(INSTALLVENDORMAN1DIR)
-INSTALLMAN3DIR = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/man/man3
-DESTINSTALLMAN3DIR = $(DESTDIR)$(INSTALLMAN3DIR)
-INSTALLSITEMAN3DIR = /Users/craigkaiser/bps/rts/rt5/local/man/man3
-DESTINSTALLSITEMAN3DIR = $(DESTDIR)$(INSTALLSITEMAN3DIR)
-INSTALLVENDORMAN3DIR = /usr/share/man/man3
-DESTINSTALLVENDORMAN3DIR = $(DESTDIR)$(INSTALLVENDORMAN3DIR)
-PERL_LIB =
-PERL_ARCHLIB = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/lib/5.26.1/darwin-2level
-PERL_ARCHLIBDEP = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/lib/5.26.1/darwin-2level
-LIBPERL_A = libperl.a
-FIRST_MAKEFILE = Makefile
-MAKEFILE_OLD = Makefile.old
-MAKE_APERL_FILE = Makefile.aperl
-PERLMAINCC = $(CC)
-PERL_INC = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/lib/5.26.1/darwin-2level/CORE
-PERL_INCDEP = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/lib/5.26.1/darwin-2level/CORE
-PERL = "/Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/bin/perl" "-Iinc"
-FULLPERL = "/Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/bin/perl" "-Iinc"
-ABSPERL = $(PERL)
-PERLRUN = $(PERL)
-FULLPERLRUN = $(FULLPERL)
-ABSPERLRUN = $(ABSPERL)
-PERLRUNINST = $(PERLRUN) "-I$(INST_ARCHLIB)" "-Iinc" "-I$(INST_LIB)"
-FULLPERLRUNINST = $(FULLPERLRUN) "-I$(INST_ARCHLIB)" "-Iinc" "-I$(INST_LIB)"
-ABSPERLRUNINST = $(ABSPERLRUN) "-I$(INST_ARCHLIB)" "-Iinc" "-I$(INST_LIB)"
-PERL_CORE = 0
-PERM_DIR = 755
-PERM_RW = 644
-PERM_RWX = 755
-
-MAKEMAKER   = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/lib/5.26.1/ExtUtils/MakeMaker.pm
-MM_VERSION  = 7.24
-MM_REVISION = 72400
-
-# FULLEXT = Pathname for extension directory (eg Foo/Bar/Oracle).
-# BASEEXT = Basename part of FULLEXT. May be just equal FULLEXT. (eg Oracle)
-# PARENT_NAME = NAME without BASEEXT and no trailing :: (eg Foo::Bar)
-# DLBASE  = Basename part of dynamic library. May be just equal BASEEXT.
-MAKE = make
-FULLEXT = App/wsgetmail
-BASEEXT = wsgetmail
-PARENT_NAME = App
-DLBASE = $(BASEEXT)
-VERSION_FROM = lib/App/wsgetmail.pm
-OBJECT = 
-LDFROM = $(OBJECT)
-LINKTYPE = dynamic
-BOOTDEP = 
-
-# Handy lists of source code files:
-XS_FILES = 
-C_FILES  = 
-O_FILES  = 
-H_FILES  = 
-MAN1PODS = 
-MAN3PODS = lib/App/wsgetmail.pm \
-	lib/App/wsgetmail/Handler/RT/Mailgate.pm \
-	lib/App/wsgetmail/MS365.pm \
-	lib/App/wsgetmail/MS365/Client.pm \
-	lib/App/wsgetmail/MS365/Message.pm
-
-# Where is the Config information that we are using/depend on
-CONFIGDEP = $(PERL_ARCHLIBDEP)$(DFSEP)Config.pm $(PERL_INCDEP)$(DFSEP)config.h
-
-# Where to build things
-INST_LIBDIR      = $(INST_LIB)/App
-INST_ARCHLIBDIR  = $(INST_ARCHLIB)/App
-
-INST_AUTODIR     = $(INST_LIB)/auto/$(FULLEXT)
-INST_ARCHAUTODIR = $(INST_ARCHLIB)/auto/$(FULLEXT)
-
-INST_STATIC      = 
-INST_DYNAMIC     = 
-INST_BOOT        = 
-
-# Extra linker info
-EXPORT_LIST        = 
-PERL_ARCHIVE       = 
-PERL_ARCHIVEDEP    = 
-PERL_ARCHIVE_AFTER = 
-
-
-TO_INST_PM = lib/App/wsgetmail.pm \
-	lib/App/wsgetmail/Handler/RT/Mailgate.pm \
-	lib/App/wsgetmail/MS365.pm \
-	lib/App/wsgetmail/MS365/Client.pm \
-	lib/App/wsgetmail/MS365/Message.pm
-
-
-# --- MakeMaker platform_constants section:
-MM_Unix_VERSION = 7.24
-PERL_MALLOC_DEF = -DPERL_EXTMALLOC_DEF -Dmalloc=Perl_malloc -Dfree=Perl_mfree -Drealloc=Perl_realloc -Dcalloc=Perl_calloc
-
-
-# --- MakeMaker tool_autosplit section:
-# Usage: $(AUTOSPLITFILE) FileToSplit AutoDirToSplitInto
-AUTOSPLITFILE = $(ABSPERLRUN)  -e 'use AutoSplit;  autosplit($$$$ARGV[0], $$$$ARGV[1], 0, 1, 1)' --
-
-
-
-# --- MakeMaker tool_xsubpp section:
-
-
-# --- MakeMaker tools_other section:
-SHELL = /bin/sh
-CHMOD = chmod
-CP = cp
-MV = mv
-NOOP = $(TRUE)
-NOECHO = @
-RM_F = rm -f
-RM_RF = rm -rf
-TEST_F = test -f
-TOUCH = touch
-UMASK_NULL = umask 0
-DEV_NULL = > /dev/null 2>&1
-MKPATH = $(ABSPERLRUN) -MExtUtils::Command -e 'mkpath' --
-EQUALIZE_TIMESTAMP = $(ABSPERLRUN) -MExtUtils::Command -e 'eqtime' --
-FALSE = false
-TRUE = true
-ECHO = echo
-ECHO_N = echo -n
-UNINST = 0
-VERBINST = 0
-MOD_INSTALL = $(ABSPERLRUN) -MExtUtils::Install -e 'install([ from_to => {@ARGV}, verbose => '\''$(VERBINST)'\'', uninstall_shadows => '\''$(UNINST)'\'', dir_mode => '\''$(PERM_DIR)'\'' ]);' --
-DOC_INSTALL = $(ABSPERLRUN) -MExtUtils::Command::MM -e 'perllocal_install' --
-UNINSTALL = $(ABSPERLRUN) -MExtUtils::Command::MM -e 'uninstall' --
-WARN_IF_OLD_PACKLIST = $(ABSPERLRUN) -MExtUtils::Command::MM -e 'warn_if_old_packlist' --
-MACROSTART = 
-MACROEND = 
-USEMAKEFILE = -f
-FIXIN = $(ABSPERLRUN) -MExtUtils::MY -e 'MY->fixin(shift)' --
-CP_NONEMPTY = $(ABSPERLRUN) -MExtUtils::Command::MM -e 'cp_nonempty' --
-
-
-# --- MakeMaker makemakerdflt section:
-makemakerdflt : all
-	$(NOECHO) $(NOOP)
-
-
-# --- MakeMaker dist section:
-TAR = COPY_EXTENDED_ATTRIBUTES_DISABLE=1 COPYFILE_DISABLE=1 tar
-TARFLAGS = cvf
-ZIP = zip
-ZIPFLAGS = -r
-COMPRESS = gzip --best
-SUFFIX = .gz
-SHAR = shar
-PREOP = $(PERL) -I. "-MModule::Install::Admin" -e "dist_preop(q($(DISTVNAME)))"
-POSTOP = $(NOECHO) $(NOOP)
-TO_UNIX = $(NOECHO) $(NOOP)
-CI = ci -u
-RCS_LABEL = rcs -Nv$(VERSION_SYM): -q
-DIST_CP = best
-DIST_DEFAULT = tardist
-DISTNAME = App-wsgetmail
-DISTVNAME = App-wsgetmail-0.01
-
-
-# --- MakeMaker macro section:
-
-
-# --- MakeMaker depend section:
-
-
-# --- MakeMaker cflags section:
-
-
-# --- MakeMaker const_loadlibs section:
-
-
-# --- MakeMaker const_cccmd section:
-
-
-# --- MakeMaker post_constants section:
-
-
-# --- MakeMaker pasthru section:
-
-PASTHRU = LIBPERL_A="$(LIBPERL_A)"\
-	LINKTYPE="$(LINKTYPE)"\
-	PREFIX="$(PREFIX)"\
-	PASTHRU_DEFINE='$(DEFINE) $(PASTHRU_DEFINE)'\
-	PASTHRU_INC='$(INC) $(PASTHRU_INC)'
-
-
-# --- MakeMaker special_targets section:
-.SUFFIXES : .xs .c .C .cpp .i .s .cxx .cc $(OBJ_EXT)
-
-.PHONY: all config static dynamic test linkext manifest blibdirs clean realclean disttest distdir pure_all subdirs clean_subdirs makemakerdflt manifypods realclean_subdirs subdirs_dynamic subdirs_pure_nolink subdirs_static subdirs-test_dynamic subdirs-test_static test_dynamic test_static
-
-
-
-# --- MakeMaker c_o section:
-
-
-# --- MakeMaker xs_c section:
-
-
-# --- MakeMaker xs_o section:
-
-
-# --- MakeMaker top_targets section:
-all :: pure_all manifypods
-	$(NOECHO) $(NOOP)
-
-pure_all :: config pm_to_blib subdirs linkext
-	$(NOECHO) $(NOOP)
-
-	$(NOECHO) $(NOOP)
-
-subdirs :: $(MYEXTLIB)
-	$(NOECHO) $(NOOP)
-
-config :: $(FIRST_MAKEFILE) blibdirs
-	$(NOECHO) $(NOOP)
-
-help :
-	perldoc ExtUtils::MakeMaker
-
-
-# --- MakeMaker blibdirs section:
-blibdirs : $(INST_LIBDIR)$(DFSEP).exists $(INST_ARCHLIB)$(DFSEP).exists $(INST_AUTODIR)$(DFSEP).exists $(INST_ARCHAUTODIR)$(DFSEP).exists $(INST_BIN)$(DFSEP).exists $(INST_SCRIPT)$(DFSEP).exists $(INST_MAN1DIR)$(DFSEP).exists $(INST_MAN3DIR)$(DFSEP).exists
-	$(NOECHO) $(NOOP)
-
-# Backwards compat with 6.18 through 6.25
-blibdirs.ts : blibdirs
-	$(NOECHO) $(NOOP)
-
-$(INST_LIBDIR)$(DFSEP).exists :: Makefile.PL
-	$(NOECHO) $(MKPATH) $(INST_LIBDIR)
-	$(NOECHO) $(CHMOD) $(PERM_DIR) $(INST_LIBDIR)
-	$(NOECHO) $(TOUCH) $(INST_LIBDIR)$(DFSEP).exists
-
-$(INST_ARCHLIB)$(DFSEP).exists :: Makefile.PL
-	$(NOECHO) $(MKPATH) $(INST_ARCHLIB)
-	$(NOECHO) $(CHMOD) $(PERM_DIR) $(INST_ARCHLIB)
-	$(NOECHO) $(TOUCH) $(INST_ARCHLIB)$(DFSEP).exists
-
-$(INST_AUTODIR)$(DFSEP).exists :: Makefile.PL
-	$(NOECHO) $(MKPATH) $(INST_AUTODIR)
-	$(NOECHO) $(CHMOD) $(PERM_DIR) $(INST_AUTODIR)
-	$(NOECHO) $(TOUCH) $(INST_AUTODIR)$(DFSEP).exists
-
-$(INST_ARCHAUTODIR)$(DFSEP).exists :: Makefile.PL
-	$(NOECHO) $(MKPATH) $(INST_ARCHAUTODIR)
-	$(NOECHO) $(CHMOD) $(PERM_DIR) $(INST_ARCHAUTODIR)
-	$(NOECHO) $(TOUCH) $(INST_ARCHAUTODIR)$(DFSEP).exists
-
-$(INST_BIN)$(DFSEP).exists :: Makefile.PL
-	$(NOECHO) $(MKPATH) $(INST_BIN)
-	$(NOECHO) $(CHMOD) $(PERM_DIR) $(INST_BIN)
-	$(NOECHO) $(TOUCH) $(INST_BIN)$(DFSEP).exists
-
-$(INST_SCRIPT)$(DFSEP).exists :: Makefile.PL
-	$(NOECHO) $(MKPATH) $(INST_SCRIPT)
-	$(NOECHO) $(CHMOD) $(PERM_DIR) $(INST_SCRIPT)
-	$(NOECHO) $(TOUCH) $(INST_SCRIPT)$(DFSEP).exists
-
-$(INST_MAN1DIR)$(DFSEP).exists :: Makefile.PL
-	$(NOECHO) $(MKPATH) $(INST_MAN1DIR)
-	$(NOECHO) $(CHMOD) $(PERM_DIR) $(INST_MAN1DIR)
-	$(NOECHO) $(TOUCH) $(INST_MAN1DIR)$(DFSEP).exists
-
-$(INST_MAN3DIR)$(DFSEP).exists :: Makefile.PL
-	$(NOECHO) $(MKPATH) $(INST_MAN3DIR)
-	$(NOECHO) $(CHMOD) $(PERM_DIR) $(INST_MAN3DIR)
-	$(NOECHO) $(TOUCH) $(INST_MAN3DIR)$(DFSEP).exists
-
-
-
-# --- MakeMaker linkext section:
-
-linkext :: dynamic
-	$(NOECHO) $(NOOP)
-
-
-# --- MakeMaker dlsyms section:
-
-
-# --- MakeMaker dynamic_bs section:
-
-BOOTSTRAP =
-
-
-# --- MakeMaker dynamic section:
-
-dynamic :: $(FIRST_MAKEFILE) config $(INST_BOOT) $(INST_DYNAMIC)
-	$(NOECHO) $(NOOP)
-
-
-# --- MakeMaker dynamic_lib section:
-
-
-# --- MakeMaker static section:
-
-## $(INST_PM) has been moved to the all: target.
-## It remains here for awhile to allow for old usage: "make static"
-static :: $(FIRST_MAKEFILE) $(INST_STATIC)
-	$(NOECHO) $(NOOP)
-
-
-# --- MakeMaker static_lib section:
-
-
-# --- MakeMaker manifypods section:
-
-POD2MAN_EXE = $(PERLRUN) "-MExtUtils::Command::MM" -e pod2man "--"
-POD2MAN = $(POD2MAN_EXE)
-
-
-manifypods : pure_all config  \
-	lib/App/wsgetmail.pm \
-	lib/App/wsgetmail/Handler/RT/Mailgate.pm \
-	lib/App/wsgetmail/MS365.pm \
-	lib/App/wsgetmail/MS365/Client.pm \
-	lib/App/wsgetmail/MS365/Message.pm
-	$(NOECHO) $(POD2MAN) --section=3 --perm_rw=$(PERM_RW) -u \
-	  lib/App/wsgetmail.pm $(INST_MAN3DIR)/App::wsgetmail.$(MAN3EXT) \
-	  lib/App/wsgetmail/Handler/RT/Mailgate.pm $(INST_MAN3DIR)/App::wsgetmail::Handler::RT::Mailgate.$(MAN3EXT) \
-	  lib/App/wsgetmail/MS365.pm $(INST_MAN3DIR)/App::wsgetmail::MS365.$(MAN3EXT) \
-	  lib/App/wsgetmail/MS365/Client.pm $(INST_MAN3DIR)/App::wsgetmail::MS365::Client.$(MAN3EXT) \
-	  lib/App/wsgetmail/MS365/Message.pm $(INST_MAN3DIR)/App::wsgetmail::MS365::Message.$(MAN3EXT) 
-
-
-
-
-# --- MakeMaker processPL section:
-
-
-# --- MakeMaker installbin section:
-
-
-# --- MakeMaker subdirs section:
-
-# none
-
-# --- MakeMaker clean_subdirs section:
-clean_subdirs :
-	$(NOECHO) $(NOOP)
-
-
-# --- MakeMaker clean section:
-
-# Delete temporary files but do not touch installed files. We don't delete
-# the Makefile here so a later make realclean still has a makefile to use.
-
-clean :: clean_subdirs
-	- $(RM_F) \
-	  $(BASEEXT).bso $(BASEEXT).def \
-	  $(BASEEXT).exp $(BASEEXT).x \
-	  $(BOOTSTRAP) $(INST_ARCHAUTODIR)/extralibs.all \
-	  $(INST_ARCHAUTODIR)/extralibs.ld $(MAKE_APERL_FILE) \
-	  *$(LIB_EXT) *$(OBJ_EXT) \
-	  *perl.core MYMETA.json \
-	  MYMETA.yml blibdirs.ts \
-	  core core.*perl.*.? \
-	  core.[0-9] core.[0-9][0-9] \
-	  core.[0-9][0-9][0-9] core.[0-9][0-9][0-9][0-9] \
-	  core.[0-9][0-9][0-9][0-9][0-9] lib$(BASEEXT).def \
-	  mon.out perl \
-	  perl$(EXE_EXT) perl.exe \
-	  perlmain.c pm_to_blib \
-	  pm_to_blib.ts so_locations \
-	  tmon.out 
-	- $(RM_RF) \
-	  blib 
-	  $(NOECHO) $(RM_F) $(MAKEFILE_OLD)
-	- $(MV) $(FIRST_MAKEFILE) $(MAKEFILE_OLD) $(DEV_NULL)
-
-
-# --- MakeMaker realclean_subdirs section:
-# so clean is forced to complete before realclean_subdirs runs
-realclean_subdirs : clean
-	$(NOECHO) $(NOOP)
-
-
-# --- MakeMaker realclean section:
-# Delete temporary files (via clean) and also delete dist files
-realclean purge :: realclean_subdirs
-	- $(RM_F) \
-	  $(FIRST_MAKEFILE) $(MAKEFILE_OLD) 
-	- $(RM_RF) \
-	  $(DISTVNAME) MYMETA.yml 
-
-
-# --- MakeMaker metafile section:
-metafile :
-	$(NOECHO) $(NOOP)
-
-
-# --- MakeMaker signature section:
-signature :
-	cpansign -s
-
-
-# --- MakeMaker dist_basics section:
-distclean :: realclean distcheck
-	$(NOECHO) $(NOOP)
-
-distcheck :
-	$(PERLRUN) "-MExtUtils::Manifest=fullcheck" -e fullcheck
-
-skipcheck :
-	$(PERLRUN) "-MExtUtils::Manifest=skipcheck" -e skipcheck
-
-manifest :
-	$(PERLRUN) "-MExtUtils::Manifest=mkmanifest" -e mkmanifest
-
-veryclean : realclean
-	$(RM_F) *~ */*~ *.orig */*.orig *.bak */*.bak *.old */*.old
-
-
-
-# --- MakeMaker dist_core section:
-
-dist : $(DIST_DEFAULT) $(FIRST_MAKEFILE)
-	$(NOECHO) $(ABSPERLRUN) -l -e 'print '\''Warning: Makefile possibly out of date with $(VERSION_FROM)'\''' \
-	  -e '    if -e '\''$(VERSION_FROM)'\'' and -M '\''$(VERSION_FROM)'\'' < -M '\''$(FIRST_MAKEFILE)'\'';' --
-
-tardist : $(DISTVNAME).tar$(SUFFIX)
-	$(NOECHO) $(NOOP)
-
-uutardist : $(DISTVNAME).tar$(SUFFIX)
-	uuencode $(DISTVNAME).tar$(SUFFIX) $(DISTVNAME).tar$(SUFFIX) > $(DISTVNAME).tar$(SUFFIX)_uu
-	$(NOECHO) $(ECHO) 'Created $(DISTVNAME).tar$(SUFFIX)_uu'
-
-$(DISTVNAME).tar$(SUFFIX) : distdir
-	$(PREOP)
-	$(TO_UNIX)
-	$(TAR) $(TARFLAGS) $(DISTVNAME).tar $(DISTVNAME)
-	$(RM_RF) $(DISTVNAME)
-	$(COMPRESS) $(DISTVNAME).tar
-	$(NOECHO) $(ECHO) 'Created $(DISTVNAME).tar$(SUFFIX)'
-	$(POSTOP)
-
-zipdist : $(DISTVNAME).zip
-	$(NOECHO) $(NOOP)
-
-$(DISTVNAME).zip : distdir
-	$(PREOP)
-	$(ZIP) $(ZIPFLAGS) $(DISTVNAME).zip $(DISTVNAME)
-	$(RM_RF) $(DISTVNAME)
-	$(NOECHO) $(ECHO) 'Created $(DISTVNAME).zip'
-	$(POSTOP)
-
-shdist : distdir
-	$(PREOP)
-	$(SHAR) $(DISTVNAME) > $(DISTVNAME).shar
-	$(RM_RF) $(DISTVNAME)
-	$(NOECHO) $(ECHO) 'Created $(DISTVNAME).shar'
-	$(POSTOP)
-
-
-# --- MakeMaker distdir section:
-create_distdir :
-	$(RM_RF) $(DISTVNAME)
-	$(PERLRUN) "-MExtUtils::Manifest=manicopy,maniread" \
-		-e "manicopy(maniread(),'$(DISTVNAME)', '$(DIST_CP)');"
-
-distdir : create_distdir  distsignature
-	$(NOECHO) $(NOOP)
-
-
-
-# --- MakeMaker dist_test section:
-disttest : distdir
-	cd $(DISTVNAME) && $(ABSPERLRUN) Makefile.PL 
-	cd $(DISTVNAME) && $(MAKE) $(PASTHRU)
-	cd $(DISTVNAME) && $(MAKE) test $(PASTHRU)
-
-
-
-# --- MakeMaker dist_ci section:
-ci :
-	$(ABSPERLRUN) -MExtUtils::Manifest=maniread -e '@all = sort keys %{ maniread() };' \
-	  -e 'print(qq{Executing $(CI) @all\n});' \
-	  -e 'system(qq{$(CI) @all}) == 0 or die $$!;' \
-	  -e 'print(qq{Executing $(RCS_LABEL) ...\n});' \
-	  -e 'system(qq{$(RCS_LABEL) @all}) == 0 or die $$!;' --
-
-
-# --- MakeMaker distmeta section:
-distmeta : create_distdir metafile
-	$(NOECHO) cd $(DISTVNAME) && $(ABSPERLRUN) -MExtUtils::Manifest=maniadd -e 'exit unless -e q{META.yml};' \
-	  -e 'eval { maniadd({q{META.yml} => q{Module YAML meta-data (added by MakeMaker)}}) }' \
-	  -e '    or die "Could not add META.yml to MANIFEST: $${'\''@'\''}"' --
-	$(NOECHO) cd $(DISTVNAME) && $(ABSPERLRUN) -MExtUtils::Manifest=maniadd -e 'exit unless -f q{META.json};' \
-	  -e 'eval { maniadd({q{META.json} => q{Module JSON meta-data (added by MakeMaker)}}) }' \
-	  -e '    or die "Could not add META.json to MANIFEST: $${'\''@'\''}"' --
-
-
-
-# --- MakeMaker distsignature section:
-distsignature : distmeta
-	$(NOECHO) cd $(DISTVNAME) && $(ABSPERLRUN) -MExtUtils::Manifest=maniadd -e 'eval { maniadd({q{SIGNATURE} => q{Public-key signature (added by MakeMaker)}}) }' \
-	  -e '    or die "Could not add SIGNATURE to MANIFEST: $${'\''@'\''}"' --
-	$(NOECHO) cd $(DISTVNAME) && $(TOUCH) SIGNATURE
-	cd $(DISTVNAME) && cpansign -s
-
-
-
-# --- MakeMaker install section:
-
-install :: pure_install doc_install
-	$(NOECHO) $(NOOP)
-
-install_perl :: pure_perl_install doc_perl_install
-	$(NOECHO) $(NOOP)
-
-install_site :: pure_site_install doc_site_install
-	$(NOECHO) $(NOOP)
-
-install_vendor :: pure_vendor_install doc_vendor_install
-	$(NOECHO) $(NOOP)
-
-pure_install :: pure_$(INSTALLDIRS)_install
-	$(NOECHO) $(NOOP)
-
-doc_install :: doc_$(INSTALLDIRS)_install
-	$(NOECHO) $(NOOP)
-
-pure__install : pure_site_install
-	$(NOECHO) $(ECHO) INSTALLDIRS not defined, defaulting to INSTALLDIRS=site
-
-doc__install : doc_site_install
-	$(NOECHO) $(ECHO) INSTALLDIRS not defined, defaulting to INSTALLDIRS=site
-
-pure_perl_install :: all
-	$(NOECHO) $(MOD_INSTALL) \
-		read "$(PERL_ARCHLIB)/auto/$(FULLEXT)/.packlist" \
-		write "$(DESTINSTALLARCHLIB)/auto/$(FULLEXT)/.packlist" \
-		"$(INST_LIB)" "$(DESTINSTALLPRIVLIB)" \
-		"$(INST_ARCHLIB)" "$(DESTINSTALLARCHLIB)" \
-		"$(INST_BIN)" "$(DESTINSTALLBIN)" \
-		"$(INST_SCRIPT)" "$(DESTINSTALLSCRIPT)" \
-		"$(INST_MAN1DIR)" "$(DESTINSTALLMAN1DIR)" \
-		"$(INST_MAN3DIR)" "$(DESTINSTALLMAN3DIR)"
-	$(NOECHO) $(WARN_IF_OLD_PACKLIST) \
-		"$(SITEARCHEXP)/auto/$(FULLEXT)"
-
-
-pure_site_install :: all
-	$(NOECHO) $(MOD_INSTALL) \
-		read "$(SITEARCHEXP)/auto/$(FULLEXT)/.packlist" \
-		write "$(DESTINSTALLSITEARCH)/auto/$(FULLEXT)/.packlist" \
-		"$(INST_LIB)" "$(DESTINSTALLSITELIB)" \
-		"$(INST_ARCHLIB)" "$(DESTINSTALLSITEARCH)" \
-		"$(INST_BIN)" "$(DESTINSTALLSITEBIN)" \
-		"$(INST_SCRIPT)" "$(DESTINSTALLSITESCRIPT)" \
-		"$(INST_MAN1DIR)" "$(DESTINSTALLSITEMAN1DIR)" \
-		"$(INST_MAN3DIR)" "$(DESTINSTALLSITEMAN3DIR)"
-	$(NOECHO) $(WARN_IF_OLD_PACKLIST) \
-		"$(PERL_ARCHLIB)/auto/$(FULLEXT)"
-
-pure_vendor_install :: all
-	$(NOECHO) $(MOD_INSTALL) \
-		read "$(VENDORARCHEXP)/auto/$(FULLEXT)/.packlist" \
-		write "$(DESTINSTALLVENDORARCH)/auto/$(FULLEXT)/.packlist" \
-		"$(INST_LIB)" "$(DESTINSTALLVENDORLIB)" \
-		"$(INST_ARCHLIB)" "$(DESTINSTALLVENDORARCH)" \
-		"$(INST_BIN)" "$(DESTINSTALLVENDORBIN)" \
-		"$(INST_SCRIPT)" "$(DESTINSTALLVENDORSCRIPT)" \
-		"$(INST_MAN1DIR)" "$(DESTINSTALLVENDORMAN1DIR)" \
-		"$(INST_MAN3DIR)" "$(DESTINSTALLVENDORMAN3DIR)"
-
-
-doc_perl_install :: all
-	$(NOECHO) $(ECHO) Appending installation info to "$(DESTINSTALLARCHLIB)/perllocal.pod"
-	-$(NOECHO) $(MKPATH) "$(DESTINSTALLARCHLIB)"
-	-$(NOECHO) $(DOC_INSTALL) \
-		"Module" "$(NAME)" \
-		"installed into" "$(INSTALLPRIVLIB)" \
-		LINKTYPE "$(LINKTYPE)" \
-		VERSION "$(VERSION)" \
-		EXE_FILES "$(EXE_FILES)" \
-		>> "$(DESTINSTALLARCHLIB)/perllocal.pod"
-
-doc_site_install :: all
-	$(NOECHO) $(ECHO) Appending installation info to "$(DESTINSTALLARCHLIB)/perllocal.pod"
-	-$(NOECHO) $(MKPATH) "$(DESTINSTALLARCHLIB)"
-	-$(NOECHO) $(DOC_INSTALL) \
-		"Module" "$(NAME)" \
-		"installed into" "$(INSTALLSITELIB)" \
-		LINKTYPE "$(LINKTYPE)" \
-		VERSION "$(VERSION)" \
-		EXE_FILES "$(EXE_FILES)" \
-		>> "$(DESTINSTALLARCHLIB)/perllocal.pod"
-
-doc_vendor_install :: all
-	$(NOECHO) $(ECHO) Appending installation info to "$(DESTINSTALLARCHLIB)/perllocal.pod"
-	-$(NOECHO) $(MKPATH) "$(DESTINSTALLARCHLIB)"
-	-$(NOECHO) $(DOC_INSTALL) \
-		"Module" "$(NAME)" \
-		"installed into" "$(INSTALLVENDORLIB)" \
-		LINKTYPE "$(LINKTYPE)" \
-		VERSION "$(VERSION)" \
-		EXE_FILES "$(EXE_FILES)" \
-		>> "$(DESTINSTALLARCHLIB)/perllocal.pod"
-
-
-uninstall :: uninstall_from_$(INSTALLDIRS)dirs
-	$(NOECHO) $(NOOP)
-
-uninstall_from_perldirs ::
-	$(NOECHO) $(UNINSTALL) "$(PERL_ARCHLIB)/auto/$(FULLEXT)/.packlist"
-
-uninstall_from_sitedirs ::
-	$(NOECHO) $(UNINSTALL) "$(SITEARCHEXP)/auto/$(FULLEXT)/.packlist"
-
-uninstall_from_vendordirs ::
-	$(NOECHO) $(UNINSTALL) "$(VENDORARCHEXP)/auto/$(FULLEXT)/.packlist"
-
-
-# --- MakeMaker force section:
-# Phony target to force checking subdirectories.
-FORCE :
-	$(NOECHO) $(NOOP)
-
-
-# --- MakeMaker perldepend section:
-
-
-# --- MakeMaker makefile section:
-# We take a very conservative approach here, but it's worth it.
-# We move Makefile to Makefile.old here to avoid gnu make looping.
-$(FIRST_MAKEFILE) : Makefile.PL $(CONFIGDEP)
-	$(NOECHO) $(ECHO) "Makefile out-of-date with respect to $?"
-	$(NOECHO) $(ECHO) "Cleaning current config before rebuilding Makefile..."
-	-$(NOECHO) $(RM_F) $(MAKEFILE_OLD)
-	-$(NOECHO) $(MV)   $(FIRST_MAKEFILE) $(MAKEFILE_OLD)
-	- $(MAKE) $(USEMAKEFILE) $(MAKEFILE_OLD) clean $(DEV_NULL)
-	$(PERLRUN) Makefile.PL 
-	$(NOECHO) $(ECHO) "==> Your Makefile has been rebuilt. <=="
-	$(NOECHO) $(ECHO) "==> Please rerun the $(MAKE) command.  <=="
-	$(FALSE)
-
-
-
-# --- MakeMaker staticmake section:
-
-# --- MakeMaker makeaperl section ---
-MAP_TARGET    = perl
-FULLPERL      = "/Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/bin/perl"
-MAP_PERLINC   = "-Iblib/arch" "-Iblib/lib" "-I/Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/lib/5.26.1/darwin-2level" "-I/Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/lib/5.26.1"
-
-$(MAP_TARGET) :: $(MAKE_APERL_FILE)
-	$(MAKE) $(USEMAKEFILE) $(MAKE_APERL_FILE) $@
-
-$(MAKE_APERL_FILE) : static $(FIRST_MAKEFILE) pm_to_blib
-	$(NOECHO) $(ECHO) Writing \"$(MAKE_APERL_FILE)\" for this $(MAP_TARGET)
-	$(NOECHO) $(PERLRUNINST) \
-		Makefile.PL DIR="" \
-		MAKEFILE=$(MAKE_APERL_FILE) LINKTYPE=static \
-		MAKEAPERL=1 NORECURS=1 CCCDLFLAGS=
-
-
-# --- MakeMaker test section:
-TEST_VERBOSE=0
-TEST_TYPE=test_$(LINKTYPE)
-TEST_FILE = test.pl
-TEST_FILES = t/*.t xt/*.t
-TESTDB_SW = -d
-
-testdb :: testdb_$(LINKTYPE)
-	$(NOECHO) $(NOOP)
-
-test :: $(TEST_TYPE)
-	$(NOECHO) $(NOOP)
-
-# Occasionally we may face this degenerate target:
-test_ : test_dynamic
-	$(NOECHO) $(NOOP)
-
-subdirs-test_dynamic :: dynamic pure_all
-
-test_dynamic :: subdirs-test_dynamic
-	PERL_DL_NONLAZY=1 $(FULLPERLRUN) "-MExtUtils::Command::MM" "-MTest::Harness" "-e" "undef *Test::Harness::Switches; test_harness($(TEST_VERBOSE), 'inc', '$(INST_LIB)', '$(INST_ARCHLIB)')" $(TEST_FILES)
-
-testdb_dynamic :: dynamic pure_all
-	PERL_DL_NONLAZY=1 $(FULLPERLRUN) $(TESTDB_SW) "-Iinc" "-I$(INST_LIB)" "-I$(INST_ARCHLIB)" $(TEST_FILE)
-
-subdirs-test_static :: static pure_all
-
-test_static :: subdirs-test_static
-	PERL_DL_NONLAZY=1 $(FULLPERLRUN) "-MExtUtils::Command::MM" "-MTest::Harness" "-e" "undef *Test::Harness::Switches; test_harness($(TEST_VERBOSE), '$(INST_LIB)', '$(INST_ARCHLIB)')" $(TEST_FILES)
-
-testdb_static :: static pure_all
-	PERL_DL_NONLAZY=1 $(FULLPERLRUN) $(TESTDB_SW) "-Iinc" "-I$(INST_LIB)" "-I$(INST_ARCHLIB)" $(TEST_FILE)
-
-
-
-# --- MakeMaker ppd section:
-# Creates a PPD (Perl Package Description) for a binary distribution.
-ppd :
-	$(NOECHO) $(ECHO) '<SOFTPKG NAME="App-wsgetmail" VERSION="0.01">' > App-wsgetmail.ppd
-	$(NOECHO) $(ECHO) '    <ABSTRACT>Fetch mail from the cloud using webservices</ABSTRACT>' >> App-wsgetmail.ppd
-	$(NOECHO) $(ECHO) '    <AUTHOR>Best Practical Solutions, LLC <modules at bestpractical.com></AUTHOR>' >> App-wsgetmail.ppd
-	$(NOECHO) $(ECHO) '    <IMPLEMENTATION>' >> App-wsgetmail.ppd
-	$(NOECHO) $(ECHO) '        <PERLCORE VERSION="5,010001,0,0" />' >> App-wsgetmail.ppd
-	$(NOECHO) $(ECHO) '        <REQUIRE NAME="Azure::AD::ClientCredentials" />' >> App-wsgetmail.ppd
-	$(NOECHO) $(ECHO) '        <REQUIRE NAME="IPC::Run" />' >> App-wsgetmail.ppd
-	$(NOECHO) $(ECHO) '        <REQUIRE NAME="JSON::" />' >> App-wsgetmail.ppd
-	$(NOECHO) $(ECHO) '        <REQUIRE NAME="LWP::UserAgent" VERSION="6.42" />' >> App-wsgetmail.ppd
-	$(NOECHO) $(ECHO) '        <REQUIRE NAME="Moo::" />' >> App-wsgetmail.ppd
-	$(NOECHO) $(ECHO) '        <REQUIRE NAME="Test::LWP::UserAgent" />' >> App-wsgetmail.ppd
-	$(NOECHO) $(ECHO) '        <REQUIRE NAME="Test::More" />' >> App-wsgetmail.ppd
-	$(NOECHO) $(ECHO) '        <REQUIRE NAME="URI::" />' >> App-wsgetmail.ppd
-	$(NOECHO) $(ECHO) '        <REQUIRE NAME="URI::Escape" />' >> App-wsgetmail.ppd
-	$(NOECHO) $(ECHO) '        <ARCHITECTURE NAME="darwin-2level-5.26" />' >> App-wsgetmail.ppd
-	$(NOECHO) $(ECHO) '        <CODEBASE HREF="" />' >> App-wsgetmail.ppd
-	$(NOECHO) $(ECHO) '    </IMPLEMENTATION>' >> App-wsgetmail.ppd
-	$(NOECHO) $(ECHO) '</SOFTPKG>' >> App-wsgetmail.ppd
-
-
-# --- MakeMaker pm_to_blib section:
-
-pm_to_blib : $(FIRST_MAKEFILE) $(TO_INST_PM)
-	$(NOECHO) $(ABSPERLRUN) -MExtUtils::Install -e 'pm_to_blib({@ARGV}, '\''$(INST_LIB)/auto'\'', q[$(PM_FILTER)], '\''$(PERM_DIR)'\'')' -- \
-	  'lib/App/wsgetmail.pm' 'blib/lib/App/wsgetmail.pm' \
-	  'lib/App/wsgetmail/Handler/RT/Mailgate.pm' 'blib/lib/App/wsgetmail/Handler/RT/Mailgate.pm' \
-	  'lib/App/wsgetmail/MS365.pm' 'blib/lib/App/wsgetmail/MS365.pm' \
-	  'lib/App/wsgetmail/MS365/Client.pm' 'blib/lib/App/wsgetmail/MS365/Client.pm' \
-	  'lib/App/wsgetmail/MS365/Message.pm' 'blib/lib/App/wsgetmail/MS365/Message.pm' 
-	$(NOECHO) $(TOUCH) pm_to_blib
-
-
-# --- MakeMaker selfdocument section:
-
-# here so even if top_targets is overridden, these will still be defined
-# gmake will silently still work if any are .PHONY-ed but nmake won't
-
-static ::
-	$(NOECHO) $(NOOP)
-
-dynamic ::
-	$(NOECHO) $(NOOP)
-
-config ::
-	$(NOECHO) $(NOOP)
-
-
-# --- MakeMaker postamble section:
-
-
-# End.
-# Postamble by Module::Install 1.19
-# --- Module::Install::Admin::Makefile section:
-
-realclean purge ::
-	$(RM_F) $(DISTVNAME).tar$(SUFFIX)
-	$(RM_F) MANIFEST.bak _build
-	$(PERL) "-Ilib" "-MModule::Install::Admin" -e "remove_meta()"
-	$(RM_RF) inc
-
-reset :: purge
-
-upload :: test dist
-	cpan-upload -verbose $(DISTVNAME).tar$(SUFFIX)
-
-grok ::
-	perldoc Module::Install
-
-distsign ::
-	cpansign -s
-
-install ::
-	$(NOECHO) $(PERL) -Ilib -I"/Users/craigkaiser/bps/rts/rt5/local/lib" -I"/Users/craigkaiser/bps/rts/rt5/lib" -Iinc -MModule::Install::RTx::Runtime -e"RTxPlugin()"
-	$(NOECHO) $(PERL) -MExtUtils::Install -e "install({q(bin), q($(DESTDIR)/Users/craigkaiser/bps/rts/rt5/local/plugins/App-wsgetmail/bin), q(lib), q($(DESTDIR)/Users/craigkaiser/bps/rts/rt5/local/plugins/App-wsgetmail/lib)})"
-

commit 47d6dd0341ed1aee3a4a9e323a7936d03d2587b0
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Fri Jul 17 17:32:11 2020 +0100

    fixed and cleaned up tests for wsgetmail

diff --git a/t/00-load.t b/t/00-load.t
index ee3a0df..08b51ff 100644
--- a/t/00-load.t
+++ b/t/00-load.t
@@ -1,12 +1,13 @@
 #!perl 
 use strict;
 use Test::More;
-
-use lib qw(lib);
+use FindBin;
+use lib "$FindBin::Bin/../lib";
 
 use_ok( 'App::wsgetmail' );
 use_ok( 'App::wsgetmail::MS365' );
 use_ok( 'App::wsgetmail::MS365::Client' );
-use_ok( 'App::wsgetmail::Handler::RT::Mailgate' );
+use_ok( 'App::wsgetmail::MS365::Message');
+use_ok( 'App::wsgetmail::MDA' );
 
 done_testing();
diff --git a/t/manifest.t b/t/manifest.t
deleted file mode 100644
index e0b558e..0000000
--- a/t/manifest.t
+++ /dev/null
@@ -1,15 +0,0 @@
-#!perl -T
-use 5.006;
-use strict;
-use warnings;
-use Test::More;
-
-unless ( $ENV{RELEASE_TESTING} ) {
-    plan( skip_all => "Author tests not required for installation" );
-}
-
-my $min_tcm = 0.9;
-eval "use Test::CheckManifest $min_tcm";
-plan skip_all => "Test::CheckManifest $min_tcm required" if $@;
-
-ok_manifest();
diff --git a/t/pod-coverage.t b/t/pod-coverage.t
deleted file mode 100644
index f5728a5..0000000
--- a/t/pod-coverage.t
+++ /dev/null
@@ -1,24 +0,0 @@
-#!perl -T
-use 5.006;
-use strict;
-use warnings;
-use Test::More;
-
-unless ( $ENV{RELEASE_TESTING} ) {
-    plan( skip_all => "Author tests not required for installation" );
-}
-
-# Ensure a recent version of Test::Pod::Coverage
-my $min_tpc = 1.08;
-eval "use Test::Pod::Coverage $min_tpc";
-plan skip_all => "Test::Pod::Coverage $min_tpc required for testing POD coverage"
-    if $@;
-
-# Test::Pod::Coverage doesn't require a minimum Pod::Coverage version,
-# but older versions don't recognize some common documentation styles
-my $min_pc = 0.18;
-eval "use Pod::Coverage $min_pc";
-plan skip_all => "Pod::Coverage $min_pc required for testing POD coverage"
-    if $@;
-
-all_pod_coverage_ok();
diff --git a/t/pod.t b/t/pod.t
deleted file mode 100644
index 4d3a0ce..0000000
--- a/t/pod.t
+++ /dev/null
@@ -1,16 +0,0 @@
-#!perl -T
-use 5.006;
-use strict;
-use warnings;
-use Test::More;
-
-unless ( $ENV{RELEASE_TESTING} ) {
-    plan( skip_all => "Author tests not required for installation" );
-}
-
-# Ensure a recent version of Test::Pod
-my $min_tp = 1.22;
-eval "use Test::Pod $min_tp";
-plan skip_all => "Test::Pod $min_tp required for testing POD" if $@;
-
-all_pod_files_ok();

commit dde278ba6ba5d4b8c9d3f2c86116fb7641ee28b3
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Fri Jul 17 17:27:29 2020 +0100

    Cleanup or dump mime email files in wsgetmail according to configuration

diff --git a/lib/App/wsgetmail.pm b/lib/App/wsgetmail.pm
index 5b36d79..96455c4 100644
--- a/lib/App/wsgetmail.pm
+++ b/lib/App/wsgetmail.pm
@@ -113,15 +113,21 @@ sub _build__post_fetch_action {
 sub process_message {
     my ($self, $message) = @_;
     my $client = $self->client;
-    my $file = $client->get_message_mime_content($message->id);
-    unless ($file) {
+    my $filename = $client->get_message_mime_content($message->id);
+    unless ($filename) {
         warn "failed to get mime content for message ". $message->id;
         return 0;
     }
-    my $ok = $self->forward($message, $file);
+    my $ok = $self->forward($message, $filename);
     if ($ok) {
         $ok = $self->post_fetch_action($message);
     }
+    if ($self->config->{dump_messages}) {
+        warn "dumped message in file $filename" if ($self->config->{debug});
+    }
+    else {
+        unlink $filename or warn "couldn't delete message file $filename : $!";
+    }
     return $ok;
 }
 

commit 9145e59c10f8c90dafb2c6b0b240c17367a893c7
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Fri Jul 17 17:27:00 2020 +0100

    Allow empty command to be a successful noop in wsgetmail tool

diff --git a/lib/App/wsgetmail/MDA.pm b/lib/App/wsgetmail/MDA.pm
index a913689..78964c1 100644
--- a/lib/App/wsgetmail/MDA.pm
+++ b/lib/App/wsgetmail/MDA.pm
@@ -70,6 +70,10 @@ sub _run_command {
     my ($self, $filename) = @_;
     open my $fh, "<$filename"  or die $!;
     my ($input, $output, $error);
+    unless ($self->command) {
+        warn "no action to delivery message, command option is empty or null" if ($self->debug);
+        return 1;
+    }
     my $ok = run ([ $self->command, _split_command_args($self->command_args, 1)], $fh, \$output, \$error, timeout( $self->command_timeout + 5 ) );
     close $fh;
 

commit cbff1ab1fcba54d6cc327085d33dce258f368198
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Fri Jul 17 17:16:24 2020 +0100

    use FindBin for include path in wsgetmail test

diff --git a/t/basic.t b/t/basic.t
index edc9bf4..3fa9fbd 100644
--- a/t/basic.t
+++ b/t/basic.t
@@ -1,6 +1,8 @@
 #!perl
 
 use strict;
+use FindBin;
+use lib "$FindBin::Bin/../lib";
 use Test::More;
 use JSON;
 use File::Slurp;

commit 53a42869bac8511774e4b7f1ca23f235d945341e
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Fri Jul 17 16:30:00 2020 +0100

    Updated example configuration for wsgetmail

diff --git a/README b/README
index bc2d036..e41697d 100644
--- a/README
+++ b/README
@@ -19,19 +19,21 @@ CONFIGURATION
     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 : { "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" }
+    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
diff --git a/doc/example.conf b/doc/example.conf
index cbab0e0..f15d022 100644
--- a/doc/example.conf
+++ b/doc/example.conf
@@ -1,6 +1,6 @@
 {
    "command": "/path/to/rt/bin/rt-mailgate",
-   "command_args": '--url http://rt.example.tld/ --queue "general" --action correspond',
+   "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",
@@ -8,6 +8,5 @@
    "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/lib/App/wsgetmail.pm b/lib/App/wsgetmail.pm
index a9ecf30..5b36d79 100644
--- a/lib/App/wsgetmail.pm
+++ b/lib/App/wsgetmail.pm
@@ -40,7 +40,7 @@ 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_args": "--url http://rt.example.tld/ --queue general --action correspond",
    "command_timeout": 15,
    "recipient":"rt at example.tld",
    "action_on_fetched":"mark_as_read",
@@ -48,7 +48,6 @@ example configuration :
    "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"
 }
 

commit ce26983359c81ab84b35bc4afe481dcd92070c69
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Fri Jul 17 16:29:29 2020 +0100

    Added FindBin for path to wsgetmail script

diff --git a/bin/wsgetmail.in b/bin/wsgetmail.in
index 5053fee..3e2f8b5 100644
--- a/bin/wsgetmail.in
+++ b/bin/wsgetmail.in
@@ -2,6 +2,8 @@
 ### before: #!@PERL@
 
 use strict;
+use FindBin;
+use lib "$FindBin::Bin/../lib";
 use JSON;
 use App::wsgetmail;
 use File::Slurp;
@@ -70,6 +72,8 @@ wsgetmail --config[uration] path/to/file.json [--options "{..}"] [--debug]
 
 =item debug - flag indicating that debug warnings should be output (optional)
 
+=item dry-run - fetch mail and deliver it but don't delete or mark as read (optional)
+
 =back
 
 =head1 CONFIGURATION
@@ -83,29 +87,18 @@ 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"
-
-Any additional options in handler_options will be passed through to the command called
-
 example configuration :
+
 {
-  "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"
-   },
+   "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",
-   "action_on_fetched":"mark_as_read",
    "folder":"Inbox"
 }
 

commit dc7e2b814be1a5a0816180ac1e30859ef392baa6
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Fri Jul 17 16:06:44 2020 +0100

    cleanup unused Data::Dumper

diff --git a/bin/wsgetmail.in b/bin/wsgetmail.in
index 3876c62..5053fee 100644
--- a/bin/wsgetmail.in
+++ b/bin/wsgetmail.in
@@ -4,7 +4,6 @@
 use strict;
 use JSON;
 use App::wsgetmail;
-use Data::Dumper;
 use File::Slurp;
 use Pod::Usage;
 use Getopt::Long;

commit 48e8f4477ac3a04a24e93bb0670ca6c7fe24b032
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Fri Jul 17 16:06:24 2020 +0100

    Makefile fixes

diff --git a/Makefile.PL b/Makefile.PL
index 51de10a..71c9ef0 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -1,3 +1,4 @@
+use lib qw(.);
 use inc::Module::Install;
 
 RTx 'App-wsgetmail';
@@ -18,7 +19,7 @@ my $perl_path = $Config{perlpath};
 $perl_path .= $Config{_exe}
     if $^O ne 'VMS' and $perl_path !~ m/$Config{_exe}$/i;
 
-for my $file ( 'bin/wsgetmail365' ) {
+for my $file ( 'bin/wsgetmail' ) {
     substitute(
         {
             PERL => $perl_path,

commit 56254579633d5276c9bf41f26630db14973c0f23
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Fri Jul 17 12:39:19 2020 +0100

    Reworked to simplify mail delivery

diff --git a/bin/wsgetmail365.in b/bin/wsgetmail.in
old mode 100755
new mode 100644
similarity index 65%
rename from bin/wsgetmail365.in
rename to bin/wsgetmail.in
index 715936a..3876c62
--- a/bin/wsgetmail365.in
+++ b/bin/wsgetmail.in
@@ -3,59 +3,45 @@
 
 use strict;
 use JSON;
-use App::wsgetmail::MS365;
+use App::wsgetmail;
 use Data::Dumper;
 use File::Slurp;
 use Pod::Usage;
 use Getopt::Long;
 
-my ($config_file, $options);
-my ($verbose, $help) = (0,0);
+my ($config_file, $options, $dry_run);
+my ($debug, $help) = (0,0);
 
-GetOptions('help|?' => \$help, "verbose" => \$verbose, "c|config|configuration=s" => \$config_file, 'options' => \&options);
+GetOptions('h|help|?' => \$help,
+           "v|verbose|debug" => \$debug,
+           'dry-run' => \$dry_run,
+           "c|config|configuration=s" => \$config_file,
+           'options=s' => \$options);
 
 pod2usage(1) if $help;
 pod2usage(1) unless ($config_file);
 die "Can't find config file $config_file" unless (-f $config_file);
 
+# parse options, over-ride config if provided extra options
 my $config_json = read_file($config_file);
-
 my $config = decode_json($config_json);
-
-# parse handler options, over-ride config if provided
-
 my $extra_options = (defined($options) && $options ) ? decode_json($options) : { };
 foreach my $option ( keys %$extra_options ) {
     $config->{$option} = $extra_options->{$option};
 }
+$config->{dry_run} = $dry_run if (defined $dry_run);
+$config->{debug} = $debug if (defined $debug);
 
-my $getmail = App::wsgetmail::MS365->new($config);
+
+my $getmail = App::wsgetmail->new({config => $config});
 
 print "\nfetching mail using configuration $config_file\n";
 
 my $count = 0;
-my $fetched_action_method;
-my $action = $config->{handler_options}{action_on_fetched};
-my $filter;
-if (lc($action) eq 'mark_as_read') {
-    $fetched_action_method = 'mark_message_as_read';
-} elsif ( lc($action) eq "delete" ) {
-    $fetched_action_method = 'delete_message';
-} else {
-    warn "no recognised action for fetched mail, mailbox not updated";
-}
 
 while (my $message = $getmail->get_next_message()) {
-    my $file = $getmail->get_message_mime_content($message->id);
-    unless ($file) {
-        warn "failed to get mime content for message, skipping..";
-        next;
-    }
-    my $forwarded_ok = $getmail->forward($message, $file);
-    if ($forwarded_ok) {
-        $getmail->$fetched_action_method($message->id);
-        $count++;
-    }
+    my $ok = $getmail->process_message($message);
+    $count++ if ($ok);
 }
 
 print "\nprocessed $count messages\n";
@@ -65,15 +51,15 @@ __END__
 
 =head1 NAME
 
-wsgetmail365 - get mail from microsoft365 cloud
+wsgetmail - get mail from cloud webservices
 
 =head1 DESCRIPTION
 
-get mail from microsoft365 Exchange Online
+get mail from cloud webservices
 
 =head1 SYNOPSIS
 
-wsgetmail365 --config[uration] path/to/file.json [--options "{..}"] [--debug]
+wsgetmail --config[uration] path/to/file.json [--options "{..}"] [--debug]
 
 =head1 ARGUMENTS
 
diff --git a/doc/example.conf b/doc/example.conf
index 53a1b40..cbab0e0 100644
--- a/doc/example.conf
+++ b/doc/example.conf
@@ -1,12 +1,9 @@
 {
-  "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"
-   },
+   "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",
diff --git a/lib/App/wsgetmail.pm b/lib/App/wsgetmail.pm
index ba79cae..a9ecf30 100644
--- a/lib/App/wsgetmail.pm
+++ b/lib/App/wsgetmail.pm
@@ -1,17 +1,13 @@
-use strict;
-use warnings;
-use 5.010001;
-
 package App::wsgetmail;
 
+use Moo;
+
 our $VERSION = '0.01';
 
 =head1 NAME
 
 App::wsgetmail - Fetch mail from the cloud using webservices
 
-=cut
-
 =head1 DESCRIPTION
 
 A simple command line application/script to fetch mail from the cloud
@@ -22,7 +18,7 @@ configurable action with the fetched email.
 
 =head1 SYNOPSIS
 
-wsgetmail365 --configuration path/to/file.json [--debug]
+wsgetmail365 --configuration path/to/file.json [--debug] [ --dry-run]
 
 =head1 CONFIGURATION
 
@@ -43,14 +39,11 @@ action on fetching mail : either "mark_as_read" or "delete"
 
 example configuration :
 {
-  "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"
-   },
+   "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",
@@ -61,10 +54,125 @@ example configuration :
 
 an example configuration file is included in the docs/ directory of this package
 
+=cut
+
+use Clone 'clone';
+use Module::Load;
+use App::wsgetmail::MDA;
+
+has config => (
+    is => 'ro',
+    required => 1
+);
+
+has mda => (
+    is => 'rw',
+    lazy => 1,
+    handles => [ qw(forward) ],
+    builder => '_build_mda'
+);
+
+
+has client_class => (
+    is => 'ro',
+    default => sub { 'MS365' }
+);
+
+has client => (
+    is => 'ro',
+    lazy => 1,
+    handles => [ qw( get_next_message
+                     get_message_mime_content
+                     mark_message_as_read
+                     delete_message) ],
+    builder => '_build_client'
+);
+
+
+has _post_fetch_action => (
+    is => 'ro',
+    lazy => 1,
+    builder => '_build__post_fetch_action'
+);
+
+
+sub _build__post_fetch_action {
+    my $self = shift;
+    my $fetched_action_method;
+    my $action = $self->config->{action_on_fetched};
+    if (lc($action) eq 'mark_as_read') {
+        $fetched_action_method = 'mark_message_as_read';
+    } elsif ( lc($action) eq "delete" ) {
+        $fetched_action_method = 'delete_message';
+    } else {
+        warn "no recognised action for fetched mail, mailbox not updated";
+    }
+    return $fetched_action_method;
+}
+
+
+sub process_message {
+    my ($self, $message) = @_;
+    my $client = $self->client;
+    my $file = $client->get_message_mime_content($message->id);
+    unless ($file) {
+        warn "failed to get mime content for message ". $message->id;
+        return 0;
+    }
+    my $ok = $self->forward($message, $file);
+    if ($ok) {
+        $ok = $self->post_fetch_action($message);
+    }
+    return $ok;
+}
+
+sub post_fetch_action {
+    my ($self, $message) = @_;
+    my $method = $self->_post_fetch_action;
+    my $ok = 1;
+    # check for dry-run option
+    if ($self->config->{dry_run}) {
+        warn "dry run so not running $method action on fetched mail";
+        return 1;
+    }
+    if ($method) {
+        $ok = $self->$method($message->id);
+    }
+    return $ok;
+}
+
+###
+
+sub _build_client {
+    my $self = shift;
+    my $classname = 'App::wsgetmail::' . $self->client_class;
+    load $classname;
+    my $config = clone $self->config;
+    $config->{post_fetch_action} = $self->_post_fetch_action;
+    return $classname->new($config);
+}
+
+
+sub _build_mda {
+    my $self = shift;
+    my $config = clone $self->config;
+    if ( defined $self->config->{username}) {
+        $config->{recipient} //= $self->config->{username};
+    }
+    return App::wsgetmail::MDA->new($config);
+}
+
+
+
+##
+
+
 =head1 SEE ALSO
 
 =over 4
 
+=item App::wsgetmail::MDA
+
 =item App::wsgetmail::MS365
 
 =item wsgemail365
diff --git a/lib/App/wsgetmail/Handler/RT/Mailgate.pm b/lib/App/wsgetmail/Handler/RT/Mailgate.pm
deleted file mode 100644
index 08a4e86..0000000
--- a/lib/App/wsgetmail/Handler/RT/Mailgate.pm
+++ /dev/null
@@ -1,184 +0,0 @@
-package App::wsgetmail::Handler::RT::Mailgate;
-
-=head1 NAME
-
-App::wsgetmail::Handler::RT::Mailgate - forward mail via rt-mailgate
-
-=cut
-
-use Moo;
-use IPC::Run qw( run timeout );
-
-=head1 DESCRIPTION
-
-Forward mail to RT with rt-mailgate.
-
-=head1 ATTRIBUTES
-
-=over 4
-
-=item debug - default false, optional
-
-=item url - URI to RT, required
-
-=item request_timeout - timeout, optional
-
-=item extension - field to use for username extension, optional
-
-=item recipient - address to expect and parse for extension, required
-
-=item command_path - path to rt-mailgate
-
-=item extra_options - hashref of extra options to pass to rt-mailgate
-
-=back
-
-=cut
-
-has debug => (
-    is => 'ro',
-    lazy => 1,
-    default => sub { 0 }
-);
-
-has url => (
-    is => 'ro',
-    required => 1,
-);
-
-has request_timeout => (
-    is => 'ro',
-    lazy => 1,
-    default => sub { 10 }
-);
-
-has action => (
-    is => 'ro',
-    lazy => 1,
-    default => sub { 'correspond' }
-);
-
-has queue => (
-    is => 'ro',
-    required => 0
-);
-
-has extension => (
-    is => 'ro',
-    required => 0
-);
-
-has recipient => (
-    is => 'ro',
-    required => 1,
-);
-
-has command_path  => (
-    is => 'ro',
-    required => 0
-);
-
-has extra_options => (
-    is => 'ro',
-    required => 1,
-);
-
-my $option_configuration = {
-    url => 'string',
-    action => [qw(correspond comment)],
-    extension => [qw(queue action ticket)],
-    request_timeout => 'integer',
-    queue => 'string'
-};
-
-my $standard_options = { %$option_configuration,
-                         debug => 1,
-                         recipient => 1,
-                         command_path => 1,
-                         action_on_fetched => 1
-                     };
-
-
-around BUILDARGS => sub {
-  my ( $orig, $class, $args ) = @_;
-  my $extra_options = { };
-  foreach my $extra_option (grep { ! exists $standard_options->{$_} } keys %$args) {
-      $extra_options->{$extra_option} = delete $args->{$extra_option};
-  }
-  $args->{extra_options} = $extra_options;
-  return $class->$orig($args);
-};
-
-
-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;
-            }
-        }
-    }
-
-    my $arguments = [ ];
-    foreach my $option (keys %$option_configuration) {
-        next unless ($self->$option);
-        if ( $option eq 'request_timeout' ) {
-            push(@$arguments, "--timeout", $self->request_timeout);
-        }
-        else {
-            push(@$arguments, "--$option", $self->$option);
-        }
-    }
-    push (@$arguments, "--debug") if ($self->debug);
-
-    my %extra = %{$self->extra_options};
-    foreach my $option ( keys %extra ) {
-        if ($extra{$option} eq '1' or (JSON::is_bool($extra{$option}) && $extra{$option})) {
-            push(@$arguments, "--$option");
-        } else {
-            push(@$arguments, "--$option", $extra{$option});
-        }
-    }
-
-    if ($self->debug) {
-        warn "calling $filename with arguments " . join (' ', @$arguments);
-    }
-    # run command
-    return $self->_run_command($filename, $arguments);
-}
-
-
-sub _run_command {
-    my ($self, $filename, $arguments) = @_;
-    my $command = 'rt-mailgate';
-    if ($self->command_path) {
-        $command = join('/', $self->command_path, $command);
-    }
-    open my $fh, "<$filename"  or die $!;
-    my ($input,$output, $error);
-    my $ok = run ([ $command, @$arguments], $fh, \$output, \$error, timeout( $self->request_timeout + 5 ) );
-    close $fh;
-
-    unless ($ok) {
-        warn "failed to run rt-mailgate for file $filename : $! $?";
-        return 0;
-    }
-
-    if ($self->debug or not $ok) {
-        warn $error;
-    }
-
-    return $ok;
-}
-
-
-
-1;
diff --git a/lib/App/wsgetmail/MDA.pm b/lib/App/wsgetmail/MDA.pm
new file mode 100644
index 0000000..a913689
--- /dev/null
+++ b/lib/App/wsgetmail/MDA.pm
@@ -0,0 +1,145 @@
+package App::wsgetmail::MDA;
+use Moo;
+
+use IPC::Run qw( run timeout );
+
+has command => (
+    is => 'ro',
+    required => 1,
+);
+has command_args => (
+    is => 'ro',
+    required => 1,
+);
+
+has command_timeout => (
+    is => 'ro',
+    default => sub { 30; }
+);
+
+has extension => (
+    is => 'ro',
+    required => 0
+);
+
+has recipient => (
+    is => 'ro',
+    required => 0,
+);
+
+has debug => (
+    is => 'ro',
+    default => sub { 0 }
+);
+
+
+
+my @config_fields = qw( command command_args command_timeout extension recipient debug);
+around BUILDARGS => sub {
+    my ( $orig, $class, $config ) = @_;
+    my $attributes = { map { $_ => $config->{$_} } @config_fields };
+    return $class->$orig($attributes);
+};
+
+
+###
+
+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);
+}
+
+
+sub _run_command {
+    my ($self, $filename) = @_;
+    open my $fh, "<$filename"  or die $!;
+    my ($input, $output, $error);
+    my $ok = run ([ $self->command, _split_command_args($self->command_args, 1)], $fh, \$output, \$error, timeout( $self->command_timeout + 5 ) );
+    close $fh;
+
+    unless ($ok) {
+        warn sprintf('failed to run command "%s %s" for file %s : %s : %s',
+                     $self->command,
+                     ($self->debug ? join(' ', _split_command_args($self->command_args)) : '' ),
+                     $filename, $!, $?);
+        warn "output : $output\nerror:$error\n" if ($self->debug);
+    }
+
+    return $ok;
+}
+
+
+#TODO: make into a simple cpan module
+# Loosely based on https://metacpan.org/pod/Parse::CommandLine
+sub _split_command_args {
+    my ($line, $strip_quotes) = @_;
+
+    # strip leading/trailing spaces
+    $line =~ s/^\s+//;
+    $line =~ s/\s+$//;
+
+    my (@args, $quoted, $escape_next, $next_arg);
+    foreach my $character (split('', $line) ) {
+        if ($escape_next) {
+            $next_arg .= $character;
+            $escape_next = undef;
+            next;
+        }
+
+        if ($character =~ m|\\|) {
+            $next_arg .= $character;
+            if ($quoted) {
+                $escape_next = 1;
+            }
+            next;
+        }
+
+        if ($character =~ m/\s/) {
+            if ($quoted) {
+                $next_arg .= $character;
+            }
+            else {
+                push @args, $next_arg if defined $next_arg;
+                undef $next_arg;
+            }
+            next;
+        }
+
+        if ($character =~ m/['"]/) {
+            if ($quoted) {
+                if ($character eq $quoted) {
+                    $quoted = undef;
+                    $next_arg .= $character unless ($strip_quotes);
+                } else {
+                    $next_arg .= $character;
+                }
+            }
+            else {
+                $quoted = $character;
+                $next_arg .= $character unless ($strip_quotes);
+            }
+            next;
+        }
+        $next_arg .= $character;
+    }
+    push @args, $next_arg if defined $next_arg;
+    return @args;
+}
+
+1;
diff --git a/lib/App/wsgetmail/MS365.pm b/lib/App/wsgetmail/MS365.pm
index d8afdf7..ee4e261 100644
--- a/lib/App/wsgetmail/MS365.pm
+++ b/lib/App/wsgetmail/MS365.pm
@@ -36,8 +36,6 @@ Moo class providing methods to connect to and fetch mail from Microsoft 365
 
 =item folder
 
-=item handler_options
-
 =back
 
 =cut
@@ -73,23 +71,17 @@ has global_access => (
     default => sub { return 0 }
 );
 
-has secret  => (
+has secret => (
     is => 'ro',
     required => 0,
 );
 
-has handler_options => (
+has post_fetch_action => (
     is => 'ro',
-    required => 1,
+    required => 1
 );
 
-#
-
-has _delivery_handler => (
-    is => 'ro',
-    lazy => 1,
-    builder => '_build_handler'
-);
+###
 
 has _client => (
     is => 'ro',
@@ -115,17 +107,16 @@ has _next_fetch_url => (
     default => sub { '' }
 );
 
-has _handler => (
-    is => 'ro',
-    lazy => 1,
-    builder => '_build__handler',
-    handles => [qw/forward/]
-);
 
-our $handlers = {
-    Mailgate => 'App::wsgetmail::Handler::RT::Mailgate',
+my @config_fields = qw(client_id tenant_id username user_password global_access secret folder post_fetch_action);
+around BUILDARGS => sub {
+  my ( $orig, $class, $config ) = @_;
+
+  my $attributes = { map { $_ => $config->{$_} } @config_fields };
+  return $class->$orig($attributes);
 };
 
+
 =head1 METHODS
 
 =head2 new
@@ -311,9 +302,8 @@ sub _get_message_list {
 sub _get_message_filters {
     my $self = shift;
     #TODO: handle filtering multiple folders
-    my $action = lc($self->handler_options->{action_on_fetched});
     my $filters = [ ];
-    if (lc($action) eq 'mark_as_read') {
+    if ( $self->post_fetch_action eq 'mark_message_as_read') {
         push(@$filters, 'isRead eq false');
     }
 
@@ -321,19 +311,6 @@ sub _get_message_filters {
     return $filter;
  }
 
-sub _build__handler {
-    my $self = shift;
-    my $handler_options = $self->handler_options;
-    my $handler_class = delete $handler_options->{class};
-    unless ($handler_class && $handlers->{$handler_class}) {
-        die "valid class required for handler, classes available " . join(', ', keys %$handlers);
-    }
-    my $classname =  $handlers->{$handler_class};
-    eval "require $classname;";
-    my $handler = $handlers->{$handler_class}->new($handler_options);
-    return $handler;
-}
-
 sub _build_client {
     my $self = shift;
     my $client = App::wsgetmail::MS365::Client->new( {
diff --git a/t/basic.t b/t/basic.t
index 545ae9d..edc9bf4 100644
--- a/t/basic.t
+++ b/t/basic.t
@@ -5,9 +5,8 @@ use Test::More;
 use JSON;
 use File::Slurp;
 use Test::LWP::UserAgent;
-
 use App::wsgetmail::MS365::Client;
-use App::wsgetmail::MS365;
+use App::wsgetmail;
 
 
 my $test_config = get_test_config();
@@ -23,8 +22,9 @@ $useragent->map_response( qr{$graph_v1_url/me/mailFolders/} => get_mocked_messag
 
 *App::wsgetmail::MS365::Client::_new_useragent = sub { return $useragent };
 
-my $getmail = App::wsgetmail::MS365->new($test_config);
-isa_ok($getmail, 'App::wsgetmail::MS365');
+my $getmail = App::wsgetmail->new({config => $test_config});
+isa_ok($getmail, 'App::wsgetmail');
+isa_ok($getmail->client, 'App::wsgetmail::MS365');
 
 my $message1 = $getmail->get_next_message();
 isa_ok($message1, 'App::wsgetmail::MS365::Message');
@@ -68,14 +68,9 @@ sub get_mocked_messages_response {
 
 sub get_test_config {
     return {
-        "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"
-        },
+        "command" => 'rt-mailgate',
+        "command_args" => '--url http://test.local/ --queue "general" --action correspond --debug --no-verify-ssl',
+        "action_on_fetched" => "mark_as_read",
         "username" => 'rt at example.tld',
         "user_password" => "password",
         "tenant_id" => "abcd1234-xxxx-xxxx-xxxx-123abcde1234",

commit 05b9385bcd0d39b6048a2b0a9324fc09fd7205b2
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Fri Jul 10 18:23:06 2020 +0100

    Remove boilerplate tests from wsgetmail app

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 60d50f0340d4a7df242665e3e30386c6b0db49b1
Author: craig kaiser <craig at bestpractical.com>
Date:   Thu Jul 9 10:43:01 2020 -0400

    Make bin/wsgetmail365 into bin/wsgetmail365.in

diff --git a/bin/wsgetmail365 b/bin/wsgetmail365.in
similarity index 99%
rename from bin/wsgetmail365
rename to bin/wsgetmail365.in
index da232e0..715936a 100755
--- a/bin/wsgetmail365
+++ b/bin/wsgetmail365.in
@@ -1,4 +1,5 @@
 #!/usr/bin/env perl
+### before: #!@PERL@
 
 use strict;
 use JSON;

commit b2bb95bf76a38afda9ecc91737a752deac25c82d
Author: craig kaiser <craig at bestpractical.com>
Date:   Thu Jul 9 10:42:18 2020 -0400

    Add gitignore file

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4851496
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+blib*
+Makefile
+Makefile.old
+pm_to_blib*
+*.tar.gz
+cover_db
+pod2htm*.tmp
+*.bak
+*.swp
+/MYMETA.*
+/t/tmp
+/xt/tmp

commit 5a0e7c30031c58016ef3f1dc450c88ee8a490882
Author: craig kaiser <craig at bestpractical.com>
Date:   Thu Jul 9 10:41:58 2020 -0400

    Use Module::Install in place of ExtUtils::MakeMaker

diff --git a/MANIFEST b/MANIFEST
index 9e47df3..44331f2 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -1,15 +1,41 @@
+bin/wsgetmail365
+bin/wsgetmail365.in
 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
+ignore.txt
+inc/Module/Install.pm
+inc/Module/Install/Base.pm
+inc/Module/Install/Can.pm
+inc/Module/Install/Fetch.pm
+inc/Module/Install/Include.pm
+inc/Module/Install/Makefile.pm
+inc/Module/Install/Metadata.pm
+inc/Module/Install/ReadmeFromPod.pm
+inc/Module/Install/RTx.pm
+inc/Module/Install/RTx/Runtime.pm
+inc/Module/Install/Substitute.pm
+inc/Module/Install/Win32.pm
+inc/Module/Install/WriteAll.pm
+inc/YAML/Tiny.pm
 lib/App/wsgetmail.pm
-lib/App/wsgetmail/MS360.pm
-lib/App/wsgetmail/MS360/Client.pm
 lib/App/wsgetmail/Handler/RT/Mailgate.pm
-bin/wsgetmail
+lib/App/wsgetmail/MS365.pm
+lib/App/wsgetmail/MS365/Client.pm
+lib/App/wsgetmail/MS365/Message.pm
 Makefile.PL
 MANIFEST			This list of files
+META.yml
 README
-ignore.txt
 t/00-load.t
+t/basic.t
 t/manifest.t
+t/mock_responses/messages.json
 t/pod-coverage.t
 t/pod.t
-
+xt/boilerplate.t
diff --git a/META.yml b/META.yml
new file mode 100644
index 0000000..fb58366
--- /dev/null
+++ b/META.yml
@@ -0,0 +1,37 @@
+---
+abstract: 'Fetch mail from the cloud using webservices'
+author:
+  - 'Best Practical Solutions, LLC <modules at bestpractical.com>'
+build_requires:
+  ExtUtils::MakeMaker: 6.59
+configure_requires:
+  ExtUtils::MakeMaker: 6.59
+distribution_type: module
+dynamic_config: 1
+generated_by: 'Module::Install version 1.19'
+license: gpl_2
+meta-spec:
+  url: http://module-build.sourceforge.net/META-spec-v1.4.html
+  version: 1.4
+name: App-wsgetmail
+no_index:
+  directory:
+    - inc
+    - t
+    - xt
+requires:
+  Azure::AD::ClientCredentials: 0
+  IPC::Run: 0
+  JSON: 0
+  LWP::UserAgent: '6.42'
+  Moo: 0
+  Test::LWP::UserAgent: 0
+  Test::More: 0
+  URI: 0
+  URI::Escape: 0
+  perl: 5.10.1
+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
diff --git a/MYMETA.json b/MYMETA.json
new file mode 100644
index 0000000..4123c8f
--- /dev/null
+++ b/MYMETA.json
@@ -0,0 +1,59 @@
+{
+   "abstract" : "Fetch mail from the cloud using webservices",
+   "author" : [
+      "Best Practical Solutions, LLC <modules at bestpractical.com>"
+   ],
+   "dynamic_config" : 0,
+   "generated_by" : "Module::Install version 1.19, CPAN::Meta::Converter version 2.150010",
+   "license" : [
+      "gpl_2"
+   ],
+   "meta-spec" : {
+      "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec",
+      "version" : "2"
+   },
+   "name" : "App-wsgetmail",
+   "no_index" : {
+      "directory" : [
+         "inc",
+         "t",
+         "xt"
+      ]
+   },
+   "prereqs" : {
+      "build" : {
+         "requires" : {
+            "ExtUtils::MakeMaker" : "6.59"
+         }
+      },
+      "configure" : {
+         "requires" : {
+            "ExtUtils::MakeMaker" : "0"
+         }
+      },
+      "runtime" : {
+         "requires" : {
+            "Azure::AD::ClientCredentials" : "0",
+            "IPC::Run" : "0",
+            "JSON" : "0",
+            "LWP::UserAgent" : "6.42",
+            "Moo" : "0",
+            "Test::LWP::UserAgent" : "0",
+            "Test::More" : "0",
+            "URI" : "0",
+            "URI::Escape" : "0",
+            "perl" : "5.010001"
+         }
+      }
+   },
+   "release_status" : "stable",
+   "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",
+   "x_serialization_backend" : "JSON::PP version 2.27400_02"
+}
diff --git a/MYMETA.yml b/MYMETA.yml
new file mode 100644
index 0000000..5e984ba
--- /dev/null
+++ b/MYMETA.yml
@@ -0,0 +1,37 @@
+---
+abstract: 'Fetch mail from the cloud using webservices'
+author:
+  - 'Best Practical Solutions, LLC <modules at bestpractical.com>'
+build_requires:
+  ExtUtils::MakeMaker: '6.59'
+configure_requires:
+  ExtUtils::MakeMaker: '0'
+dynamic_config: 0
+generated_by: 'Module::Install version 1.19, CPAN::Meta::Converter version 2.150010'
+license: gpl
+meta-spec:
+  url: http://module-build.sourceforge.net/META-spec-v1.4.html
+  version: '1.4'
+name: App-wsgetmail
+no_index:
+  directory:
+    - inc
+    - t
+    - xt
+requires:
+  Azure::AD::ClientCredentials: '0'
+  IPC::Run: '0'
+  JSON: '0'
+  LWP::UserAgent: '6.42'
+  Moo: '0'
+  Test::LWP::UserAgent: '0'
+  Test::More: '0'
+  URI: '0'
+  URI::Escape: '0'
+  perl: '5.010001'
+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
+x_serialization_backend: 'CPAN::Meta::YAML version 0.018'
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..9b8b1bf
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,898 @@
+# This Makefile is for the App::wsgetmail extension to perl.
+#
+# It was generated automatically by MakeMaker version
+# 7.24 (Revision: 72400) from the contents of
+# Makefile.PL. Don't edit this file, edit Makefile.PL instead.
+#
+#       ANY CHANGES MADE HERE WILL BE LOST!
+#
+#   MakeMaker ARGV: ()
+#
+
+#   MakeMaker Parameters:
+
+#     ABSTRACT => q[Fetch mail from the cloud using webservices]
+#     AUTHOR => [q[Best Practical Solutions, LLC <modules at bestpractical.com>]]
+#     BUILD_REQUIRES => { ExtUtils::MakeMaker=>q[6.59] }
+#     CONFIGURE_REQUIRES => {  }
+#     DISTNAME => q[App-wsgetmail]
+#     INSTALLARCHLIB => q[/Users/craigkaiser/bps/rts/rt5/local/plugins/App-wsgetmail/lib]
+#     INSTALLSITEARCH => q[/Users/craigkaiser/bps/rts/rt5/local/man]
+#     INSTALLSITELIB => q[/Users/craigkaiser/bps/rts/rt5/local/plugins/App-wsgetmail/lib]
+#     INSTALLSITEMAN1DIR => q[/Users/craigkaiser/bps/rts/rt5/local/man/man1]
+#     INSTALLSITEMAN3DIR => q[/Users/craigkaiser/bps/rts/rt5/local/man/man3]
+#     INSTALLVENDORARCH => q[/usr/share/man]
+#     INSTALLVENDORLIB => q[/Users/craigkaiser/bps/rts/rt5/local/plugins/App-wsgetmail/lib]
+#     INSTALLVENDORMAN1DIR => q[/usr/share/man/man1]
+#     INSTALLVENDORMAN3DIR => q[/usr/share/man/man3]
+#     LICENSE => q[gpl_2]
+#     MIN_PERL_VERSION => q[5.010001]
+#     NAME => q[App::wsgetmail]
+#     NO_META => q[1]
+#     PREREQ_PM => { Azure::AD::ClientCredentials=>q[0], ExtUtils::MakeMaker=>q[6.59], IPC::Run=>q[0], JSON=>q[0], LWP::UserAgent=>q[6.42], Moo=>q[0], Test::LWP::UserAgent=>q[0], Test::More=>q[0], URI=>q[0], URI::Escape=>q[0] }
+#     SIGN => q[1]
+#     TEST_REQUIRES => {  }
+#     VERSION => q[0.01]
+#     VERSION_FROM => q[lib/App/wsgetmail.pm]
+#     dist => { PREOP=>q[$(PERL) -I. "-MModule::Install::Admin" -e "dist_preop(q($(DISTVNAME)))"] }
+#     realclean => { FILES=>q[MYMETA.yml] }
+#     test => { TESTS=>q[t/*.t xt/*.t] }
+
+# --- MakeMaker post_initialize section:
+
+
+# --- MakeMaker const_config section:
+
+# These definitions are from config.sh (via /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/lib/5.26.1/darwin-2level/Config.pm).
+# They may have been overridden via Makefile.PL or on the command line.
+AR = ar
+CC = cc
+CCCDLFLAGS =  
+CCDLFLAGS =  
+DLEXT = bundle
+DLSRC = dl_dlopen.xs
+EXE_EXT = 
+FULL_AR = /usr/bin/ar
+LD = cc
+LDDLFLAGS =  -mmacosx-version-min=10.14 -bundle -undefined dynamic_lookup -fstack-protector-strong
+LDFLAGS =  -mmacosx-version-min=10.14 -fstack-protector-strong
+LIBC = 
+LIB_EXT = .a
+OBJ_EXT = .o
+OSNAME = darwin
+OSVERS = 18.0.0
+RANLIB = ranlib
+SITELIBEXP = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/lib/site_perl/5.26.1
+SITEARCHEXP = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/lib/site_perl/5.26.1/darwin-2level
+SO = dylib
+VENDORARCHEXP = /usr/share/man
+VENDORLIBEXP = /Users/craigkaiser/bps/rts/rt5/local/plugins/App-wsgetmail/lib
+
+
+# --- MakeMaker constants section:
+AR_STATIC_ARGS = cr
+DIRFILESEP = /
+DFSEP = $(DIRFILESEP)
+NAME = App::wsgetmail
+NAME_SYM = App_wsgetmail
+VERSION = 0.01
+VERSION_MACRO = VERSION
+VERSION_SYM = 0_01
+DEFINE_VERSION = -D$(VERSION_MACRO)=\"$(VERSION)\"
+XS_VERSION = 0.01
+XS_VERSION_MACRO = XS_VERSION
+XS_DEFINE_VERSION = -D$(XS_VERSION_MACRO)=\"$(XS_VERSION)\"
+INST_ARCHLIB = blib/arch
+INST_SCRIPT = blib/script
+INST_BIN = blib/bin
+INST_LIB = blib/lib
+INST_MAN1DIR = blib/man1
+INST_MAN3DIR = blib/man3
+MAN1EXT = 1
+MAN3EXT = 3
+INSTALLDIRS = site
+DESTDIR = 
+PREFIX = $(SITEPREFIX)
+PERLPREFIX = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1
+SITEPREFIX = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1
+VENDORPREFIX = 
+INSTALLPRIVLIB = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/lib/5.26.1
+DESTINSTALLPRIVLIB = $(DESTDIR)$(INSTALLPRIVLIB)
+INSTALLSITELIB = /Users/craigkaiser/bps/rts/rt5/local/plugins/App-wsgetmail/lib
+DESTINSTALLSITELIB = $(DESTDIR)$(INSTALLSITELIB)
+INSTALLVENDORLIB = /Users/craigkaiser/bps/rts/rt5/local/plugins/App-wsgetmail/lib
+DESTINSTALLVENDORLIB = $(DESTDIR)$(INSTALLVENDORLIB)
+INSTALLARCHLIB = /Users/craigkaiser/bps/rts/rt5/local/plugins/App-wsgetmail/lib
+DESTINSTALLARCHLIB = $(DESTDIR)$(INSTALLARCHLIB)
+INSTALLSITEARCH = /Users/craigkaiser/bps/rts/rt5/local/man
+DESTINSTALLSITEARCH = $(DESTDIR)$(INSTALLSITEARCH)
+INSTALLVENDORARCH = /usr/share/man
+DESTINSTALLVENDORARCH = $(DESTDIR)$(INSTALLVENDORARCH)
+INSTALLBIN = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/bin
+DESTINSTALLBIN = $(DESTDIR)$(INSTALLBIN)
+INSTALLSITEBIN = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/bin
+DESTINSTALLSITEBIN = $(DESTDIR)$(INSTALLSITEBIN)
+INSTALLVENDORBIN = 
+DESTINSTALLVENDORBIN = $(DESTDIR)$(INSTALLVENDORBIN)
+INSTALLSCRIPT = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/bin
+DESTINSTALLSCRIPT = $(DESTDIR)$(INSTALLSCRIPT)
+INSTALLSITESCRIPT = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/bin
+DESTINSTALLSITESCRIPT = $(DESTDIR)$(INSTALLSITESCRIPT)
+INSTALLVENDORSCRIPT = 
+DESTINSTALLVENDORSCRIPT = $(DESTDIR)$(INSTALLVENDORSCRIPT)
+INSTALLMAN1DIR = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/man/man1
+DESTINSTALLMAN1DIR = $(DESTDIR)$(INSTALLMAN1DIR)
+INSTALLSITEMAN1DIR = /Users/craigkaiser/bps/rts/rt5/local/man/man1
+DESTINSTALLSITEMAN1DIR = $(DESTDIR)$(INSTALLSITEMAN1DIR)
+INSTALLVENDORMAN1DIR = /usr/share/man/man1
+DESTINSTALLVENDORMAN1DIR = $(DESTDIR)$(INSTALLVENDORMAN1DIR)
+INSTALLMAN3DIR = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/man/man3
+DESTINSTALLMAN3DIR = $(DESTDIR)$(INSTALLMAN3DIR)
+INSTALLSITEMAN3DIR = /Users/craigkaiser/bps/rts/rt5/local/man/man3
+DESTINSTALLSITEMAN3DIR = $(DESTDIR)$(INSTALLSITEMAN3DIR)
+INSTALLVENDORMAN3DIR = /usr/share/man/man3
+DESTINSTALLVENDORMAN3DIR = $(DESTDIR)$(INSTALLVENDORMAN3DIR)
+PERL_LIB =
+PERL_ARCHLIB = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/lib/5.26.1/darwin-2level
+PERL_ARCHLIBDEP = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/lib/5.26.1/darwin-2level
+LIBPERL_A = libperl.a
+FIRST_MAKEFILE = Makefile
+MAKEFILE_OLD = Makefile.old
+MAKE_APERL_FILE = Makefile.aperl
+PERLMAINCC = $(CC)
+PERL_INC = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/lib/5.26.1/darwin-2level/CORE
+PERL_INCDEP = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/lib/5.26.1/darwin-2level/CORE
+PERL = "/Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/bin/perl" "-Iinc"
+FULLPERL = "/Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/bin/perl" "-Iinc"
+ABSPERL = $(PERL)
+PERLRUN = $(PERL)
+FULLPERLRUN = $(FULLPERL)
+ABSPERLRUN = $(ABSPERL)
+PERLRUNINST = $(PERLRUN) "-I$(INST_ARCHLIB)" "-Iinc" "-I$(INST_LIB)"
+FULLPERLRUNINST = $(FULLPERLRUN) "-I$(INST_ARCHLIB)" "-Iinc" "-I$(INST_LIB)"
+ABSPERLRUNINST = $(ABSPERLRUN) "-I$(INST_ARCHLIB)" "-Iinc" "-I$(INST_LIB)"
+PERL_CORE = 0
+PERM_DIR = 755
+PERM_RW = 644
+PERM_RWX = 755
+
+MAKEMAKER   = /Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/lib/5.26.1/ExtUtils/MakeMaker.pm
+MM_VERSION  = 7.24
+MM_REVISION = 72400
+
+# FULLEXT = Pathname for extension directory (eg Foo/Bar/Oracle).
+# BASEEXT = Basename part of FULLEXT. May be just equal FULLEXT. (eg Oracle)
+# PARENT_NAME = NAME without BASEEXT and no trailing :: (eg Foo::Bar)
+# DLBASE  = Basename part of dynamic library. May be just equal BASEEXT.
+MAKE = make
+FULLEXT = App/wsgetmail
+BASEEXT = wsgetmail
+PARENT_NAME = App
+DLBASE = $(BASEEXT)
+VERSION_FROM = lib/App/wsgetmail.pm
+OBJECT = 
+LDFROM = $(OBJECT)
+LINKTYPE = dynamic
+BOOTDEP = 
+
+# Handy lists of source code files:
+XS_FILES = 
+C_FILES  = 
+O_FILES  = 
+H_FILES  = 
+MAN1PODS = 
+MAN3PODS = lib/App/wsgetmail.pm \
+	lib/App/wsgetmail/Handler/RT/Mailgate.pm \
+	lib/App/wsgetmail/MS365.pm \
+	lib/App/wsgetmail/MS365/Client.pm \
+	lib/App/wsgetmail/MS365/Message.pm
+
+# Where is the Config information that we are using/depend on
+CONFIGDEP = $(PERL_ARCHLIBDEP)$(DFSEP)Config.pm $(PERL_INCDEP)$(DFSEP)config.h
+
+# Where to build things
+INST_LIBDIR      = $(INST_LIB)/App
+INST_ARCHLIBDIR  = $(INST_ARCHLIB)/App
+
+INST_AUTODIR     = $(INST_LIB)/auto/$(FULLEXT)
+INST_ARCHAUTODIR = $(INST_ARCHLIB)/auto/$(FULLEXT)
+
+INST_STATIC      = 
+INST_DYNAMIC     = 
+INST_BOOT        = 
+
+# Extra linker info
+EXPORT_LIST        = 
+PERL_ARCHIVE       = 
+PERL_ARCHIVEDEP    = 
+PERL_ARCHIVE_AFTER = 
+
+
+TO_INST_PM = lib/App/wsgetmail.pm \
+	lib/App/wsgetmail/Handler/RT/Mailgate.pm \
+	lib/App/wsgetmail/MS365.pm \
+	lib/App/wsgetmail/MS365/Client.pm \
+	lib/App/wsgetmail/MS365/Message.pm
+
+
+# --- MakeMaker platform_constants section:
+MM_Unix_VERSION = 7.24
+PERL_MALLOC_DEF = -DPERL_EXTMALLOC_DEF -Dmalloc=Perl_malloc -Dfree=Perl_mfree -Drealloc=Perl_realloc -Dcalloc=Perl_calloc
+
+
+# --- MakeMaker tool_autosplit section:
+# Usage: $(AUTOSPLITFILE) FileToSplit AutoDirToSplitInto
+AUTOSPLITFILE = $(ABSPERLRUN)  -e 'use AutoSplit;  autosplit($$$$ARGV[0], $$$$ARGV[1], 0, 1, 1)' --
+
+
+
+# --- MakeMaker tool_xsubpp section:
+
+
+# --- MakeMaker tools_other section:
+SHELL = /bin/sh
+CHMOD = chmod
+CP = cp
+MV = mv
+NOOP = $(TRUE)
+NOECHO = @
+RM_F = rm -f
+RM_RF = rm -rf
+TEST_F = test -f
+TOUCH = touch
+UMASK_NULL = umask 0
+DEV_NULL = > /dev/null 2>&1
+MKPATH = $(ABSPERLRUN) -MExtUtils::Command -e 'mkpath' --
+EQUALIZE_TIMESTAMP = $(ABSPERLRUN) -MExtUtils::Command -e 'eqtime' --
+FALSE = false
+TRUE = true
+ECHO = echo
+ECHO_N = echo -n
+UNINST = 0
+VERBINST = 0
+MOD_INSTALL = $(ABSPERLRUN) -MExtUtils::Install -e 'install([ from_to => {@ARGV}, verbose => '\''$(VERBINST)'\'', uninstall_shadows => '\''$(UNINST)'\'', dir_mode => '\''$(PERM_DIR)'\'' ]);' --
+DOC_INSTALL = $(ABSPERLRUN) -MExtUtils::Command::MM -e 'perllocal_install' --
+UNINSTALL = $(ABSPERLRUN) -MExtUtils::Command::MM -e 'uninstall' --
+WARN_IF_OLD_PACKLIST = $(ABSPERLRUN) -MExtUtils::Command::MM -e 'warn_if_old_packlist' --
+MACROSTART = 
+MACROEND = 
+USEMAKEFILE = -f
+FIXIN = $(ABSPERLRUN) -MExtUtils::MY -e 'MY->fixin(shift)' --
+CP_NONEMPTY = $(ABSPERLRUN) -MExtUtils::Command::MM -e 'cp_nonempty' --
+
+
+# --- MakeMaker makemakerdflt section:
+makemakerdflt : all
+	$(NOECHO) $(NOOP)
+
+
+# --- MakeMaker dist section:
+TAR = COPY_EXTENDED_ATTRIBUTES_DISABLE=1 COPYFILE_DISABLE=1 tar
+TARFLAGS = cvf
+ZIP = zip
+ZIPFLAGS = -r
+COMPRESS = gzip --best
+SUFFIX = .gz
+SHAR = shar
+PREOP = $(PERL) -I. "-MModule::Install::Admin" -e "dist_preop(q($(DISTVNAME)))"
+POSTOP = $(NOECHO) $(NOOP)
+TO_UNIX = $(NOECHO) $(NOOP)
+CI = ci -u
+RCS_LABEL = rcs -Nv$(VERSION_SYM): -q
+DIST_CP = best
+DIST_DEFAULT = tardist
+DISTNAME = App-wsgetmail
+DISTVNAME = App-wsgetmail-0.01
+
+
+# --- MakeMaker macro section:
+
+
+# --- MakeMaker depend section:
+
+
+# --- MakeMaker cflags section:
+
+
+# --- MakeMaker const_loadlibs section:
+
+
+# --- MakeMaker const_cccmd section:
+
+
+# --- MakeMaker post_constants section:
+
+
+# --- MakeMaker pasthru section:
+
+PASTHRU = LIBPERL_A="$(LIBPERL_A)"\
+	LINKTYPE="$(LINKTYPE)"\
+	PREFIX="$(PREFIX)"\
+	PASTHRU_DEFINE='$(DEFINE) $(PASTHRU_DEFINE)'\
+	PASTHRU_INC='$(INC) $(PASTHRU_INC)'
+
+
+# --- MakeMaker special_targets section:
+.SUFFIXES : .xs .c .C .cpp .i .s .cxx .cc $(OBJ_EXT)
+
+.PHONY: all config static dynamic test linkext manifest blibdirs clean realclean disttest distdir pure_all subdirs clean_subdirs makemakerdflt manifypods realclean_subdirs subdirs_dynamic subdirs_pure_nolink subdirs_static subdirs-test_dynamic subdirs-test_static test_dynamic test_static
+
+
+
+# --- MakeMaker c_o section:
+
+
+# --- MakeMaker xs_c section:
+
+
+# --- MakeMaker xs_o section:
+
+
+# --- MakeMaker top_targets section:
+all :: pure_all manifypods
+	$(NOECHO) $(NOOP)
+
+pure_all :: config pm_to_blib subdirs linkext
+	$(NOECHO) $(NOOP)
+
+	$(NOECHO) $(NOOP)
+
+subdirs :: $(MYEXTLIB)
+	$(NOECHO) $(NOOP)
+
+config :: $(FIRST_MAKEFILE) blibdirs
+	$(NOECHO) $(NOOP)
+
+help :
+	perldoc ExtUtils::MakeMaker
+
+
+# --- MakeMaker blibdirs section:
+blibdirs : $(INST_LIBDIR)$(DFSEP).exists $(INST_ARCHLIB)$(DFSEP).exists $(INST_AUTODIR)$(DFSEP).exists $(INST_ARCHAUTODIR)$(DFSEP).exists $(INST_BIN)$(DFSEP).exists $(INST_SCRIPT)$(DFSEP).exists $(INST_MAN1DIR)$(DFSEP).exists $(INST_MAN3DIR)$(DFSEP).exists
+	$(NOECHO) $(NOOP)
+
+# Backwards compat with 6.18 through 6.25
+blibdirs.ts : blibdirs
+	$(NOECHO) $(NOOP)
+
+$(INST_LIBDIR)$(DFSEP).exists :: Makefile.PL
+	$(NOECHO) $(MKPATH) $(INST_LIBDIR)
+	$(NOECHO) $(CHMOD) $(PERM_DIR) $(INST_LIBDIR)
+	$(NOECHO) $(TOUCH) $(INST_LIBDIR)$(DFSEP).exists
+
+$(INST_ARCHLIB)$(DFSEP).exists :: Makefile.PL
+	$(NOECHO) $(MKPATH) $(INST_ARCHLIB)
+	$(NOECHO) $(CHMOD) $(PERM_DIR) $(INST_ARCHLIB)
+	$(NOECHO) $(TOUCH) $(INST_ARCHLIB)$(DFSEP).exists
+
+$(INST_AUTODIR)$(DFSEP).exists :: Makefile.PL
+	$(NOECHO) $(MKPATH) $(INST_AUTODIR)
+	$(NOECHO) $(CHMOD) $(PERM_DIR) $(INST_AUTODIR)
+	$(NOECHO) $(TOUCH) $(INST_AUTODIR)$(DFSEP).exists
+
+$(INST_ARCHAUTODIR)$(DFSEP).exists :: Makefile.PL
+	$(NOECHO) $(MKPATH) $(INST_ARCHAUTODIR)
+	$(NOECHO) $(CHMOD) $(PERM_DIR) $(INST_ARCHAUTODIR)
+	$(NOECHO) $(TOUCH) $(INST_ARCHAUTODIR)$(DFSEP).exists
+
+$(INST_BIN)$(DFSEP).exists :: Makefile.PL
+	$(NOECHO) $(MKPATH) $(INST_BIN)
+	$(NOECHO) $(CHMOD) $(PERM_DIR) $(INST_BIN)
+	$(NOECHO) $(TOUCH) $(INST_BIN)$(DFSEP).exists
+
+$(INST_SCRIPT)$(DFSEP).exists :: Makefile.PL
+	$(NOECHO) $(MKPATH) $(INST_SCRIPT)
+	$(NOECHO) $(CHMOD) $(PERM_DIR) $(INST_SCRIPT)
+	$(NOECHO) $(TOUCH) $(INST_SCRIPT)$(DFSEP).exists
+
+$(INST_MAN1DIR)$(DFSEP).exists :: Makefile.PL
+	$(NOECHO) $(MKPATH) $(INST_MAN1DIR)
+	$(NOECHO) $(CHMOD) $(PERM_DIR) $(INST_MAN1DIR)
+	$(NOECHO) $(TOUCH) $(INST_MAN1DIR)$(DFSEP).exists
+
+$(INST_MAN3DIR)$(DFSEP).exists :: Makefile.PL
+	$(NOECHO) $(MKPATH) $(INST_MAN3DIR)
+	$(NOECHO) $(CHMOD) $(PERM_DIR) $(INST_MAN3DIR)
+	$(NOECHO) $(TOUCH) $(INST_MAN3DIR)$(DFSEP).exists
+
+
+
+# --- MakeMaker linkext section:
+
+linkext :: dynamic
+	$(NOECHO) $(NOOP)
+
+
+# --- MakeMaker dlsyms section:
+
+
+# --- MakeMaker dynamic_bs section:
+
+BOOTSTRAP =
+
+
+# --- MakeMaker dynamic section:
+
+dynamic :: $(FIRST_MAKEFILE) config $(INST_BOOT) $(INST_DYNAMIC)
+	$(NOECHO) $(NOOP)
+
+
+# --- MakeMaker dynamic_lib section:
+
+
+# --- MakeMaker static section:
+
+## $(INST_PM) has been moved to the all: target.
+## It remains here for awhile to allow for old usage: "make static"
+static :: $(FIRST_MAKEFILE) $(INST_STATIC)
+	$(NOECHO) $(NOOP)
+
+
+# --- MakeMaker static_lib section:
+
+
+# --- MakeMaker manifypods section:
+
+POD2MAN_EXE = $(PERLRUN) "-MExtUtils::Command::MM" -e pod2man "--"
+POD2MAN = $(POD2MAN_EXE)
+
+
+manifypods : pure_all config  \
+	lib/App/wsgetmail.pm \
+	lib/App/wsgetmail/Handler/RT/Mailgate.pm \
+	lib/App/wsgetmail/MS365.pm \
+	lib/App/wsgetmail/MS365/Client.pm \
+	lib/App/wsgetmail/MS365/Message.pm
+	$(NOECHO) $(POD2MAN) --section=3 --perm_rw=$(PERM_RW) -u \
+	  lib/App/wsgetmail.pm $(INST_MAN3DIR)/App::wsgetmail.$(MAN3EXT) \
+	  lib/App/wsgetmail/Handler/RT/Mailgate.pm $(INST_MAN3DIR)/App::wsgetmail::Handler::RT::Mailgate.$(MAN3EXT) \
+	  lib/App/wsgetmail/MS365.pm $(INST_MAN3DIR)/App::wsgetmail::MS365.$(MAN3EXT) \
+	  lib/App/wsgetmail/MS365/Client.pm $(INST_MAN3DIR)/App::wsgetmail::MS365::Client.$(MAN3EXT) \
+	  lib/App/wsgetmail/MS365/Message.pm $(INST_MAN3DIR)/App::wsgetmail::MS365::Message.$(MAN3EXT) 
+
+
+
+
+# --- MakeMaker processPL section:
+
+
+# --- MakeMaker installbin section:
+
+
+# --- MakeMaker subdirs section:
+
+# none
+
+# --- MakeMaker clean_subdirs section:
+clean_subdirs :
+	$(NOECHO) $(NOOP)
+
+
+# --- MakeMaker clean section:
+
+# Delete temporary files but do not touch installed files. We don't delete
+# the Makefile here so a later make realclean still has a makefile to use.
+
+clean :: clean_subdirs
+	- $(RM_F) \
+	  $(BASEEXT).bso $(BASEEXT).def \
+	  $(BASEEXT).exp $(BASEEXT).x \
+	  $(BOOTSTRAP) $(INST_ARCHAUTODIR)/extralibs.all \
+	  $(INST_ARCHAUTODIR)/extralibs.ld $(MAKE_APERL_FILE) \
+	  *$(LIB_EXT) *$(OBJ_EXT) \
+	  *perl.core MYMETA.json \
+	  MYMETA.yml blibdirs.ts \
+	  core core.*perl.*.? \
+	  core.[0-9] core.[0-9][0-9] \
+	  core.[0-9][0-9][0-9] core.[0-9][0-9][0-9][0-9] \
+	  core.[0-9][0-9][0-9][0-9][0-9] lib$(BASEEXT).def \
+	  mon.out perl \
+	  perl$(EXE_EXT) perl.exe \
+	  perlmain.c pm_to_blib \
+	  pm_to_blib.ts so_locations \
+	  tmon.out 
+	- $(RM_RF) \
+	  blib 
+	  $(NOECHO) $(RM_F) $(MAKEFILE_OLD)
+	- $(MV) $(FIRST_MAKEFILE) $(MAKEFILE_OLD) $(DEV_NULL)
+
+
+# --- MakeMaker realclean_subdirs section:
+# so clean is forced to complete before realclean_subdirs runs
+realclean_subdirs : clean
+	$(NOECHO) $(NOOP)
+
+
+# --- MakeMaker realclean section:
+# Delete temporary files (via clean) and also delete dist files
+realclean purge :: realclean_subdirs
+	- $(RM_F) \
+	  $(FIRST_MAKEFILE) $(MAKEFILE_OLD) 
+	- $(RM_RF) \
+	  $(DISTVNAME) MYMETA.yml 
+
+
+# --- MakeMaker metafile section:
+metafile :
+	$(NOECHO) $(NOOP)
+
+
+# --- MakeMaker signature section:
+signature :
+	cpansign -s
+
+
+# --- MakeMaker dist_basics section:
+distclean :: realclean distcheck
+	$(NOECHO) $(NOOP)
+
+distcheck :
+	$(PERLRUN) "-MExtUtils::Manifest=fullcheck" -e fullcheck
+
+skipcheck :
+	$(PERLRUN) "-MExtUtils::Manifest=skipcheck" -e skipcheck
+
+manifest :
+	$(PERLRUN) "-MExtUtils::Manifest=mkmanifest" -e mkmanifest
+
+veryclean : realclean
+	$(RM_F) *~ */*~ *.orig */*.orig *.bak */*.bak *.old */*.old
+
+
+
+# --- MakeMaker dist_core section:
+
+dist : $(DIST_DEFAULT) $(FIRST_MAKEFILE)
+	$(NOECHO) $(ABSPERLRUN) -l -e 'print '\''Warning: Makefile possibly out of date with $(VERSION_FROM)'\''' \
+	  -e '    if -e '\''$(VERSION_FROM)'\'' and -M '\''$(VERSION_FROM)'\'' < -M '\''$(FIRST_MAKEFILE)'\'';' --
+
+tardist : $(DISTVNAME).tar$(SUFFIX)
+	$(NOECHO) $(NOOP)
+
+uutardist : $(DISTVNAME).tar$(SUFFIX)
+	uuencode $(DISTVNAME).tar$(SUFFIX) $(DISTVNAME).tar$(SUFFIX) > $(DISTVNAME).tar$(SUFFIX)_uu
+	$(NOECHO) $(ECHO) 'Created $(DISTVNAME).tar$(SUFFIX)_uu'
+
+$(DISTVNAME).tar$(SUFFIX) : distdir
+	$(PREOP)
+	$(TO_UNIX)
+	$(TAR) $(TARFLAGS) $(DISTVNAME).tar $(DISTVNAME)
+	$(RM_RF) $(DISTVNAME)
+	$(COMPRESS) $(DISTVNAME).tar
+	$(NOECHO) $(ECHO) 'Created $(DISTVNAME).tar$(SUFFIX)'
+	$(POSTOP)
+
+zipdist : $(DISTVNAME).zip
+	$(NOECHO) $(NOOP)
+
+$(DISTVNAME).zip : distdir
+	$(PREOP)
+	$(ZIP) $(ZIPFLAGS) $(DISTVNAME).zip $(DISTVNAME)
+	$(RM_RF) $(DISTVNAME)
+	$(NOECHO) $(ECHO) 'Created $(DISTVNAME).zip'
+	$(POSTOP)
+
+shdist : distdir
+	$(PREOP)
+	$(SHAR) $(DISTVNAME) > $(DISTVNAME).shar
+	$(RM_RF) $(DISTVNAME)
+	$(NOECHO) $(ECHO) 'Created $(DISTVNAME).shar'
+	$(POSTOP)
+
+
+# --- MakeMaker distdir section:
+create_distdir :
+	$(RM_RF) $(DISTVNAME)
+	$(PERLRUN) "-MExtUtils::Manifest=manicopy,maniread" \
+		-e "manicopy(maniread(),'$(DISTVNAME)', '$(DIST_CP)');"
+
+distdir : create_distdir  distsignature
+	$(NOECHO) $(NOOP)
+
+
+
+# --- MakeMaker dist_test section:
+disttest : distdir
+	cd $(DISTVNAME) && $(ABSPERLRUN) Makefile.PL 
+	cd $(DISTVNAME) && $(MAKE) $(PASTHRU)
+	cd $(DISTVNAME) && $(MAKE) test $(PASTHRU)
+
+
+
+# --- MakeMaker dist_ci section:
+ci :
+	$(ABSPERLRUN) -MExtUtils::Manifest=maniread -e '@all = sort keys %{ maniread() };' \
+	  -e 'print(qq{Executing $(CI) @all\n});' \
+	  -e 'system(qq{$(CI) @all}) == 0 or die $$!;' \
+	  -e 'print(qq{Executing $(RCS_LABEL) ...\n});' \
+	  -e 'system(qq{$(RCS_LABEL) @all}) == 0 or die $$!;' --
+
+
+# --- MakeMaker distmeta section:
+distmeta : create_distdir metafile
+	$(NOECHO) cd $(DISTVNAME) && $(ABSPERLRUN) -MExtUtils::Manifest=maniadd -e 'exit unless -e q{META.yml};' \
+	  -e 'eval { maniadd({q{META.yml} => q{Module YAML meta-data (added by MakeMaker)}}) }' \
+	  -e '    or die "Could not add META.yml to MANIFEST: $${'\''@'\''}"' --
+	$(NOECHO) cd $(DISTVNAME) && $(ABSPERLRUN) -MExtUtils::Manifest=maniadd -e 'exit unless -f q{META.json};' \
+	  -e 'eval { maniadd({q{META.json} => q{Module JSON meta-data (added by MakeMaker)}}) }' \
+	  -e '    or die "Could not add META.json to MANIFEST: $${'\''@'\''}"' --
+
+
+
+# --- MakeMaker distsignature section:
+distsignature : distmeta
+	$(NOECHO) cd $(DISTVNAME) && $(ABSPERLRUN) -MExtUtils::Manifest=maniadd -e 'eval { maniadd({q{SIGNATURE} => q{Public-key signature (added by MakeMaker)}}) }' \
+	  -e '    or die "Could not add SIGNATURE to MANIFEST: $${'\''@'\''}"' --
+	$(NOECHO) cd $(DISTVNAME) && $(TOUCH) SIGNATURE
+	cd $(DISTVNAME) && cpansign -s
+
+
+
+# --- MakeMaker install section:
+
+install :: pure_install doc_install
+	$(NOECHO) $(NOOP)
+
+install_perl :: pure_perl_install doc_perl_install
+	$(NOECHO) $(NOOP)
+
+install_site :: pure_site_install doc_site_install
+	$(NOECHO) $(NOOP)
+
+install_vendor :: pure_vendor_install doc_vendor_install
+	$(NOECHO) $(NOOP)
+
+pure_install :: pure_$(INSTALLDIRS)_install
+	$(NOECHO) $(NOOP)
+
+doc_install :: doc_$(INSTALLDIRS)_install
+	$(NOECHO) $(NOOP)
+
+pure__install : pure_site_install
+	$(NOECHO) $(ECHO) INSTALLDIRS not defined, defaulting to INSTALLDIRS=site
+
+doc__install : doc_site_install
+	$(NOECHO) $(ECHO) INSTALLDIRS not defined, defaulting to INSTALLDIRS=site
+
+pure_perl_install :: all
+	$(NOECHO) $(MOD_INSTALL) \
+		read "$(PERL_ARCHLIB)/auto/$(FULLEXT)/.packlist" \
+		write "$(DESTINSTALLARCHLIB)/auto/$(FULLEXT)/.packlist" \
+		"$(INST_LIB)" "$(DESTINSTALLPRIVLIB)" \
+		"$(INST_ARCHLIB)" "$(DESTINSTALLARCHLIB)" \
+		"$(INST_BIN)" "$(DESTINSTALLBIN)" \
+		"$(INST_SCRIPT)" "$(DESTINSTALLSCRIPT)" \
+		"$(INST_MAN1DIR)" "$(DESTINSTALLMAN1DIR)" \
+		"$(INST_MAN3DIR)" "$(DESTINSTALLMAN3DIR)"
+	$(NOECHO) $(WARN_IF_OLD_PACKLIST) \
+		"$(SITEARCHEXP)/auto/$(FULLEXT)"
+
+
+pure_site_install :: all
+	$(NOECHO) $(MOD_INSTALL) \
+		read "$(SITEARCHEXP)/auto/$(FULLEXT)/.packlist" \
+		write "$(DESTINSTALLSITEARCH)/auto/$(FULLEXT)/.packlist" \
+		"$(INST_LIB)" "$(DESTINSTALLSITELIB)" \
+		"$(INST_ARCHLIB)" "$(DESTINSTALLSITEARCH)" \
+		"$(INST_BIN)" "$(DESTINSTALLSITEBIN)" \
+		"$(INST_SCRIPT)" "$(DESTINSTALLSITESCRIPT)" \
+		"$(INST_MAN1DIR)" "$(DESTINSTALLSITEMAN1DIR)" \
+		"$(INST_MAN3DIR)" "$(DESTINSTALLSITEMAN3DIR)"
+	$(NOECHO) $(WARN_IF_OLD_PACKLIST) \
+		"$(PERL_ARCHLIB)/auto/$(FULLEXT)"
+
+pure_vendor_install :: all
+	$(NOECHO) $(MOD_INSTALL) \
+		read "$(VENDORARCHEXP)/auto/$(FULLEXT)/.packlist" \
+		write "$(DESTINSTALLVENDORARCH)/auto/$(FULLEXT)/.packlist" \
+		"$(INST_LIB)" "$(DESTINSTALLVENDORLIB)" \
+		"$(INST_ARCHLIB)" "$(DESTINSTALLVENDORARCH)" \
+		"$(INST_BIN)" "$(DESTINSTALLVENDORBIN)" \
+		"$(INST_SCRIPT)" "$(DESTINSTALLVENDORSCRIPT)" \
+		"$(INST_MAN1DIR)" "$(DESTINSTALLVENDORMAN1DIR)" \
+		"$(INST_MAN3DIR)" "$(DESTINSTALLVENDORMAN3DIR)"
+
+
+doc_perl_install :: all
+	$(NOECHO) $(ECHO) Appending installation info to "$(DESTINSTALLARCHLIB)/perllocal.pod"
+	-$(NOECHO) $(MKPATH) "$(DESTINSTALLARCHLIB)"
+	-$(NOECHO) $(DOC_INSTALL) \
+		"Module" "$(NAME)" \
+		"installed into" "$(INSTALLPRIVLIB)" \
+		LINKTYPE "$(LINKTYPE)" \
+		VERSION "$(VERSION)" \
+		EXE_FILES "$(EXE_FILES)" \
+		>> "$(DESTINSTALLARCHLIB)/perllocal.pod"
+
+doc_site_install :: all
+	$(NOECHO) $(ECHO) Appending installation info to "$(DESTINSTALLARCHLIB)/perllocal.pod"
+	-$(NOECHO) $(MKPATH) "$(DESTINSTALLARCHLIB)"
+	-$(NOECHO) $(DOC_INSTALL) \
+		"Module" "$(NAME)" \
+		"installed into" "$(INSTALLSITELIB)" \
+		LINKTYPE "$(LINKTYPE)" \
+		VERSION "$(VERSION)" \
+		EXE_FILES "$(EXE_FILES)" \
+		>> "$(DESTINSTALLARCHLIB)/perllocal.pod"
+
+doc_vendor_install :: all
+	$(NOECHO) $(ECHO) Appending installation info to "$(DESTINSTALLARCHLIB)/perllocal.pod"
+	-$(NOECHO) $(MKPATH) "$(DESTINSTALLARCHLIB)"
+	-$(NOECHO) $(DOC_INSTALL) \
+		"Module" "$(NAME)" \
+		"installed into" "$(INSTALLVENDORLIB)" \
+		LINKTYPE "$(LINKTYPE)" \
+		VERSION "$(VERSION)" \
+		EXE_FILES "$(EXE_FILES)" \
+		>> "$(DESTINSTALLARCHLIB)/perllocal.pod"
+
+
+uninstall :: uninstall_from_$(INSTALLDIRS)dirs
+	$(NOECHO) $(NOOP)
+
+uninstall_from_perldirs ::
+	$(NOECHO) $(UNINSTALL) "$(PERL_ARCHLIB)/auto/$(FULLEXT)/.packlist"
+
+uninstall_from_sitedirs ::
+	$(NOECHO) $(UNINSTALL) "$(SITEARCHEXP)/auto/$(FULLEXT)/.packlist"
+
+uninstall_from_vendordirs ::
+	$(NOECHO) $(UNINSTALL) "$(VENDORARCHEXP)/auto/$(FULLEXT)/.packlist"
+
+
+# --- MakeMaker force section:
+# Phony target to force checking subdirectories.
+FORCE :
+	$(NOECHO) $(NOOP)
+
+
+# --- MakeMaker perldepend section:
+
+
+# --- MakeMaker makefile section:
+# We take a very conservative approach here, but it's worth it.
+# We move Makefile to Makefile.old here to avoid gnu make looping.
+$(FIRST_MAKEFILE) : Makefile.PL $(CONFIGDEP)
+	$(NOECHO) $(ECHO) "Makefile out-of-date with respect to $?"
+	$(NOECHO) $(ECHO) "Cleaning current config before rebuilding Makefile..."
+	-$(NOECHO) $(RM_F) $(MAKEFILE_OLD)
+	-$(NOECHO) $(MV)   $(FIRST_MAKEFILE) $(MAKEFILE_OLD)
+	- $(MAKE) $(USEMAKEFILE) $(MAKEFILE_OLD) clean $(DEV_NULL)
+	$(PERLRUN) Makefile.PL 
+	$(NOECHO) $(ECHO) "==> Your Makefile has been rebuilt. <=="
+	$(NOECHO) $(ECHO) "==> Please rerun the $(MAKE) command.  <=="
+	$(FALSE)
+
+
+
+# --- MakeMaker staticmake section:
+
+# --- MakeMaker makeaperl section ---
+MAP_TARGET    = perl
+FULLPERL      = "/Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/bin/perl"
+MAP_PERLINC   = "-Iblib/arch" "-Iblib/lib" "-I/Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/lib/5.26.1/darwin-2level" "-I/Users/craigkaiser/perl5/perlbrew/perls/perl-5.26.1/lib/5.26.1"
+
+$(MAP_TARGET) :: $(MAKE_APERL_FILE)
+	$(MAKE) $(USEMAKEFILE) $(MAKE_APERL_FILE) $@
+
+$(MAKE_APERL_FILE) : static $(FIRST_MAKEFILE) pm_to_blib
+	$(NOECHO) $(ECHO) Writing \"$(MAKE_APERL_FILE)\" for this $(MAP_TARGET)
+	$(NOECHO) $(PERLRUNINST) \
+		Makefile.PL DIR="" \
+		MAKEFILE=$(MAKE_APERL_FILE) LINKTYPE=static \
+		MAKEAPERL=1 NORECURS=1 CCCDLFLAGS=
+
+
+# --- MakeMaker test section:
+TEST_VERBOSE=0
+TEST_TYPE=test_$(LINKTYPE)
+TEST_FILE = test.pl
+TEST_FILES = t/*.t xt/*.t
+TESTDB_SW = -d
+
+testdb :: testdb_$(LINKTYPE)
+	$(NOECHO) $(NOOP)
+
+test :: $(TEST_TYPE)
+	$(NOECHO) $(NOOP)
+
+# Occasionally we may face this degenerate target:
+test_ : test_dynamic
+	$(NOECHO) $(NOOP)
+
+subdirs-test_dynamic :: dynamic pure_all
+
+test_dynamic :: subdirs-test_dynamic
+	PERL_DL_NONLAZY=1 $(FULLPERLRUN) "-MExtUtils::Command::MM" "-MTest::Harness" "-e" "undef *Test::Harness::Switches; test_harness($(TEST_VERBOSE), 'inc', '$(INST_LIB)', '$(INST_ARCHLIB)')" $(TEST_FILES)
+
+testdb_dynamic :: dynamic pure_all
+	PERL_DL_NONLAZY=1 $(FULLPERLRUN) $(TESTDB_SW) "-Iinc" "-I$(INST_LIB)" "-I$(INST_ARCHLIB)" $(TEST_FILE)
+
+subdirs-test_static :: static pure_all
+
+test_static :: subdirs-test_static
+	PERL_DL_NONLAZY=1 $(FULLPERLRUN) "-MExtUtils::Command::MM" "-MTest::Harness" "-e" "undef *Test::Harness::Switches; test_harness($(TEST_VERBOSE), '$(INST_LIB)', '$(INST_ARCHLIB)')" $(TEST_FILES)
+
+testdb_static :: static pure_all
+	PERL_DL_NONLAZY=1 $(FULLPERLRUN) $(TESTDB_SW) "-Iinc" "-I$(INST_LIB)" "-I$(INST_ARCHLIB)" $(TEST_FILE)
+
+
+
+# --- MakeMaker ppd section:
+# Creates a PPD (Perl Package Description) for a binary distribution.
+ppd :
+	$(NOECHO) $(ECHO) '<SOFTPKG NAME="App-wsgetmail" VERSION="0.01">' > App-wsgetmail.ppd
+	$(NOECHO) $(ECHO) '    <ABSTRACT>Fetch mail from the cloud using webservices</ABSTRACT>' >> App-wsgetmail.ppd
+	$(NOECHO) $(ECHO) '    <AUTHOR>Best Practical Solutions, LLC <modules at bestpractical.com></AUTHOR>' >> App-wsgetmail.ppd
+	$(NOECHO) $(ECHO) '    <IMPLEMENTATION>' >> App-wsgetmail.ppd
+	$(NOECHO) $(ECHO) '        <PERLCORE VERSION="5,010001,0,0" />' >> App-wsgetmail.ppd
+	$(NOECHO) $(ECHO) '        <REQUIRE NAME="Azure::AD::ClientCredentials" />' >> App-wsgetmail.ppd
+	$(NOECHO) $(ECHO) '        <REQUIRE NAME="IPC::Run" />' >> App-wsgetmail.ppd
+	$(NOECHO) $(ECHO) '        <REQUIRE NAME="JSON::" />' >> App-wsgetmail.ppd
+	$(NOECHO) $(ECHO) '        <REQUIRE NAME="LWP::UserAgent" VERSION="6.42" />' >> App-wsgetmail.ppd
+	$(NOECHO) $(ECHO) '        <REQUIRE NAME="Moo::" />' >> App-wsgetmail.ppd
+	$(NOECHO) $(ECHO) '        <REQUIRE NAME="Test::LWP::UserAgent" />' >> App-wsgetmail.ppd
+	$(NOECHO) $(ECHO) '        <REQUIRE NAME="Test::More" />' >> App-wsgetmail.ppd
+	$(NOECHO) $(ECHO) '        <REQUIRE NAME="URI::" />' >> App-wsgetmail.ppd
+	$(NOECHO) $(ECHO) '        <REQUIRE NAME="URI::Escape" />' >> App-wsgetmail.ppd
+	$(NOECHO) $(ECHO) '        <ARCHITECTURE NAME="darwin-2level-5.26" />' >> App-wsgetmail.ppd
+	$(NOECHO) $(ECHO) '        <CODEBASE HREF="" />' >> App-wsgetmail.ppd
+	$(NOECHO) $(ECHO) '    </IMPLEMENTATION>' >> App-wsgetmail.ppd
+	$(NOECHO) $(ECHO) '</SOFTPKG>' >> App-wsgetmail.ppd
+
+
+# --- MakeMaker pm_to_blib section:
+
+pm_to_blib : $(FIRST_MAKEFILE) $(TO_INST_PM)
+	$(NOECHO) $(ABSPERLRUN) -MExtUtils::Install -e 'pm_to_blib({@ARGV}, '\''$(INST_LIB)/auto'\'', q[$(PM_FILTER)], '\''$(PERM_DIR)'\'')' -- \
+	  'lib/App/wsgetmail.pm' 'blib/lib/App/wsgetmail.pm' \
+	  'lib/App/wsgetmail/Handler/RT/Mailgate.pm' 'blib/lib/App/wsgetmail/Handler/RT/Mailgate.pm' \
+	  'lib/App/wsgetmail/MS365.pm' 'blib/lib/App/wsgetmail/MS365.pm' \
+	  'lib/App/wsgetmail/MS365/Client.pm' 'blib/lib/App/wsgetmail/MS365/Client.pm' \
+	  'lib/App/wsgetmail/MS365/Message.pm' 'blib/lib/App/wsgetmail/MS365/Message.pm' 
+	$(NOECHO) $(TOUCH) pm_to_blib
+
+
+# --- MakeMaker selfdocument section:
+
+# here so even if top_targets is overridden, these will still be defined
+# gmake will silently still work if any are .PHONY-ed but nmake won't
+
+static ::
+	$(NOECHO) $(NOOP)
+
+dynamic ::
+	$(NOECHO) $(NOOP)
+
+config ::
+	$(NOECHO) $(NOOP)
+
+
+# --- MakeMaker postamble section:
+
+
+# End.
+# Postamble by Module::Install 1.19
+# --- Module::Install::Admin::Makefile section:
+
+realclean purge ::
+	$(RM_F) $(DISTVNAME).tar$(SUFFIX)
+	$(RM_F) MANIFEST.bak _build
+	$(PERL) "-Ilib" "-MModule::Install::Admin" -e "remove_meta()"
+	$(RM_RF) inc
+
+reset :: purge
+
+upload :: test dist
+	cpan-upload -verbose $(DISTVNAME).tar$(SUFFIX)
+
+grok ::
+	perldoc Module::Install
+
+distsign ::
+	cpansign -s
+
+install ::
+	$(NOECHO) $(PERL) -Ilib -I"/Users/craigkaiser/bps/rts/rt5/local/lib" -I"/Users/craigkaiser/bps/rts/rt5/lib" -Iinc -MModule::Install::RTx::Runtime -e"RTxPlugin()"
+	$(NOECHO) $(PERL) -MExtUtils::Install -e "install({q(bin), q($(DESTDIR)/Users/craigkaiser/bps/rts/rt5/local/plugins/App-wsgetmail/bin), q(lib), q($(DESTDIR)/Users/craigkaiser/bps/rts/rt5/local/plugins/App-wsgetmail/lib)})"
+
diff --git a/Makefile.PL b/Makefile.PL
index 389d870..51de10a 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -1,51 +1,32 @@
-use 5.006;
-use strict;
-use warnings;
-use ExtUtils::MakeMaker;
+use inc::Module::Install;
 
-my %WriteMakefileArgs = (
-    NAME             => 'App::wsgetmail',
-    AUTHOR           => q{Aaron Trevena <ast at bestpractical.com>},
-    VERSION_FROM     => 'lib/App/wsgetmail.pm',
-    ABSTRACT_FROM    => 'lib/App/wsgetmail.pm',
-    LICENSE          => 'artistic_2',
-    MIN_PERL_VERSION => '5.006',
-    CONFIGURE_REQUIRES => {
-        'ExtUtils::MakeMaker' => '0',
-    },
-    TEST_REQUIRES => {
-        'Test::More' => '0',
-	'Test::LWP::UserAgent' => 0,
-    },
-    PREREQ_PM => {
-        'Azure::AD::ClientCredentials' => 0,
-        'LWP::UserAgent' => 0,
-        'URI::Escape' => 0,
-	'URI' => 0,
-        'Moo' => 0,
-        'JSON' => -,
-	'IPC::Run => 0'
-    },
-    dist  => { COMPRESS => 'gzip -9f', SUFFIX => 'gz', },
-    clean => { FILES => 'App-wsgetmail-*' },
-);
+RTx 'App-wsgetmail';
+license 'gpl_2';
 
-# Compatibility with old versions of ExtUtils::MakeMaker
-unless (eval { ExtUtils::MakeMaker->VERSION('6.64'); 1 }) {
-    my $test_requires = delete $WriteMakefileArgs{TEST_REQUIRES} || {};
-    @{$WriteMakefileArgs{PREREQ_PM}}{keys %$test_requires} = values %$test_requires;
-}
+requires 'Azure::AD::ClientCredentials';
+requires 'Test::LWP::UserAgent';
+requires 'Test::More';
+requires 'LWP::UserAgent' => '6.42';
+requires 'URI::Escape';
+requires 'URI';
+requires 'Moo';
+requires 'JSON';
+requires 'IPC::Run';
 
-unless (eval { ExtUtils::MakeMaker->VERSION('6.55_03'); 1 }) {
-    my $build_requires = delete $WriteMakefileArgs{BUILD_REQUIRES} || {};
-    @{$WriteMakefileArgs{PREREQ_PM}}{keys %$build_requires} = values %$build_requires;
-}
+use Config;
+my $perl_path = $Config{perlpath};
+$perl_path .= $Config{_exe}
+    if $^O ne 'VMS' and $perl_path !~ m/$Config{_exe}$/i;
 
-delete $WriteMakefileArgs{CONFIGURE_REQUIRES}
-    unless eval { ExtUtils::MakeMaker->VERSION('6.52'); 1 };
-delete $WriteMakefileArgs{MIN_PERL_VERSION}
-    unless eval { ExtUtils::MakeMaker->VERSION('6.48'); 1 };
-delete $WriteMakefileArgs{LICENSE}
-    unless eval { ExtUtils::MakeMaker->VERSION('6.31'); 1 };
+for my $file ( 'bin/wsgetmail365' ) {
+    substitute(
+        {
+            PERL => $perl_path,
+        },
+        { sufix => ".in" },
+        $file,
+    );
+}
 
-WriteMakefile(%WriteMakefileArgs);
+sign;
+WriteAll;
diff --git a/README b/README
index 43c68c3..bc2d036 100644
--- a/README
+++ b/README
@@ -1,47 +1,53 @@
-App-wsgetmail
+NAME
+    App::wsgetmail - Fetch mail from the cloud using webservices
 
-A simple command line application/script to fetch mail from the cloud
-using webservices instead of IMAP and POP.
+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.
+    Configurable to mark fetched mail as read, or to delete it, and with
+    configurable action with the fetched email.
 
-INSTALLATION
+SYNOPSIS
+    wsgetmail365 --configuration path/to/file.json [--debug]
 
-To install this module, run the following commands:
+CONFIGURATION
+    Configuration of the wsgetmail tool needs the following fields specific
+    to the ms365 application: Application (client) ID, Directory (tenant) ID
 
-	perl Makefile.PL
-	make
-	make test
-	make install
+    For access to the email account you need: Account email address Account
+    password Folder (defaults to inbox, currently only one folder is
+    supported)
 
-SUPPORT AND DOCUMENTATION
+    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"
 
-After installing, you can find documentation for this module with the
-perldoc command.
+    example configuration : { "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" }
 
-    perldoc App::wsgetmail
+    an example configuration file is included in the docs/ directory of this
+    package
 
-You can also look for information at:
-
-    RT, CPAN's request tracker (report bugs here)
-        https://rt.cpan.org/NoAuth/Bugs.html?Dist=App-wsgetmail
-
-    AnnoCPAN, Annotated CPAN documentation
-        http://annocpan.org/dist/App-wsgetmail
-
-    CPAN Ratings
-        https://cpanratings.perl.org/d/App-wsgetmail
-
-    Search CPAN
-        https://metacpan.org/release/App-wsgetmail
+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 software is Copyright (c) 2020 by Best Practical Solutions, LLC.
-
-This is free software, licensed under:
+    This is free software, licensed under:
 
-  The Artistic License 2.0 (GPL Compatible)
+    The GNU General Public License, Version 2, June 1991
 
diff --git a/inc/Module/Install.pm b/inc/Module/Install.pm
new file mode 100644
index 0000000..7ba98c2
--- /dev/null
+++ b/inc/Module/Install.pm
@@ -0,0 +1,451 @@
+#line 1
+package Module::Install;
+
+# For any maintainers:
+# The load order for Module::Install is a bit magic.
+# It goes something like this...
+#
+# IF ( host has Module::Install installed, creating author mode ) {
+#     1. Makefile.PL calls "use inc::Module::Install"
+#     2. $INC{inc/Module/Install.pm} set to installed version of inc::Module::Install
+#     3. The installed version of inc::Module::Install loads
+#     4. inc::Module::Install calls "require Module::Install"
+#     5. The ./inc/ version of Module::Install loads
+# } ELSE {
+#     1. Makefile.PL calls "use inc::Module::Install"
+#     2. $INC{inc/Module/Install.pm} set to ./inc/ version of Module::Install
+#     3. The ./inc/ version of Module::Install loads
+# }
+
+use 5.006;
+use strict 'vars';
+use Cwd        ();
+use File::Find ();
+use File::Path ();
+
+use vars qw{$VERSION $MAIN};
+BEGIN {
+	# All Module::Install core packages now require synchronised versions.
+	# This will be used to ensure we don't accidentally load old or
+	# different versions of modules.
+	# This is not enforced yet, but will be some time in the next few
+	# releases once we can make sure it won't clash with custom
+	# Module::Install extensions.
+	$VERSION = '1.19';
+
+	# Storage for the pseudo-singleton
+	$MAIN    = undef;
+
+	*inc::Module::Install::VERSION = *VERSION;
+	@inc::Module::Install::ISA     = __PACKAGE__;
+
+}
+
+sub import {
+	my $class = shift;
+	my $self  = $class->new(@_);
+	my $who   = $self->_caller;
+
+	#-------------------------------------------------------------
+	# all of the following checks should be included in import(),
+	# to allow "eval 'require Module::Install; 1' to test
+	# installation of Module::Install. (RT #51267)
+	#-------------------------------------------------------------
+
+	# Whether or not inc::Module::Install is actually loaded, the
+	# $INC{inc/Module/Install.pm} is what will still get set as long as
+	# the caller loaded module this in the documented manner.
+	# If not set, the caller may NOT have loaded the bundled version, and thus
+	# they may not have a MI version that works with the Makefile.PL. This would
+	# result in false errors or unexpected behaviour. And we don't want that.
+	my $file = join( '/', 'inc', split /::/, __PACKAGE__ ) . '.pm';
+	unless ( $INC{$file} ) { die <<"END_DIE" }
+
+Please invoke ${\__PACKAGE__} with:
+
+	use inc::${\__PACKAGE__};
+
+not:
+
+	use ${\__PACKAGE__};
+
+END_DIE
+
+	# This reportedly fixes a rare Win32 UTC file time issue, but
+	# as this is a non-cross-platform XS module not in the core,
+	# we shouldn't really depend on it. See RT #24194 for detail.
+	# (Also, this module only supports Perl 5.6 and above).
+	eval "use Win32::UTCFileTime" if $^O eq 'MSWin32' && $] >= 5.006;
+
+	# If the script that is loading Module::Install is from the future,
+	# then make will detect this and cause it to re-run over and over
+	# again. This is bad. Rather than taking action to touch it (which
+	# is unreliable on some platforms and requires write permissions)
+	# for now we should catch this and refuse to run.
+	if ( -f $0 ) {
+		my $s = (stat($0))[9];
+
+		# If the modification time is only slightly in the future,
+		# sleep briefly to remove the problem.
+		my $a = $s - time;
+		if ( $a > 0 and $a < 5 ) { sleep 5 }
+
+		# Too far in the future, throw an error.
+		my $t = time;
+		if ( $s > $t ) { die <<"END_DIE" }
+
+Your installer $0 has a modification time in the future ($s > $t).
+
+This is known to create infinite loops in make.
+
+Please correct this, then run $0 again.
+
+END_DIE
+	}
+
+
+	# Build.PL was formerly supported, but no longer is due to excessive
+	# difficulty in implementing every single feature twice.
+	if ( $0 =~ /Build.PL$/i ) { die <<"END_DIE" }
+
+Module::Install no longer supports Build.PL.
+
+It was impossible to maintain duel backends, and has been deprecated.
+
+Please remove all Build.PL files and only use the Makefile.PL installer.
+
+END_DIE
+
+	#-------------------------------------------------------------
+
+	# To save some more typing in Module::Install installers, every...
+	# use inc::Module::Install
+	# ...also acts as an implicit use strict.
+	$^H |= strict::bits(qw(refs subs vars));
+
+	#-------------------------------------------------------------
+
+	unless ( -f $self->{file} ) {
+		foreach my $key (keys %INC) {
+			delete $INC{$key} if $key =~ /Module\/Install/;
+		}
+
+		local $^W;
+		require "$self->{path}/$self->{dispatch}.pm";
+		File::Path::mkpath("$self->{prefix}/$self->{author}");
+		$self->{admin} = "$self->{name}::$self->{dispatch}"->new( _top => $self );
+		$self->{admin}->init;
+		@_ = ($class, _self => $self);
+		goto &{"$self->{name}::import"};
+	}
+
+	local $^W;
+	*{"${who}::AUTOLOAD"} = $self->autoload;
+	$self->preload;
+
+	# Unregister loader and worker packages so subdirs can use them again
+	delete $INC{'inc/Module/Install.pm'};
+	delete $INC{'Module/Install.pm'};
+
+	# Save to the singleton
+	$MAIN = $self;
+
+	return 1;
+}
+
+sub autoload {
+	my $self = shift;
+	my $who  = $self->_caller;
+	my $cwd  = Cwd::getcwd();
+	my $sym  = "${who}::AUTOLOAD";
+	$sym->{$cwd} = sub {
+		my $pwd = Cwd::getcwd();
+		if ( my $code = $sym->{$pwd} ) {
+			# Delegate back to parent dirs
+			goto &$code unless $cwd eq $pwd;
+		}
+		unless ($$sym =~ s/([^:]+)$//) {
+			# XXX: it looks like we can't retrieve the missing function
+			# via $$sym (usually $main::AUTOLOAD) in this case.
+			# I'm still wondering if we should slurp Makefile.PL to
+			# get some context or not ...
+			my ($package, $file, $line) = caller;
+			die <<"EOT";
+Unknown function is found at $file line $line.
+Execution of $file aborted due to runtime errors.
+
+If you're a contributor to a project, you may need to install
+some Module::Install extensions from CPAN (or other repository).
+If you're a user of a module, please contact the author.
+EOT
+		}
+		my $method = $1;
+		if ( uc($method) eq $method ) {
+			# Do nothing
+			return;
+		} elsif ( $method =~ /^_/ and $self->can($method) ) {
+			# Dispatch to the root M:I class
+			return $self->$method(@_);
+		}
+
+		# Dispatch to the appropriate plugin
+		unshift @_, ( $self, $1 );
+		goto &{$self->can('call')};
+	};
+}
+
+sub preload {
+	my $self = shift;
+	unless ( $self->{extensions} ) {
+		$self->load_extensions(
+			"$self->{prefix}/$self->{path}", $self
+		);
+	}
+
+	my @exts = @{$self->{extensions}};
+	unless ( @exts ) {
+		@exts = $self->{admin}->load_all_extensions;
+	}
+
+	my %seen;
+	foreach my $obj ( @exts ) {
+		while (my ($method, $glob) = each %{ref($obj) . '::'}) {
+			next unless $obj->can($method);
+			next if $method =~ /^_/;
+			next if $method eq uc($method);
+			$seen{$method}++;
+		}
+	}
+
+	my $who = $self->_caller;
+	foreach my $name ( sort keys %seen ) {
+		local $^W;
+		*{"${who}::$name"} = sub {
+			${"${who}::AUTOLOAD"} = "${who}::$name";
+			goto &{"${who}::AUTOLOAD"};
+		};
+	}
+}
+
+sub new {
+	my ($class, %args) = @_;
+
+	delete $INC{'FindBin.pm'};
+	{
+		# to suppress the redefine warning
+		local $SIG{__WARN__} = sub {};
+		require FindBin;
+	}
+
+	# ignore the prefix on extension modules built from top level.
+	my $base_path = Cwd::abs_path($FindBin::Bin);
+	unless ( Cwd::abs_path(Cwd::getcwd()) eq $base_path ) {
+		delete $args{prefix};
+	}
+	return $args{_self} if $args{_self};
+
+	$base_path = VMS::Filespec::unixify($base_path) if $^O eq 'VMS';
+
+	$args{dispatch} ||= 'Admin';
+	$args{prefix}   ||= 'inc';
+	$args{author}   ||= ($^O eq 'VMS' ? '_author' : '.author');
+	$args{bundle}   ||= 'inc/BUNDLES';
+	$args{base}     ||= $base_path;
+	$class =~ s/^\Q$args{prefix}\E:://;
+	$args{name}     ||= $class;
+	$args{version}  ||= $class->VERSION;
+	unless ( $args{path} ) {
+		$args{path}  = $args{name};
+		$args{path}  =~ s!::!/!g;
+	}
+	$args{file}     ||= "$args{base}/$args{prefix}/$args{path}.pm";
+	$args{wrote}      = 0;
+
+	bless( \%args, $class );
+}
+
+sub call {
+	my ($self, $method) = @_;
+	my $obj = $self->load($method) or return;
+        splice(@_, 0, 2, $obj);
+	goto &{$obj->can($method)};
+}
+
+sub load {
+	my ($self, $method) = @_;
+
+	$self->load_extensions(
+		"$self->{prefix}/$self->{path}", $self
+	) unless $self->{extensions};
+
+	foreach my $obj (@{$self->{extensions}}) {
+		return $obj if $obj->can($method);
+	}
+
+	my $admin = $self->{admin} or die <<"END_DIE";
+The '$method' method does not exist in the '$self->{prefix}' path!
+Please remove the '$self->{prefix}' directory and run $0 again to load it.
+END_DIE
+
+	my $obj = $admin->load($method, 1);
+	push @{$self->{extensions}}, $obj;
+
+	$obj;
+}
+
+sub load_extensions {
+	my ($self, $path, $top) = @_;
+
+	my $should_reload = 0;
+	unless ( grep { ! ref $_ and lc $_ eq lc $self->{prefix} } @INC ) {
+		unshift @INC, $self->{prefix};
+		$should_reload = 1;
+	}
+
+	foreach my $rv ( $self->find_extensions($path) ) {
+		my ($file, $pkg) = @{$rv};
+		next if $self->{pathnames}{$pkg};
+
+		local $@;
+		my $new = eval { local $^W; require $file; $pkg->can('new') };
+		unless ( $new ) {
+			warn $@ if $@;
+			next;
+		}
+		$self->{pathnames}{$pkg} =
+			$should_reload ? delete $INC{$file} : $INC{$file};
+		push @{$self->{extensions}}, &{$new}($pkg, _top => $top );
+	}
+
+	$self->{extensions} ||= [];
+}
+
+sub find_extensions {
+	my ($self, $path) = @_;
+
+	my @found;
+	File::Find::find( {no_chdir => 1, wanted => sub {
+		my $file = $File::Find::name;
+		return unless $file =~ m!^\Q$path\E/(.+)\.pm\Z!is;
+		my $subpath = $1;
+		return if lc($subpath) eq lc($self->{dispatch});
+
+		$file = "$self->{path}/$subpath.pm";
+		my $pkg = "$self->{name}::$subpath";
+		$pkg =~ s!/!::!g;
+
+		# If we have a mixed-case package name, assume case has been preserved
+		# correctly.  Otherwise, root through the file to locate the case-preserved
+		# version of the package name.
+		if ( $subpath eq lc($subpath) || $subpath eq uc($subpath) ) {
+			my $content = Module::Install::_read($File::Find::name);
+			my $in_pod  = 0;
+			foreach ( split /\n/, $content ) {
+				$in_pod = 1 if /^=\w/;
+				$in_pod = 0 if /^=cut/;
+				next if ($in_pod || /^=cut/);  # skip pod text
+				next if /^\s*#/;               # and comments
+				if ( m/^\s*package\s+($pkg)\s*;/i ) {
+					$pkg = $1;
+					last;
+				}
+			}
+		}
+
+		push @found, [ $file, $pkg ];
+	}}, $path ) if -d $path;
+
+	@found;
+}
+
+
+
+
+
+#####################################################################
+# Common Utility Functions
+
+sub _caller {
+	my $depth = 0;
+	my $call  = caller($depth);
+	while ( $call eq __PACKAGE__ ) {
+		$depth++;
+		$call = caller($depth);
+	}
+	return $call;
+}
+
+sub _read {
+	local *FH;
+	open( FH, '<', $_[0] ) or die "open($_[0]): $!";
+	binmode FH;
+	my $string = do { local $/; <FH> };
+	close FH or die "close($_[0]): $!";
+	return $string;
+}
+
+sub _readperl {
+	my $string = Module::Install::_read($_[0]);
+	$string =~ s/(?:\015{1,2}\012|\015|\012)/\n/sg;
+	$string =~ s/(\n)\n*__(?:DATA|END)__\b.*\z/$1/s;
+	$string =~ s/\n\n=\w+.+?\n\n=cut\b.+?\n+/\n\n/sg;
+	return $string;
+}
+
+sub _readpod {
+	my $string = Module::Install::_read($_[0]);
+	$string =~ s/(?:\015{1,2}\012|\015|\012)/\n/sg;
+	return $string if $_[0] =~ /\.pod\z/;
+	$string =~ s/(^|\n=cut\b.+?\n+)[^=\s].+?\n(\n=\w+|\z)/$1$2/sg;
+	$string =~ s/\n*=pod\b[^\n]*\n+/\n\n/sg;
+	$string =~ s/\n*=cut\b[^\n]*\n+/\n\n/sg;
+	$string =~ s/^\n+//s;
+	return $string;
+}
+
+sub _write {
+	local *FH;
+	open( FH, '>', $_[0] ) or die "open($_[0]): $!";
+	binmode FH;
+	foreach ( 1 .. $#_ ) {
+		print FH $_[$_] or die "print($_[0]): $!";
+	}
+	close FH or die "close($_[0]): $!";
+}
+
+# _version is for processing module versions (eg, 1.03_05) not
+# Perl versions (eg, 5.8.1).
+sub _version {
+	my $s = shift || 0;
+	my $d =()= $s =~ /(\.)/g;
+	if ( $d >= 2 ) {
+		# Normalise multipart versions
+		$s =~ s/(\.)(\d{1,3})/sprintf("$1%03d",$2)/eg;
+	}
+	$s =~ s/^(\d+)\.?//;
+	my $l = $1 || 0;
+	my @v = map {
+		$_ . '0' x (3 - length $_)
+	} $s =~ /(\d{1,3})\D?/g;
+	$l = $l . '.' . join '', @v if @v;
+	return $l + 0;
+}
+
+sub _cmp {
+	_version($_[1]) <=> _version($_[2]);
+}
+
+# Cloned from Params::Util::_CLASS
+sub _CLASS {
+	(
+		defined $_[0]
+		and
+		! ref $_[0]
+		and
+		$_[0] =~ m/^[^\W\d]\w*(?:::\w+)*\z/s
+	) ? $_[0] : undef;
+}
+
+1;
+
+# Copyright 2008 - 2012 Adam Kennedy.
diff --git a/inc/Module/Install/Base.pm b/inc/Module/Install/Base.pm
new file mode 100644
index 0000000..9fa42c2
--- /dev/null
+++ b/inc/Module/Install/Base.pm
@@ -0,0 +1,83 @@
+#line 1
+package Module::Install::Base;
+
+use strict 'vars';
+use vars qw{$VERSION};
+BEGIN {
+	$VERSION = '1.19';
+}
+
+# Suspend handler for "redefined" warnings
+BEGIN {
+	my $w = $SIG{__WARN__};
+	$SIG{__WARN__} = sub { $w };
+}
+
+#line 42
+
+sub new {
+	my $class = shift;
+	unless ( defined &{"${class}::call"} ) {
+		*{"${class}::call"} = sub { shift->_top->call(@_) };
+	}
+	unless ( defined &{"${class}::load"} ) {
+		*{"${class}::load"} = sub { shift->_top->load(@_) };
+	}
+	bless { @_ }, $class;
+}
+
+#line 61
+
+sub AUTOLOAD {
+	local $@;
+	my $func = eval { shift->_top->autoload } or return;
+	goto &$func;
+}
+
+#line 75
+
+sub _top {
+	$_[0]->{_top};
+}
+
+#line 90
+
+sub admin {
+	$_[0]->_top->{admin}
+	or
+	Module::Install::Base::FakeAdmin->new;
+}
+
+#line 106
+
+sub is_admin {
+	! $_[0]->admin->isa('Module::Install::Base::FakeAdmin');
+}
+
+sub DESTROY {}
+
+package Module::Install::Base::FakeAdmin;
+
+use vars qw{$VERSION};
+BEGIN {
+	$VERSION = $Module::Install::Base::VERSION;
+}
+
+my $fake;
+
+sub new {
+	$fake ||= bless(\@_, $_[0]);
+}
+
+sub AUTOLOAD {}
+
+sub DESTROY {}
+
+# Restore warning handler
+BEGIN {
+	$SIG{__WARN__} = $SIG{__WARN__}->();
+}
+
+1;
+
+#line 159
diff --git a/inc/Module/Install/Can.pm b/inc/Module/Install/Can.pm
new file mode 100644
index 0000000..d65c753
--- /dev/null
+++ b/inc/Module/Install/Can.pm
@@ -0,0 +1,163 @@
+#line 1
+package Module::Install::Can;
+
+use strict;
+use Config                ();
+use ExtUtils::MakeMaker   ();
+use Module::Install::Base ();
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+	$VERSION = '1.19';
+	@ISA     = 'Module::Install::Base';
+	$ISCORE  = 1;
+}
+
+# check if we can load some module
+### Upgrade this to not have to load the module if possible
+sub can_use {
+	my ($self, $mod, $ver) = @_;
+	$mod =~ s{::|\\}{/}g;
+	$mod .= '.pm' unless $mod =~ /\.pm$/i;
+
+	my $pkg = $mod;
+	$pkg =~ s{/}{::}g;
+	$pkg =~ s{\.pm$}{}i;
+
+	local $@;
+	eval { require $mod; $pkg->VERSION($ver || 0); 1 };
+}
+
+# Check if we can run some command
+sub can_run {
+	my ($self, $cmd) = @_;
+
+	my $_cmd = $cmd;
+	return $_cmd if (-x $_cmd or $_cmd = MM->maybe_command($_cmd));
+
+	for my $dir ((split /$Config::Config{path_sep}/, $ENV{PATH}), '.') {
+		next if $dir eq '';
+		require File::Spec;
+		my $abs = File::Spec->catfile($dir, $cmd);
+		return $abs if (-x $abs or $abs = MM->maybe_command($abs));
+	}
+
+	return;
+}
+
+# Can our C compiler environment build XS files
+sub can_xs {
+	my $self = shift;
+
+	# Ensure we have the CBuilder module
+	$self->configure_requires( 'ExtUtils::CBuilder' => 0.27 );
+
+	# Do we have the configure_requires checker?
+	local $@;
+	eval "require ExtUtils::CBuilder;";
+	if ( $@ ) {
+		# They don't obey configure_requires, so it is
+		# someone old and delicate. Try to avoid hurting
+		# them by falling back to an older simpler test.
+		return $self->can_cc();
+	}
+
+	# Do we have a working C compiler
+	my $builder = ExtUtils::CBuilder->new(
+		quiet => 1,
+	);
+	unless ( $builder->have_compiler ) {
+		# No working C compiler
+		return 0;
+	}
+
+	# Write a C file representative of what XS becomes
+	require File::Temp;
+	my ( $FH, $tmpfile ) = File::Temp::tempfile(
+		"compilexs-XXXXX",
+		SUFFIX => '.c',
+	);
+	binmode $FH;
+	print $FH <<'END_C';
+#include "EXTERN.h"
+#include "perl.h"
+#include "XSUB.h"
+
+int main(int argc, char **argv) {
+    return 0;
+}
+
+int boot_sanexs() {
+    return 1;
+}
+
+END_C
+	close $FH;
+
+	# Can the C compiler access the same headers XS does
+	my @libs   = ();
+	my $object = undef;
+	eval {
+		local $^W = 0;
+		$object = $builder->compile(
+			source => $tmpfile,
+		);
+		@libs = $builder->link(
+			objects     => $object,
+			module_name => 'sanexs',
+		);
+	};
+	my $result = $@ ? 0 : 1;
+
+	# Clean up all the build files
+	foreach ( $tmpfile, $object, @libs ) {
+		next unless defined $_;
+		1 while unlink;
+	}
+
+	return $result;
+}
+
+# Can we locate a (the) C compiler
+sub can_cc {
+	my $self   = shift;
+
+	if ($^O eq 'VMS') {
+		require ExtUtils::CBuilder;
+		my $builder = ExtUtils::CBuilder->new(
+		quiet => 1,
+		);
+		return $builder->have_compiler;
+	}
+
+	my @chunks = split(/ /, $Config::Config{cc}) or return;
+
+	# $Config{cc} may contain args; try to find out the program part
+	while (@chunks) {
+		return $self->can_run("@chunks") || (pop(@chunks), next);
+	}
+
+	return;
+}
+
+# Fix Cygwin bug on maybe_command();
+if ( $^O eq 'cygwin' ) {
+	require ExtUtils::MM_Cygwin;
+	require ExtUtils::MM_Win32;
+	if ( ! defined(&ExtUtils::MM_Cygwin::maybe_command) ) {
+		*ExtUtils::MM_Cygwin::maybe_command = sub {
+			my ($self, $file) = @_;
+			if ($file =~ m{^/cygdrive/}i and ExtUtils::MM_Win32->can('maybe_command')) {
+				ExtUtils::MM_Win32->maybe_command($file);
+			} else {
+				ExtUtils::MM_Unix->maybe_command($file);
+			}
+		}
+	}
+}
+
+1;
+
+__END__
+
+#line 245
diff --git a/inc/Module/Install/Fetch.pm b/inc/Module/Install/Fetch.pm
new file mode 100644
index 0000000..3072b08
--- /dev/null
+++ b/inc/Module/Install/Fetch.pm
@@ -0,0 +1,93 @@
+#line 1
+package Module::Install::Fetch;
+
+use strict;
+use Module::Install::Base ();
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+	$VERSION = '1.19';
+	@ISA     = 'Module::Install::Base';
+	$ISCORE  = 1;
+}
+
+sub get_file {
+    my ($self, %args) = @_;
+    my ($scheme, $host, $path, $file) =
+        $args{url} =~ m|^(\w+)://([^/]+)(.+)/(.+)| or return;
+
+    if ( $scheme eq 'http' and ! eval { require LWP::Simple; 1 } ) {
+        $args{url} = $args{ftp_url}
+            or (warn("LWP support unavailable!\n"), return);
+        ($scheme, $host, $path, $file) =
+            $args{url} =~ m|^(\w+)://([^/]+)(.+)/(.+)| or return;
+    }
+
+    $|++;
+    print "Fetching '$file' from $host... ";
+
+    unless (eval { require Socket; Socket::inet_aton($host) }) {
+        warn "'$host' resolve failed!\n";
+        return;
+    }
+
+    return unless $scheme eq 'ftp' or $scheme eq 'http';
+
+    require Cwd;
+    my $dir = Cwd::getcwd();
+    chdir $args{local_dir} or return if exists $args{local_dir};
+
+    if (eval { require LWP::Simple; 1 }) {
+        LWP::Simple::mirror($args{url}, $file);
+    }
+    elsif (eval { require Net::FTP; 1 }) { eval {
+        # use Net::FTP to get past firewall
+        my $ftp = Net::FTP->new($host, Passive => 1, Timeout => 600);
+        $ftp->login("anonymous", 'anonymous at example.com');
+        $ftp->cwd($path);
+        $ftp->binary;
+        $ftp->get($file) or (warn("$!\n"), return);
+        $ftp->quit;
+    } }
+    elsif (my $ftp = $self->can_run('ftp')) { eval {
+        # no Net::FTP, fallback to ftp.exe
+        require FileHandle;
+        my $fh = FileHandle->new;
+
+        local $SIG{CHLD} = 'IGNORE';
+        unless ($fh->open("|$ftp -n")) {
+            warn "Couldn't open ftp: $!\n";
+            chdir $dir; return;
+        }
+
+        my @dialog = split(/\n/, <<"END_FTP");
+open $host
+user anonymous anonymous\@example.com
+cd $path
+binary
+get $file $file
+quit
+END_FTP
+        foreach (@dialog) { $fh->print("$_\n") }
+        $fh->close;
+    } }
+    else {
+        warn "No working 'ftp' program available!\n";
+        chdir $dir; return;
+    }
+
+    unless (-f $file) {
+        warn "Fetching failed: $@\n";
+        chdir $dir; return;
+    }
+
+    return if exists $args{size} and -s $file != $args{size};
+    system($args{run}) if exists $args{run};
+    unlink($file) if $args{remove};
+
+    print(((!exists $args{check_for} or -e $args{check_for})
+        ? "done!" : "failed! ($!)"), "\n");
+    chdir $dir; return !$?;
+}
+
+1;
diff --git a/inc/Module/Install/Include.pm b/inc/Module/Install/Include.pm
new file mode 100644
index 0000000..13fdcd0
--- /dev/null
+++ b/inc/Module/Install/Include.pm
@@ -0,0 +1,34 @@
+#line 1
+package Module::Install::Include;
+
+use strict;
+use Module::Install::Base ();
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+	$VERSION = '1.19';
+	@ISA     = 'Module::Install::Base';
+	$ISCORE  = 1;
+}
+
+sub include {
+	shift()->admin->include(@_);
+}
+
+sub include_deps {
+	shift()->admin->include_deps(@_);
+}
+
+sub auto_include {
+	shift()->admin->auto_include(@_);
+}
+
+sub auto_include_deps {
+	shift()->admin->auto_include_deps(@_);
+}
+
+sub auto_include_dependent_dists {
+	shift()->admin->auto_include_dependent_dists(@_);
+}
+
+1;
diff --git a/inc/Module/Install/Makefile.pm b/inc/Module/Install/Makefile.pm
new file mode 100644
index 0000000..13a4464
--- /dev/null
+++ b/inc/Module/Install/Makefile.pm
@@ -0,0 +1,418 @@
+#line 1
+package Module::Install::Makefile;
+
+use strict 'vars';
+use ExtUtils::MakeMaker   ();
+use Module::Install::Base ();
+use Fcntl qw/:flock :seek/;
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+	$VERSION = '1.19';
+	@ISA     = 'Module::Install::Base';
+	$ISCORE  = 1;
+}
+
+sub Makefile { $_[0] }
+
+my %seen = ();
+
+sub prompt {
+	shift;
+
+	# Infinite loop protection
+	my @c = caller();
+	if ( ++$seen{"$c[1]|$c[2]|$_[0]"} > 3 ) {
+		die "Caught an potential prompt infinite loop ($c[1]|$c[2]|$_[0])";
+	}
+
+	# In automated testing or non-interactive session, always use defaults
+	if ( ($ENV{AUTOMATED_TESTING} or -! -t STDIN) and ! $ENV{PERL_MM_USE_DEFAULT} ) {
+		local $ENV{PERL_MM_USE_DEFAULT} = 1;
+		goto &ExtUtils::MakeMaker::prompt;
+	} else {
+		goto &ExtUtils::MakeMaker::prompt;
+	}
+}
+
+# Store a cleaned up version of the MakeMaker version,
+# since we need to behave differently in a variety of
+# ways based on the MM version.
+my $makemaker = eval $ExtUtils::MakeMaker::VERSION;
+
+# If we are passed a param, do a "newer than" comparison.
+# Otherwise, just return the MakeMaker version.
+sub makemaker {
+	( @_ < 2 or $makemaker >= eval($_[1]) ) ? $makemaker : 0
+}
+
+# Ripped from ExtUtils::MakeMaker 6.56, and slightly modified
+# as we only need to know here whether the attribute is an array
+# or a hash or something else (which may or may not be appendable).
+my %makemaker_argtype = (
+ C                  => 'ARRAY',
+ CONFIG             => 'ARRAY',
+# CONFIGURE          => 'CODE', # ignore
+ DIR                => 'ARRAY',
+ DL_FUNCS           => 'HASH',
+ DL_VARS            => 'ARRAY',
+ EXCLUDE_EXT        => 'ARRAY',
+ EXE_FILES          => 'ARRAY',
+ FUNCLIST           => 'ARRAY',
+ H                  => 'ARRAY',
+ IMPORTS            => 'HASH',
+ INCLUDE_EXT        => 'ARRAY',
+ LIBS               => 'ARRAY', # ignore ''
+ MAN1PODS           => 'HASH',
+ MAN3PODS           => 'HASH',
+ META_ADD           => 'HASH',
+ META_MERGE         => 'HASH',
+ PL_FILES           => 'HASH',
+ PM                 => 'HASH',
+ PMLIBDIRS          => 'ARRAY',
+ PMLIBPARENTDIRS    => 'ARRAY',
+ PREREQ_PM          => 'HASH',
+ CONFIGURE_REQUIRES => 'HASH',
+ SKIP               => 'ARRAY',
+ TYPEMAPS           => 'ARRAY',
+ XS                 => 'HASH',
+# VERSION            => ['version',''],  # ignore
+# _KEEP_AFTER_FLUSH  => '',
+
+ clean      => 'HASH',
+ depend     => 'HASH',
+ dist       => 'HASH',
+ dynamic_lib=> 'HASH',
+ linkext    => 'HASH',
+ macro      => 'HASH',
+ postamble  => 'HASH',
+ realclean  => 'HASH',
+ test       => 'HASH',
+ tool_autosplit => 'HASH',
+
+ # special cases where you can use makemaker_append
+ CCFLAGS   => 'APPENDABLE',
+ DEFINE    => 'APPENDABLE',
+ INC       => 'APPENDABLE',
+ LDDLFLAGS => 'APPENDABLE',
+ LDFROM    => 'APPENDABLE',
+);
+
+sub makemaker_args {
+	my ($self, %new_args) = @_;
+	my $args = ( $self->{makemaker_args} ||= {} );
+	foreach my $key (keys %new_args) {
+		if ($makemaker_argtype{$key}) {
+			if ($makemaker_argtype{$key} eq 'ARRAY') {
+				$args->{$key} = [] unless defined $args->{$key};
+				unless (ref $args->{$key} eq 'ARRAY') {
+					$args->{$key} = [$args->{$key}]
+				}
+				push @{$args->{$key}},
+					ref $new_args{$key} eq 'ARRAY'
+						? @{$new_args{$key}}
+						: $new_args{$key};
+			}
+			elsif ($makemaker_argtype{$key} eq 'HASH') {
+				$args->{$key} = {} unless defined $args->{$key};
+				foreach my $skey (keys %{ $new_args{$key} }) {
+					$args->{$key}{$skey} = $new_args{$key}{$skey};
+				}
+			}
+			elsif ($makemaker_argtype{$key} eq 'APPENDABLE') {
+				$self->makemaker_append($key => $new_args{$key});
+			}
+		}
+		else {
+			if (defined $args->{$key}) {
+				warn qq{MakeMaker attribute "$key" is overriden; use "makemaker_append" to append values\n};
+			}
+			$args->{$key} = $new_args{$key};
+		}
+	}
+	return $args;
+}
+
+# For mm args that take multiple space-separated args,
+# append an argument to the current list.
+sub makemaker_append {
+	my $self = shift;
+	my $name = shift;
+	my $args = $self->makemaker_args;
+	$args->{$name} = defined $args->{$name}
+		? join( ' ', $args->{$name}, @_ )
+		: join( ' ', @_ );
+}
+
+sub build_subdirs {
+	my $self    = shift;
+	my $subdirs = $self->makemaker_args->{DIR} ||= [];
+	for my $subdir (@_) {
+		push @$subdirs, $subdir;
+	}
+}
+
+sub clean_files {
+	my $self  = shift;
+	my $clean = $self->makemaker_args->{clean} ||= {};
+	  %$clean = (
+		%$clean,
+		FILES => join ' ', grep { length $_ } ($clean->{FILES} || (), @_),
+	);
+}
+
+sub realclean_files {
+	my $self      = shift;
+	my $realclean = $self->makemaker_args->{realclean} ||= {};
+	  %$realclean = (
+		%$realclean,
+		FILES => join ' ', grep { length $_ } ($realclean->{FILES} || (), @_),
+	);
+}
+
+sub libs {
+	my $self = shift;
+	my $libs = ref $_[0] ? shift : [ shift ];
+	$self->makemaker_args( LIBS => $libs );
+}
+
+sub inc {
+	my $self = shift;
+	$self->makemaker_args( INC => shift );
+}
+
+sub _wanted_t {
+}
+
+sub tests_recursive {
+	my $self = shift;
+	my $dir = shift || 't';
+	unless ( -d $dir ) {
+		die "tests_recursive dir '$dir' does not exist";
+	}
+	my %tests = map { $_ => 1 } split / /, ($self->tests || '');
+	require File::Find;
+	File::Find::find(
+        sub { /\.t$/ and -f $_ and $tests{"$File::Find::dir/*.t"} = 1 },
+        $dir
+    );
+	$self->tests( join ' ', sort keys %tests );
+}
+
+sub write {
+	my $self = shift;
+	die "&Makefile->write() takes no arguments\n" if @_;
+
+	# Check the current Perl version
+	my $perl_version = $self->perl_version;
+	if ( $perl_version ) {
+		eval "use $perl_version; 1"
+			or die "ERROR: perl: Version $] is installed, "
+			. "but we need version >= $perl_version";
+	}
+
+	# Make sure we have a new enough MakeMaker
+	require ExtUtils::MakeMaker;
+
+	if ( $perl_version and $self->_cmp($perl_version, '5.006') >= 0 ) {
+		# This previous attempted to inherit the version of
+		# ExtUtils::MakeMaker in use by the module author, but this
+		# was found to be untenable as some authors build releases
+		# using future dev versions of EU:MM that nobody else has.
+		# Instead, #toolchain suggests we use 6.59 which is the most
+		# stable version on CPAN at time of writing and is, to quote
+		# ribasushi, "not terminally fucked, > and tested enough".
+		# TODO: We will now need to maintain this over time to push
+		# the version up as new versions are released.
+		$self->build_requires(     'ExtUtils::MakeMaker' => 6.59 );
+		$self->configure_requires( 'ExtUtils::MakeMaker' => 6.59 );
+	} else {
+		# Allow legacy-compatibility with 5.005 by depending on the
+		# most recent EU:MM that supported 5.005.
+		$self->build_requires(     'ExtUtils::MakeMaker' => 6.36 );
+		$self->configure_requires( 'ExtUtils::MakeMaker' => 6.36 );
+	}
+
+	# Generate the MakeMaker params
+	my $args = $self->makemaker_args;
+	$args->{DISTNAME} = $self->name;
+	$args->{NAME}     = $self->module_name || $self->name;
+	$args->{NAME}     =~ s/-/::/g;
+	$args->{VERSION}  = $self->version or die <<'EOT';
+ERROR: Can't determine distribution version. Please specify it
+explicitly via 'version' in Makefile.PL, or set a valid $VERSION
+in a module, and provide its file path via 'version_from' (or
+'all_from' if you prefer) in Makefile.PL.
+EOT
+
+	if ( $self->tests ) {
+		my @tests = split ' ', $self->tests;
+		my %seen;
+		$args->{test} = {
+			TESTS => (join ' ', grep {!$seen{$_}++} @tests),
+		};
+    } elsif ( $Module::Install::ExtraTests::use_extratests ) {
+        # Module::Install::ExtraTests doesn't set $self->tests and does its own tests via harness.
+        # So, just ignore our xt tests here.
+	} elsif ( -d 'xt' and ($Module::Install::AUTHOR or $ENV{RELEASE_TESTING}) ) {
+		$args->{test} = {
+			TESTS => join( ' ', map { "$_/*.t" } grep { -d $_ } qw{ t xt } ),
+		};
+	}
+	if ( $] >= 5.005 ) {
+		$args->{ABSTRACT} = $self->abstract;
+		$args->{AUTHOR}   = join ', ', @{$self->author || []};
+	}
+	if ( $self->makemaker(6.10) ) {
+		$args->{NO_META}   = 1;
+		#$args->{NO_MYMETA} = 1;
+	}
+	if ( $self->makemaker(6.17) and $self->sign ) {
+		$args->{SIGN} = 1;
+	}
+	unless ( $self->is_admin ) {
+		delete $args->{SIGN};
+	}
+	if ( $self->makemaker(6.31) and $self->license ) {
+		$args->{LICENSE} = $self->license;
+	}
+
+	my $prereq = ($args->{PREREQ_PM} ||= {});
+	%$prereq = ( %$prereq,
+		map { @$_ } # flatten [module => version]
+		map { @$_ }
+		grep $_,
+		($self->requires)
+	);
+
+	# Remove any reference to perl, PREREQ_PM doesn't support it
+	delete $args->{PREREQ_PM}->{perl};
+
+	# Merge both kinds of requires into BUILD_REQUIRES
+	my $build_prereq = ($args->{BUILD_REQUIRES} ||= {});
+	%$build_prereq = ( %$build_prereq,
+		map { @$_ } # flatten [module => version]
+		map { @$_ }
+		grep $_,
+		($self->configure_requires, $self->build_requires)
+	);
+
+	# Remove any reference to perl, BUILD_REQUIRES doesn't support it
+	delete $args->{BUILD_REQUIRES}->{perl};
+
+	# Delete bundled dists from prereq_pm, add it to Makefile DIR
+	my $subdirs = ($args->{DIR} || []);
+	if ($self->bundles) {
+		my %processed;
+		foreach my $bundle (@{ $self->bundles }) {
+			my ($mod_name, $dist_dir) = @$bundle;
+			delete $prereq->{$mod_name};
+			$dist_dir = File::Basename::basename($dist_dir); # dir for building this module
+			if (not exists $processed{$dist_dir}) {
+				if (-d $dist_dir) {
+					# List as sub-directory to be processed by make
+					push @$subdirs, $dist_dir;
+				}
+				# Else do nothing: the module is already present on the system
+				$processed{$dist_dir} = undef;
+			}
+		}
+	}
+
+	unless ( $self->makemaker('6.55_03') ) {
+		%$prereq = (%$prereq,%$build_prereq);
+		delete $args->{BUILD_REQUIRES};
+	}
+
+	if ( my $perl_version = $self->perl_version ) {
+		eval "use $perl_version; 1"
+			or die "ERROR: perl: Version $] is installed, "
+			. "but we need version >= $perl_version";
+
+		if ( $self->makemaker(6.48) ) {
+			$args->{MIN_PERL_VERSION} = $perl_version;
+		}
+	}
+
+	if ($self->installdirs) {
+		warn qq{old INSTALLDIRS (probably set by makemaker_args) is overriden by installdirs\n} if $args->{INSTALLDIRS};
+		$args->{INSTALLDIRS} = $self->installdirs;
+	}
+
+	my %args = map {
+		( $_ => $args->{$_} ) } grep {defined($args->{$_} )
+	} keys %$args;
+
+	my $user_preop = delete $args{dist}->{PREOP};
+	if ( my $preop = $self->admin->preop($user_preop) ) {
+		foreach my $key ( keys %$preop ) {
+			$args{dist}->{$key} = $preop->{$key};
+		}
+	}
+
+	my $mm = ExtUtils::MakeMaker::WriteMakefile(%args);
+	$self->fix_up_makefile($mm->{FIRST_MAKEFILE} || 'Makefile');
+}
+
+sub fix_up_makefile {
+	my $self          = shift;
+	my $makefile_name = shift;
+	my $top_class     = ref($self->_top) || '';
+	my $top_version   = $self->_top->VERSION || '';
+
+	my $preamble = $self->preamble
+		? "# Preamble by $top_class $top_version\n"
+			. $self->preamble
+		: '';
+	my $postamble = "# Postamble by $top_class $top_version\n"
+		. ($self->postamble || '');
+
+	local *MAKEFILE;
+	open MAKEFILE, "+< $makefile_name" or die "fix_up_makefile: Couldn't open $makefile_name: $!";
+	eval { flock MAKEFILE, LOCK_EX };
+	my $makefile = do { local $/; <MAKEFILE> };
+
+	$makefile =~ s/\b(test_harness\(\$\(TEST_VERBOSE\), )/$1'inc', /;
+	$makefile =~ s/( -I\$\(INST_ARCHLIB\))/ -Iinc$1/g;
+	$makefile =~ s/( "-I\$\(INST_LIB\)")/ "-Iinc"$1/g;
+	$makefile =~ s/^(FULLPERL = .*)/$1 "-Iinc"/m;
+	$makefile =~ s/^(PERL = .*)/$1 "-Iinc"/m;
+
+	# Module::Install will never be used to build the Core Perl
+	# Sometimes PERL_LIB and PERL_ARCHLIB get written anyway, which breaks
+	# PREFIX/PERL5LIB, and thus, install_share. Blank them if they exist
+	$makefile =~ s/^PERL_LIB = .+/PERL_LIB =/m;
+	#$makefile =~ s/^PERL_ARCHLIB = .+/PERL_ARCHLIB =/m;
+
+	# Perl 5.005 mentions PERL_LIB explicitly, so we have to remove that as well.
+	$makefile =~ s/(\"?)-I\$\(PERL_LIB\)\1//g;
+
+	# XXX - This is currently unused; not sure if it breaks other MM-users
+	# $makefile =~ s/^pm_to_blib\s+:\s+/pm_to_blib :: /mg;
+
+	seek MAKEFILE, 0, SEEK_SET;
+	truncate MAKEFILE, 0;
+	print MAKEFILE  "$preamble$makefile$postamble" or die $!;
+	close MAKEFILE  or die $!;
+
+	1;
+}
+
+sub preamble {
+	my ($self, $text) = @_;
+	$self->{preamble} = $text . $self->{preamble} if defined $text;
+	$self->{preamble};
+}
+
+sub postamble {
+	my ($self, $text) = @_;
+	$self->{postamble} ||= $self->admin->postamble;
+	$self->{postamble} .= $text if defined $text;
+	$self->{postamble}
+}
+
+1;
+
+__END__
+
+#line 544
diff --git a/inc/Module/Install/Metadata.pm b/inc/Module/Install/Metadata.pm
new file mode 100644
index 0000000..11bf971
--- /dev/null
+++ b/inc/Module/Install/Metadata.pm
@@ -0,0 +1,722 @@
+#line 1
+package Module::Install::Metadata;
+
+use strict 'vars';
+use Module::Install::Base ();
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+	$VERSION = '1.19';
+	@ISA     = 'Module::Install::Base';
+	$ISCORE  = 1;
+}
+
+my @boolean_keys = qw{
+	sign
+};
+
+my @scalar_keys = qw{
+	name
+	module_name
+	abstract
+	version
+	distribution_type
+	tests
+	installdirs
+};
+
+my @tuple_keys = qw{
+	configure_requires
+	build_requires
+	requires
+	recommends
+	bundles
+	resources
+};
+
+my @resource_keys = qw{
+	homepage
+	bugtracker
+	repository
+};
+
+my @array_keys = qw{
+	keywords
+	author
+};
+
+*authors = \&author;
+
+sub Meta              { shift          }
+sub Meta_BooleanKeys  { @boolean_keys  }
+sub Meta_ScalarKeys   { @scalar_keys   }
+sub Meta_TupleKeys    { @tuple_keys    }
+sub Meta_ResourceKeys { @resource_keys }
+sub Meta_ArrayKeys    { @array_keys    }
+
+foreach my $key ( @boolean_keys ) {
+	*$key = sub {
+		my $self = shift;
+		if ( defined wantarray and not @_ ) {
+			return $self->{values}->{$key};
+		}
+		$self->{values}->{$key} = ( @_ ? $_[0] : 1 );
+		return $self;
+	};
+}
+
+foreach my $key ( @scalar_keys ) {
+	*$key = sub {
+		my $self = shift;
+		return $self->{values}->{$key} if defined wantarray and !@_;
+		$self->{values}->{$key} = shift;
+		return $self;
+	};
+}
+
+foreach my $key ( @array_keys ) {
+	*$key = sub {
+		my $self = shift;
+		return $self->{values}->{$key} if defined wantarray and !@_;
+		$self->{values}->{$key} ||= [];
+		push @{$self->{values}->{$key}}, @_;
+		return $self;
+	};
+}
+
+foreach my $key ( @resource_keys ) {
+	*$key = sub {
+		my $self = shift;
+		unless ( @_ ) {
+			return () unless $self->{values}->{resources};
+			return map  { $_->[1] }
+			       grep { $_->[0] eq $key }
+			       @{ $self->{values}->{resources} };
+		}
+		return $self->{values}->{resources}->{$key} unless @_;
+		my $uri = shift or die(
+			"Did not provide a value to $key()"
+		);
+		$self->resources( $key => $uri );
+		return 1;
+	};
+}
+
+foreach my $key ( grep { $_ ne "resources" } @tuple_keys) {
+	*$key = sub {
+		my $self = shift;
+		return $self->{values}->{$key} unless @_;
+		my @added;
+		while ( @_ ) {
+			my $module  = shift or last;
+			my $version = shift || 0;
+			push @added, [ $module, $version ];
+		}
+		push @{ $self->{values}->{$key} }, @added;
+		return map {@$_} @added;
+	};
+}
+
+# Resource handling
+my %lc_resource = map { $_ => 1 } qw{
+	homepage
+	license
+	bugtracker
+	repository
+};
+
+sub resources {
+	my $self = shift;
+	while ( @_ ) {
+		my $name  = shift or last;
+		my $value = shift or next;
+		if ( $name eq lc $name and ! $lc_resource{$name} ) {
+			die("Unsupported reserved lowercase resource '$name'");
+		}
+		$self->{values}->{resources} ||= [];
+		push @{ $self->{values}->{resources} }, [ $name, $value ];
+	}
+	$self->{values}->{resources};
+}
+
+# Aliases for build_requires that will have alternative
+# meanings in some future version of META.yml.
+sub test_requires     { shift->build_requires(@_) }
+sub install_requires  { shift->build_requires(@_) }
+
+# Aliases for installdirs options
+sub install_as_core   { $_[0]->installdirs('perl')   }
+sub install_as_cpan   { $_[0]->installdirs('site')   }
+sub install_as_site   { $_[0]->installdirs('site')   }
+sub install_as_vendor { $_[0]->installdirs('vendor') }
+
+sub dynamic_config {
+	my $self  = shift;
+	my $value = @_ ? shift : 1;
+	if ( $self->{values}->{dynamic_config} ) {
+		# Once dynamic we never change to static, for safety
+		return 0;
+	}
+	$self->{values}->{dynamic_config} = $value ? 1 : 0;
+	return 1;
+}
+
+# Convenience command
+sub static_config {
+	shift->dynamic_config(0);
+}
+
+sub perl_version {
+	my $self = shift;
+	return $self->{values}->{perl_version} unless @_;
+	my $version = shift or die(
+		"Did not provide a value to perl_version()"
+	);
+
+	# Normalize the version
+	$version = $self->_perl_version($version);
+
+	# We don't support the really old versions
+	unless ( $version >= 5.005 ) {
+		die "Module::Install only supports 5.005 or newer (use ExtUtils::MakeMaker)\n";
+	}
+
+	$self->{values}->{perl_version} = $version;
+}
+
+sub all_from {
+	my ( $self, $file ) = @_;
+
+	unless ( defined($file) ) {
+		my $name = $self->name or die(
+			"all_from called with no args without setting name() first"
+		);
+		$file = join('/', 'lib', split(/-/, $name)) . '.pm';
+		$file =~ s{.*/}{} unless -e $file;
+		unless ( -e $file ) {
+			die("all_from cannot find $file from $name");
+		}
+	}
+	unless ( -f $file ) {
+		die("The path '$file' does not exist, or is not a file");
+	}
+
+	$self->{values}{all_from} = $file;
+
+	# Some methods pull from POD instead of code.
+	# If there is a matching .pod, use that instead
+	my $pod = $file;
+	$pod =~ s/\.pm$/.pod/i;
+	$pod = $file unless -e $pod;
+
+	# Pull the different values
+	$self->name_from($file)         unless $self->name;
+	$self->version_from($file)      unless $self->version;
+	$self->perl_version_from($file) unless $self->perl_version;
+	$self->author_from($pod)        unless @{$self->author || []};
+	$self->license_from($pod)       unless $self->license;
+	$self->abstract_from($pod)      unless $self->abstract;
+
+	return 1;
+}
+
+sub provides {
+	my $self     = shift;
+	my $provides = ( $self->{values}->{provides} ||= {} );
+	%$provides = (%$provides, @_) if @_;
+	return $provides;
+}
+
+sub auto_provides {
+	my $self = shift;
+	return $self unless $self->is_admin;
+	unless (-e 'MANIFEST') {
+		warn "Cannot deduce auto_provides without a MANIFEST, skipping\n";
+		return $self;
+	}
+	# Avoid spurious warnings as we are not checking manifest here.
+	local $SIG{__WARN__} = sub {1};
+	require ExtUtils::Manifest;
+	local *ExtUtils::Manifest::manicheck = sub { return };
+
+	require Module::Build;
+	my $build = Module::Build->new(
+		dist_name    => $self->name,
+		dist_version => $self->version,
+		license      => $self->license,
+	);
+	$self->provides( %{ $build->find_dist_packages || {} } );
+}
+
+sub feature {
+	my $self     = shift;
+	my $name     = shift;
+	my $features = ( $self->{values}->{features} ||= [] );
+	my $mods;
+
+	if ( @_ == 1 and ref( $_[0] ) ) {
+		# The user used ->feature like ->features by passing in the second
+		# argument as a reference.  Accomodate for that.
+		$mods = $_[0];
+	} else {
+		$mods = \@_;
+	}
+
+	my $count = 0;
+	push @$features, (
+		$name => [
+			map {
+				ref($_) ? ( ref($_) eq 'HASH' ) ? %$_ : @$_ : $_
+			} @$mods
+		]
+	);
+
+	return @$features;
+}
+
+sub features {
+	my $self = shift;
+	while ( my ( $name, $mods ) = splice( @_, 0, 2 ) ) {
+		$self->feature( $name, @$mods );
+	}
+	return $self->{values}->{features}
+		? @{ $self->{values}->{features} }
+		: ();
+}
+
+sub no_index {
+	my $self = shift;
+	my $type = shift;
+	push @{ $self->{values}->{no_index}->{$type} }, @_ if $type;
+	return $self->{values}->{no_index};
+}
+
+sub read {
+	my $self = shift;
+	$self->include_deps( 'YAML::Tiny', 0 );
+
+	require YAML::Tiny;
+	my $data = YAML::Tiny::LoadFile('META.yml');
+
+	# Call methods explicitly in case user has already set some values.
+	while ( my ( $key, $value ) = each %$data ) {
+		next unless $self->can($key);
+		if ( ref $value eq 'HASH' ) {
+			while ( my ( $module, $version ) = each %$value ) {
+				$self->can($key)->($self, $module => $version );
+			}
+		} else {
+			$self->can($key)->($self, $value);
+		}
+	}
+	return $self;
+}
+
+sub write {
+	my $self = shift;
+	return $self unless $self->is_admin;
+	$self->admin->write_meta;
+	return $self;
+}
+
+sub version_from {
+	require ExtUtils::MM_Unix;
+	my ( $self, $file ) = @_;
+	$self->version( ExtUtils::MM_Unix->parse_version($file) );
+
+	# for version integrity check
+	$self->makemaker_args( VERSION_FROM => $file );
+}
+
+sub abstract_from {
+	require ExtUtils::MM_Unix;
+	my ( $self, $file ) = @_;
+	$self->abstract(
+		bless(
+			{ DISTNAME => $self->name },
+			'ExtUtils::MM_Unix'
+		)->parse_abstract($file)
+	);
+}
+
+# Add both distribution and module name
+sub name_from {
+	my ($self, $file) = @_;
+	if (
+		Module::Install::_read($file) =~ m/
+		^ \s*
+		package \s*
+		([\w:]+)
+		[\s|;]*
+		/ixms
+	) {
+		my ($name, $module_name) = ($1, $1);
+		$name =~ s{::}{-}g;
+		$self->name($name);
+		unless ( $self->module_name ) {
+			$self->module_name($module_name);
+		}
+	} else {
+		die("Cannot determine name from $file\n");
+	}
+}
+
+sub _extract_perl_version {
+	if (
+		$_[0] =~ m/
+		^\s*
+		(?:use|require) \s*
+		v?
+		([\d_\.]+)
+		\s* ;
+		/ixms
+	) {
+		my $perl_version = $1;
+		$perl_version =~ s{_}{}g;
+		return $perl_version;
+	} else {
+		return;
+	}
+}
+
+sub perl_version_from {
+	my $self = shift;
+	my $perl_version=_extract_perl_version(Module::Install::_read($_[0]));
+	if ($perl_version) {
+		$self->perl_version($perl_version);
+	} else {
+		warn "Cannot determine perl version info from $_[0]\n";
+		return;
+	}
+}
+
+sub author_from {
+	my $self    = shift;
+	my $content = Module::Install::_read($_[0]);
+	if ($content =~ m/
+		=head \d \s+ (?:authors?)\b \s*
+		([^\n]*)
+		|
+		=head \d \s+ (?:licen[cs]e|licensing|copyright|legal)\b \s*
+		.*? copyright .*? \d\d\d[\d.]+ \s* (?:\bby\b)? \s*
+		([^\n]*)
+	/ixms) {
+		my $author = $1 || $2;
+
+		# XXX: ugly but should work anyway...
+		if (eval "require Pod::Escapes; 1") {
+			# Pod::Escapes has a mapping table.
+			# It's in core of perl >= 5.9.3, and should be installed
+			# as one of the Pod::Simple's prereqs, which is a prereq
+			# of Pod::Text 3.x (see also below).
+			$author =~ s{ E<( (\d+) | ([A-Za-z]+) )> }
+			{
+				defined $2
+				? chr($2)
+				: defined $Pod::Escapes::Name2character_number{$1}
+				? chr($Pod::Escapes::Name2character_number{$1})
+				: do {
+					warn "Unknown escape: E<$1>";
+					"E<$1>";
+				};
+			}gex;
+		}
+		elsif (eval "require Pod::Text; 1" && $Pod::Text::VERSION < 3) {
+			# Pod::Text < 3.0 has yet another mapping table,
+			# though the table name of 2.x and 1.x are different.
+			# (1.x is in core of Perl < 5.6, 2.x is in core of
+			# Perl < 5.9.3)
+			my $mapping = ($Pod::Text::VERSION < 2)
+				? \%Pod::Text::HTML_Escapes
+				: \%Pod::Text::ESCAPES;
+			$author =~ s{ E<( (\d+) | ([A-Za-z]+) )> }
+			{
+				defined $2
+				? chr($2)
+				: defined $mapping->{$1}
+				? $mapping->{$1}
+				: do {
+					warn "Unknown escape: E<$1>";
+					"E<$1>";
+				};
+			}gex;
+		}
+		else {
+			$author =~ s{E<lt>}{<}g;
+			$author =~ s{E<gt>}{>}g;
+		}
+		$self->author($author);
+	} else {
+		warn "Cannot determine author info from $_[0]\n";
+	}
+}
+
+#Stolen from M::B
+my %license_urls = (
+    perl         => 'http://dev.perl.org/licenses/',
+    apache       => 'http://apache.org/licenses/LICENSE-2.0',
+    apache_1_1   => 'http://apache.org/licenses/LICENSE-1.1',
+    artistic     => 'http://opensource.org/licenses/artistic-license.php',
+    artistic_2   => 'http://opensource.org/licenses/artistic-license-2.0.php',
+    lgpl         => 'http://opensource.org/licenses/lgpl-license.php',
+    lgpl2        => 'http://opensource.org/licenses/lgpl-2.1.php',
+    lgpl3        => 'http://opensource.org/licenses/lgpl-3.0.html',
+    bsd          => 'http://opensource.org/licenses/bsd-license.php',
+    gpl          => 'http://opensource.org/licenses/gpl-license.php',
+    gpl2         => 'http://opensource.org/licenses/gpl-2.0.php',
+    gpl3         => 'http://opensource.org/licenses/gpl-3.0.html',
+    mit          => 'http://opensource.org/licenses/mit-license.php',
+    mozilla      => 'http://opensource.org/licenses/mozilla1.1.php',
+    open_source  => undef,
+    unrestricted => undef,
+    restrictive  => undef,
+    unknown      => undef,
+);
+
+sub license {
+	my $self = shift;
+	return $self->{values}->{license} unless @_;
+	my $license = shift or die(
+		'Did not provide a value to license()'
+	);
+	$license = __extract_license($license) || lc $license;
+	$self->{values}->{license} = $license;
+
+	# Automatically fill in license URLs
+	if ( $license_urls{$license} ) {
+		$self->resources( license => $license_urls{$license} );
+	}
+
+	return 1;
+}
+
+sub _extract_license {
+	my $pod = shift;
+	my $matched;
+	return __extract_license(
+		($matched) = $pod =~ m/
+			(=head \d \s+ L(?i:ICEN[CS]E|ICENSING)\b.*?)
+			(=head \d.*|=cut.*|)\z
+		/xms
+	) || __extract_license(
+		($matched) = $pod =~ m/
+			(=head \d \s+ (?:C(?i:OPYRIGHTS?)|L(?i:EGAL))\b.*?)
+			(=head \d.*|=cut.*|)\z
+		/xms
+	);
+}
+
+sub __extract_license {
+	my $license_text = shift or return;
+	my @phrases      = (
+		'(?:under )?the same (?:terms|license) as (?:perl|the perl (?:\d )?programming language)' => 'perl', 1,
+		'(?:under )?the terms of (?:perl|the perl programming language) itself' => 'perl', 1,
+		'Artistic and GPL'                   => 'perl',         1,
+		'GNU general public license'         => 'gpl',          1,
+		'GNU public license'                 => 'gpl',          1,
+		'GNU lesser general public license'  => 'lgpl',         1,
+		'GNU lesser public license'          => 'lgpl',         1,
+		'GNU library general public license' => 'lgpl',         1,
+		'GNU library public license'         => 'lgpl',         1,
+		'GNU Free Documentation license'     => 'unrestricted', 1,
+		'GNU Affero General Public License'  => 'open_source',  1,
+		'(?:Free)?BSD license'               => 'bsd',          1,
+		'Artistic license 2\.0'              => 'artistic_2',   1,
+		'Artistic license'                   => 'artistic',     1,
+		'Apache (?:Software )?license'       => 'apache',       1,
+		'GPL'                                => 'gpl',          1,
+		'LGPL'                               => 'lgpl',         1,
+		'BSD'                                => 'bsd',          1,
+		'Artistic'                           => 'artistic',     1,
+		'MIT'                                => 'mit',          1,
+		'Mozilla Public License'             => 'mozilla',      1,
+		'Q Public License'                   => 'open_source',  1,
+		'OpenSSL License'                    => 'unrestricted', 1,
+		'SSLeay License'                     => 'unrestricted', 1,
+		'zlib License'                       => 'open_source',  1,
+		'proprietary'                        => 'proprietary',  0,
+	);
+	while ( my ($pattern, $license, $osi) = splice(@phrases, 0, 3) ) {
+		$pattern =~ s#\s+#\\s+#gs;
+		if ( $license_text =~ /\b$pattern\b/i ) {
+			return $license;
+		}
+	}
+	return '';
+}
+
+sub license_from {
+	my $self = shift;
+	if (my $license=_extract_license(Module::Install::_read($_[0]))) {
+		$self->license($license);
+	} else {
+		warn "Cannot determine license info from $_[0]\n";
+		return 'unknown';
+	}
+}
+
+sub _extract_bugtracker {
+	my @links   = $_[0] =~ m#L<(
+	 https?\Q://rt.cpan.org/\E[^>]+|
+	 https?\Q://github.com/\E[\w_]+/[\w_]+/issues|
+	 https?\Q://code.google.com/p/\E[\w_\-]+/issues/list
+	 )>#gx;
+	my %links;
+	@links{@links}=();
+	@links=keys %links;
+	return @links;
+}
+
+sub bugtracker_from {
+	my $self    = shift;
+	my $content = Module::Install::_read($_[0]);
+	my @links   = _extract_bugtracker($content);
+	unless ( @links ) {
+		warn "Cannot determine bugtracker info from $_[0]\n";
+		return 0;
+	}
+	if ( @links > 1 ) {
+		warn "Found more than one bugtracker link in $_[0]\n";
+		return 0;
+	}
+
+	# Set the bugtracker
+	bugtracker( $links[0] );
+	return 1;
+}
+
+sub requires_from {
+	my $self     = shift;
+	my $content  = Module::Install::_readperl($_[0]);
+	my @requires = $content =~ m/^use\s+([^\W\d]\w*(?:::\w+)*)\s+(v?[\d\.]+)/mg;
+	while ( @requires ) {
+		my $module  = shift @requires;
+		my $version = shift @requires;
+		$self->requires( $module => $version );
+	}
+}
+
+sub test_requires_from {
+	my $self     = shift;
+	my $content  = Module::Install::_readperl($_[0]);
+	my @requires = $content =~ m/^use\s+([^\W\d]\w*(?:::\w+)*)\s+([\d\.]+)/mg;
+	while ( @requires ) {
+		my $module  = shift @requires;
+		my $version = shift @requires;
+		$self->test_requires( $module => $version );
+	}
+}
+
+# Convert triple-part versions (eg, 5.6.1 or 5.8.9) to
+# numbers (eg, 5.006001 or 5.008009).
+# Also, convert double-part versions (eg, 5.8)
+sub _perl_version {
+	my $v = $_[-1];
+	$v =~ s/^([1-9])\.([1-9]\d?\d?)$/sprintf("%d.%03d",$1,$2)/e;
+	$v =~ s/^([1-9])\.([1-9]\d?\d?)\.(0|[1-9]\d?\d?)$/sprintf("%d.%03d%03d",$1,$2,$3 || 0)/e;
+	$v =~ s/(\.\d\d\d)000$/$1/;
+	$v =~ s/_.+$//;
+	if ( ref($v) ) {
+		# Numify
+		$v = $v + 0;
+	}
+	return $v;
+}
+
+sub add_metadata {
+    my $self = shift;
+    my %hash = @_;
+    for my $key (keys %hash) {
+        warn "add_metadata: $key is not prefixed with 'x_'.\n" .
+             "Use appopriate function to add non-private metadata.\n" unless $key =~ /^x_/;
+        $self->{values}->{$key} = $hash{$key};
+    }
+}
+
+
+######################################################################
+# MYMETA Support
+
+sub WriteMyMeta {
+	die "WriteMyMeta has been deprecated";
+}
+
+sub write_mymeta_yaml {
+	my $self = shift;
+
+	# We need YAML::Tiny to write the MYMETA.yml file
+	unless ( eval { require YAML::Tiny; 1; } ) {
+		return 1;
+	}
+
+	# Generate the data
+	my $meta = $self->_write_mymeta_data or return 1;
+
+	# Save as the MYMETA.yml file
+	print "Writing MYMETA.yml\n";
+	YAML::Tiny::DumpFile('MYMETA.yml', $meta);
+}
+
+sub write_mymeta_json {
+	my $self = shift;
+
+	# We need JSON to write the MYMETA.json file
+	unless ( eval { require JSON; 1; } ) {
+		return 1;
+	}
+
+	# Generate the data
+	my $meta = $self->_write_mymeta_data or return 1;
+
+	# Save as the MYMETA.yml file
+	print "Writing MYMETA.json\n";
+	Module::Install::_write(
+		'MYMETA.json',
+		JSON->new->pretty(1)->canonical->encode($meta),
+	);
+}
+
+sub _write_mymeta_data {
+	my $self = shift;
+
+	# If there's no existing META.yml there is nothing we can do
+	return undef unless -f 'META.yml';
+
+	# We need Parse::CPAN::Meta to load the file
+	unless ( eval { require Parse::CPAN::Meta; 1; } ) {
+		return undef;
+	}
+
+	# Merge the perl version into the dependencies
+	my $val  = $self->Meta->{values};
+	my $perl = delete $val->{perl_version};
+	if ( $perl ) {
+		$val->{requires} ||= [];
+		my $requires = $val->{requires};
+
+		# Canonize to three-dot version after Perl 5.6
+		if ( $perl >= 5.006 ) {
+			$perl =~ s{^(\d+)\.(\d\d\d)(\d*)}{join('.', $1, int($2||0), int($3||0))}e
+		}
+		unshift @$requires, [ perl => $perl ];
+	}
+
+	# Load the advisory META.yml file
+	my @yaml = Parse::CPAN::Meta::LoadFile('META.yml');
+	my $meta = $yaml[0];
+
+	# Overwrite the non-configure dependency hashes
+	delete $meta->{requires};
+	delete $meta->{build_requires};
+	delete $meta->{recommends};
+	if ( exists $val->{requires} ) {
+		$meta->{requires} = { map { @$_ } @{ $val->{requires} } };
+	}
+	if ( exists $val->{build_requires} ) {
+		$meta->{build_requires} = { map { @$_ } @{ $val->{build_requires} } };
+	}
+
+	return $meta;
+}
+
+1;
diff --git a/inc/Module/Install/RTx.pm b/inc/Module/Install/RTx.pm
new file mode 100644
index 0000000..d371af9
--- /dev/null
+++ b/inc/Module/Install/RTx.pm
@@ -0,0 +1,300 @@
+#line 1
+package Module::Install::RTx;
+
+use 5.008;
+use strict;
+use warnings;
+no warnings 'once';
+
+use Module::Install::Base;
+use base 'Module::Install::Base';
+our $VERSION = '0.41';
+
+use FindBin;
+use File::Glob     ();
+use File::Basename ();
+
+my @DIRS = qw(etc lib html static bin sbin po var);
+my @INDEX_DIRS = qw(lib bin sbin);
+
+sub RTx {
+    my ( $self, $name, $extra_args ) = @_;
+    $extra_args ||= {};
+
+    # Set up names
+    my $fname = $name;
+    $fname =~ s!-!/!g;
+
+    $self->name( $name )
+        unless $self->name;
+    $self->all_from( "lib/$fname.pm" )
+        unless $self->version;
+    $self->abstract("$name Extension")
+        unless $self->abstract;
+    unless ( $extra_args->{no_readme_generation} ) {
+        $self->readme_from( "lib/$fname.pm",
+                            { options => [ quotes => "none" ] } );
+    }
+    $self->add_metadata("x_module_install_rtx_version", $VERSION );
+
+    my $installdirs = $ENV{INSTALLDIRS};
+    for ( @ARGV ) {
+        if ( /INSTALLDIRS=(.*)/ ) {
+            $installdirs = $1;
+        }
+    }
+
+    # Try to find RT.pm
+    my @prefixes = qw( /opt /usr/local /home /usr /sw /usr/share/request-tracker4);
+    $ENV{RTHOME} =~ s{/RT\.pm$}{} if defined $ENV{RTHOME};
+    $ENV{RTHOME} =~ s{/lib/?$}{}  if defined $ENV{RTHOME};
+    my @try = $ENV{RTHOME} ? ($ENV{RTHOME}, "$ENV{RTHOME}/lib") : ();
+    while (1) {
+        my @look = @INC;
+        unshift @look, grep {defined and -d $_} @try;
+        push @look, grep {defined and -d $_}
+            map { ( "$_/rt5/lib", "$_/lib/rt5", "$_/rt4/lib", "$_/lib/rt4", "$_/lib" ) } @prefixes;
+        last if eval {local @INC = @look; require RT; $RT::LocalLibPath};
+
+        warn
+            "Cannot find the location of RT.pm that defines \$RT::LocalPath in: @look\n";
+        my $given = $self->prompt("Path to directory containing your RT.pm:") or exit;
+        $given =~ s{/RT\.pm$}{};
+        $given =~ s{/lib/?$}{};
+        @try = ($given, "$given/lib");
+    }
+
+    print "Using RT configuration from $INC{'RT.pm'}:\n";
+
+    my $local_lib_path = $RT::LocalLibPath;
+    unshift @INC, $local_lib_path;
+    my $lib_path = File::Basename::dirname( $INC{'RT.pm'} );
+    unshift @INC, $lib_path;
+
+    # Set a baseline minimum version
+    unless ( $extra_args->{deprecated_rt} ) {
+        $self->requires_rt('4.0.0');
+    }
+
+    # Installation locations
+    my %path;
+    my $plugin_path;
+    if ( $installdirs && $installdirs eq 'vendor' ) {
+        $plugin_path = $RT::PluginPath;
+    } else {
+        $plugin_path = $RT::LocalPluginPath;
+    }
+    $path{$_} = $plugin_path . "/$name/$_"
+        foreach @DIRS;
+
+    # Copy RT 4.2.0 static files into NoAuth; insufficient for
+    # images, but good enough for css and js.
+    $path{static} = "$path{html}/NoAuth/"
+        unless $RT::StaticPath;
+
+    # Delete the ones we don't need
+    delete $path{$_} for grep {not -d "$FindBin::Bin/$_"} keys %path;
+
+    my %index = map { $_ => 1 } @INDEX_DIRS;
+    $self->no_index( directory => $_ ) foreach grep !$index{$_}, @DIRS;
+
+    my $args = join ', ', map "q($_)", map { ($_, "\$(DESTDIR)$path{$_}") }
+        sort keys %path;
+
+    printf "%-10s => %s\n", $_, $path{$_} for sort keys %path;
+
+    if ( my @dirs = map { ( -D => $_ ) } grep $path{$_}, qw(bin html sbin etc) ) {
+        my @po = map { ( -o => $_ ) }
+            grep -f,
+            File::Glob::bsd_glob("po/*.po");
+        $self->postamble(<< ".") if @po;
+lexicons ::
+\t\$(NOECHO) \$(PERL) -MLocale::Maketext::Extract::Run=xgettext -e \"xgettext(qw(@dirs @po))\"
+.
+    }
+
+    my $remove_files;
+    if( $extra_args->{'remove_files'} ){
+        $self->include('Module::Install::RTx::Remove');
+        our @remove_files;
+        eval { require "etc/upgrade/remove_files" }
+          or print "No remove file located, no files to remove\n";
+        $remove_files = join ",", map {"q(\$(DESTDIR)$plugin_path/$name/$_)"} @remove_files;
+    }
+
+    $self->include('Module::Install::RTx::Runtime') if $self->admin;
+    $self->include_deps( 'YAML::Tiny', 0 ) if $self->admin;
+    my $postamble = << ".";
+install ::
+\t\$(NOECHO) \$(PERL) -Ilib -I"$local_lib_path" -I"$lib_path" -Iinc -MModule::Install::RTx::Runtime -e"RTxPlugin()"
+.
+
+    if( $remove_files ){
+        $postamble .= << ".";
+\t\$(NOECHO) \$(PERL) -MModule::Install::RTx::Remove -e \"RTxRemove([$remove_files])\"
+.
+    }
+
+    $postamble .= << ".";
+\t\$(NOECHO) \$(PERL) -MExtUtils::Install -e \"install({$args})\"
+.
+
+    if ( $path{var} and -d $RT::MasonDataDir ) {
+        my ( $uid, $gid ) = ( stat($RT::MasonDataDir) )[ 4, 5 ];
+        $postamble .= << ".";
+\t\$(NOECHO) chown -R $uid:$gid $path{var}
+.
+    }
+
+    my %has_etc;
+    if ( File::Glob::bsd_glob("$FindBin::Bin/etc/schema.*") ) {
+        $has_etc{schema}++;
+    }
+    if ( File::Glob::bsd_glob("$FindBin::Bin/etc/acl.*") ) {
+        $has_etc{acl}++;
+    }
+    if ( -e 'etc/initialdata' ) { $has_etc{initialdata}++; }
+    if ( grep { /\d+\.\d+\.\d+.*$/ } glob('etc/upgrade/*.*.*') ) {
+        $has_etc{upgrade}++;
+    }
+
+    $self->postamble("$postamble\n");
+    if ( $path{lib} ) {
+        $self->makemaker_args( INSTALLSITELIB => $path{'lib'} );
+        $self->makemaker_args( INSTALLARCHLIB => $path{'lib'} );
+        $self->makemaker_args( INSTALLVENDORLIB => $path{'lib'} )
+    } else {
+        $self->makemaker_args( PM => { "" => "" }, );
+    }
+
+    $self->makemaker_args( INSTALLSITEMAN1DIR => "$RT::LocalPath/man/man1" );
+    $self->makemaker_args( INSTALLSITEMAN3DIR => "$RT::LocalPath/man/man3" );
+    $self->makemaker_args( INSTALLSITEARCH => "$RT::LocalPath/man" );
+
+    # INSTALLDIRS=vendor should install manpages into /usr/share/man.
+    # That is the default path in most distributions. Need input from
+    # Redhat, Centos etc.
+    $self->makemaker_args( INSTALLVENDORMAN1DIR => "/usr/share/man/man1" );
+    $self->makemaker_args( INSTALLVENDORMAN3DIR => "/usr/share/man/man3" );
+    $self->makemaker_args( INSTALLVENDORARCH => "/usr/share/man" );
+
+    if (%has_etc) {
+        print "For first-time installation, type 'make initdb'.\n";
+        my $initdb = '';
+        $initdb .= <<"." if $has_etc{schema};
+\t\$(NOECHO) \$(PERL) -Ilib -I"$local_lib_path" -I"$lib_path" -Iinc -MModule::Install::RTx::Runtime -e"RTxDatabase(qw(schema \$(NAME) \$(VERSION)))"
+.
+        $initdb .= <<"." if $has_etc{acl};
+\t\$(NOECHO) \$(PERL) -Ilib -I"$local_lib_path" -I"$lib_path" -Iinc -MModule::Install::RTx::Runtime -e"RTxDatabase(qw(acl \$(NAME) \$(VERSION)))"
+.
+        $initdb .= <<"." if $has_etc{initialdata};
+\t\$(NOECHO) \$(PERL) -Ilib -I"$local_lib_path" -I"$lib_path" -Iinc -MModule::Install::RTx::Runtime -e"RTxDatabase(qw(insert \$(NAME) \$(VERSION)))"
+.
+        $self->postamble("initdb ::\n$initdb\n");
+        $self->postamble("initialize-database ::\n$initdb\n");
+        if ($has_etc{upgrade}) {
+            print "To upgrade from a previous version of this extension, use 'make upgrade-database'\n";
+            my $upgradedb = qq|\t\$(NOECHO) \$(PERL) -Ilib -I"$local_lib_path" -I"$lib_path" -Iinc -MModule::Install::RTx::Runtime -e"RTxDatabase(qw(upgrade \$(NAME) \$(VERSION)))"\n|;
+            $self->postamble("upgrade-database ::\n$upgradedb\n");
+            $self->postamble("upgradedb ::\n$upgradedb\n");
+        }
+    }
+
+}
+
+sub requires_rt {
+    my ($self,$version) = @_;
+
+    _load_rt_handle();
+
+    if ($self->is_admin) {
+        $self->add_metadata("x_requires_rt", $version);
+        my @sorted = sort RT::Handle::cmp_version $version,'4.0.0';
+        $self->perl_version('5.008003') if $sorted[0] eq '4.0.0'
+            and (not $self->perl_version or '5.008003' > $self->perl_version);
+        @sorted = sort RT::Handle::cmp_version $version,'4.2.0';
+        $self->perl_version('5.010001') if $sorted[0] eq '4.2.0'
+            and (not $self->perl_version or '5.010001' > $self->perl_version);
+    }
+
+    # if we're exactly the same version as what we want, silently return
+    return if ($version eq $RT::VERSION);
+
+    my @sorted = sort RT::Handle::cmp_version $version,$RT::VERSION;
+
+    if ($sorted[-1] eq $version) {
+        die <<"EOT";
+
+**** Error: This extension requires RT $version. Your installed version
+            of RT ($RT::VERSION) is too old.
+
+EOT
+    }
+}
+
+sub requires_rt_plugin {
+    my $self = shift;
+    my ( $plugin ) = @_;
+
+    if ($self->is_admin) {
+        my $plugins = $self->Meta->{values}{"x_requires_rt_plugins"} || [];
+        push @{$plugins}, $plugin;
+        $self->add_metadata("x_requires_rt_plugins", $plugins);
+    }
+
+    my $path = $plugin;
+    $path =~ s{\:\:}{-}g;
+    $path = "$RT::LocalPluginPath/$path/lib";
+    if ( -e $path ) {
+        unshift @INC, $path;
+    } else {
+        my $name = $self->name;
+        warn <<"EOT";
+
+**** Warning: $name requires that the $plugin plugin be installed and
+              enabled; it does not appear to be installed.
+
+EOT
+    }
+    $self->requires(@_);
+}
+
+sub rt_too_new {
+    my ($self,$version,$msg) = @_;
+    my $name = $self->name;
+    $msg ||= <<EOT;
+
+**** Error: Your installed version of RT (%s) is too new; this extension
+            only works with versions older than %s.
+
+EOT
+    $self->add_metadata("x_rt_too_new", $version) if $self->is_admin;
+
+    _load_rt_handle();
+    my @sorted = sort RT::Handle::cmp_version $version,$RT::VERSION;
+
+    if ($sorted[0] eq $version) {
+        die sprintf($msg,$RT::VERSION,$version);
+    }
+}
+
+# RT::Handle runs FinalizeDatabaseType which calls RT->Config->Get
+# On 3.8, this dies.  On 4.0/4.2 ->Config transparently runs LoadConfig.
+# LoadConfig requires being able to read RT_SiteConfig.pm (root) so we'd
+# like to avoid pushing that on users.
+# Fake up just enough Config to let FinalizeDatabaseType finish, and
+# anyone later calling LoadConfig will overwrite our shenanigans.
+sub _load_rt_handle {
+    unless ($RT::Config) {
+        require RT::Config;
+        $RT::Config = RT::Config->new;
+        RT->Config->Set('DatabaseType','mysql');
+    }
+    require RT::Handle;
+}
+
+1;
+
+__END__
+
+#line 468
diff --git a/inc/Module/Install/RTx/Runtime.pm b/inc/Module/Install/RTx/Runtime.pm
new file mode 100644
index 0000000..ae07502
--- /dev/null
+++ b/inc/Module/Install/RTx/Runtime.pm
@@ -0,0 +1,80 @@
+#line 1
+package Module::Install::RTx::Runtime;
+
+use base 'Exporter';
+our @EXPORT = qw/RTxDatabase RTxPlugin/;
+
+use strict;
+use File::Basename ();
+
+sub _rt_runtime_load {
+    require RT;
+
+    eval { RT::LoadConfig(); };
+    if (my $err = $@) {
+        die $err unless $err =~ /^RT couldn't load RT config file/m;
+        my $warn = <<EOT;
+This usually means that your current user cannot read the file.  You
+will likely need to run this installation step as root, or some user
+with more permissions.
+EOT
+        $err =~ s/This usually means.*/$warn/s;
+        die $err;
+    }
+}
+
+sub RTxDatabase {
+    my ($action, $name, $version) = @_;
+
+    _rt_runtime_load();
+
+    require RT::System;
+    my $has_upgrade = RT::System->can('AddUpgradeHistory');
+
+    my $lib_path = File::Basename::dirname($INC{'RT.pm'});
+    my @args = (
+        "-I.",
+        "-Ilib",
+        "-I$RT::LocalLibPath",
+        "-I$lib_path",
+        "$RT::SbinPath/rt-setup-database",
+        "--action"      => $action,
+        ($action eq 'upgrade' ? () : ("--datadir"     => "etc")),
+        (($action eq 'insert') ? ("--datafile"    => "etc/initialdata") : ()),
+        "--dba"         => $RT::DatabaseAdmin || $RT::DatabaseUser,
+        "--prompt-for-dba-password" => '',
+        ($has_upgrade ? ("--package" => $name, "--ext-version" => $version) : ()),
+    );
+    # If we're upgrading against an RT which isn't at least 4.2 (has
+    # AddUpgradeHistory) then pass --package.  Upgrades against later RT
+    # releases will pick up --package from AddUpgradeHistory.
+    if ($action eq 'upgrade' and not $has_upgrade) {
+        push @args, "--package" => $name;
+    }
+
+    print "$^X @args\n";
+    (system($^X, @args) == 0) or die "...returned with error: $?\n";
+}
+
+sub RTxPlugin {
+    my ($name) = @_;
+
+    _rt_runtime_load();
+    require YAML::Tiny;
+    my $data = YAML::Tiny::LoadFile('META.yml');
+    my $name = $data->{name};
+
+    my @enabled = RT->Config->Get('Plugins');
+    for my $required (@{$data->{x_requires_rt_plugins} || []}) {
+        next if grep {$required eq $_} @enabled;
+
+        warn <<"EOT";
+
+**** Warning: $name requires that the $required plugin be installed and
+              enabled; it is not currently in \@Plugins.
+
+EOT
+    }
+}
+
+1;
diff --git a/inc/Module/Install/ReadmeFromPod.pm b/inc/Module/Install/ReadmeFromPod.pm
new file mode 100644
index 0000000..3738232
--- /dev/null
+++ b/inc/Module/Install/ReadmeFromPod.pm
@@ -0,0 +1,184 @@
+#line 1
+package Module::Install::ReadmeFromPod;
+
+use 5.006;
+use strict;
+use warnings;
+use base qw(Module::Install::Base);
+use vars qw($VERSION);
+
+$VERSION = '0.30';
+
+{
+
+    # these aren't defined until after _require_admin is run, so
+    # define them so prototypes are available during compilation.
+    sub io;
+    sub capture(&;@);
+
+#line 28
+
+    my $done = 0;
+
+    sub _require_admin {
+
+	# do this once to avoid redefinition warnings from IO::All
+	return if $done;
+
+	require IO::All;
+	IO::All->import( '-binary' );
+
+	require Capture::Tiny;
+	Capture::Tiny->import ( 'capture' );
+
+	return;
+    }
+
+}
+
+sub readme_from {
+  my $self = shift;
+  return unless $self->is_admin;
+
+  _require_admin;
+
+  # Input file
+  my $in_file  = shift || $self->_all_from
+    or die "Can't determine file to make readme_from";
+
+  # Get optional arguments
+  my ($clean, $format, $out_file, $options);
+  my $args = shift;
+  if ( ref $args ) {
+    # Arguments are in a hashref
+    if ( ref($args) ne 'HASH' ) {
+      die "Expected a hashref but got a ".ref($args)."\n";
+    } else {
+      $clean    = $args->{'clean'};
+      $format   = $args->{'format'};
+      $out_file = $args->{'output_file'};
+      $options  = $args->{'options'};
+    }
+  } else {
+    # Arguments are in a list
+    $clean    = $args;
+    $format   = shift;
+    $out_file = shift;
+    $options  = \@_;
+  }
+
+  # Default values;
+  $clean  ||= 0;
+  $format ||= 'txt';
+
+  # Generate README
+  print "readme_from $in_file to $format\n";
+  if ($format =~ m/te?xt/) {
+    $out_file = $self->_readme_txt($in_file, $out_file, $options);
+  } elsif ($format =~ m/html?/) {
+    $out_file = $self->_readme_htm($in_file, $out_file, $options);
+  } elsif ($format eq 'man') {
+    $out_file = $self->_readme_man($in_file, $out_file, $options);
+  } elsif ($format eq 'md') {
+    $out_file = $self->_readme_md($in_file, $out_file, $options);
+  } elsif ($format eq 'pdf') {
+    $out_file = $self->_readme_pdf($in_file, $out_file, $options);
+  }
+
+  if ($clean) {
+    $self->clean_files($out_file);
+  }
+
+  return 1;
+}
+
+
+sub _readme_txt {
+  my ($self, $in_file, $out_file, $options) = @_;
+  $out_file ||= 'README';
+  require Pod::Text;
+  my $parser = Pod::Text->new( @$options );
+  my $io = io->file($out_file)->open(">");
+  my $out_fh = $io->io_handle;
+  $parser->output_fh( *$out_fh );
+  $parser->parse_file( $in_file );
+  return $out_file;
+}
+
+
+sub _readme_htm {
+  my ($self, $in_file, $out_file, $options) = @_;
+  $out_file ||= 'README.htm';
+  require Pod::Html;
+  my ($o) = capture {
+    Pod::Html::pod2html(
+      "--infile=$in_file",
+      "--outfile=-",
+      @$options,
+    );
+  };
+  io->file($out_file)->print($o);
+  # Remove temporary files if needed
+  for my $file ('pod2htmd.tmp', 'pod2htmi.tmp') {
+    if (-e $file) {
+      unlink $file or warn "Warning: Could not remove file '$file'.\n$!\n";
+    }
+  }
+  return $out_file;
+}
+
+
+sub _readme_man {
+  my ($self, $in_file, $out_file, $options) = @_;
+  $out_file ||= 'README.1';
+  require Pod::Man;
+  my $parser = Pod::Man->new( @$options );
+  my $io = io->file($out_file)->open(">");
+  my $out_fh = $io->io_handle;
+  $parser->output_fh( *$out_fh );
+  $parser->parse_file( $in_file );
+  return $out_file;
+}
+
+
+sub _readme_pdf {
+  my ($self, $in_file, $out_file, $options) = @_;
+  $out_file ||= 'README.pdf';
+  eval { require App::pod2pdf; }
+    or die "Could not generate $out_file because pod2pdf could not be found\n";
+  my $parser = App::pod2pdf->new( @$options );
+  $parser->parse_from_file($in_file);
+  my ($o) = capture { $parser->output };
+  io->file($out_file)->print($o);
+  return $out_file;
+}
+
+sub _readme_md {
+  my ($self, $in_file, $out_file, $options) = @_;
+  $out_file ||= 'README.md';
+  require Pod::Markdown;
+  my $parser = Pod::Markdown->new( @$options );
+  my $io = io->file($out_file)->open(">");
+  my $out_fh = $io->io_handle;
+  $parser->output_fh( *$out_fh );
+  $parser->parse_file( $in_file );
+  return $out_file;
+}
+
+
+sub _all_from {
+  my $self = shift;
+  return unless $self->admin->{extensions};
+  my ($metadata) = grep {
+    ref($_) eq 'Module::Install::Metadata';
+  } @{$self->admin->{extensions}};
+  return unless $metadata;
+  return $metadata->{values}{all_from} || '';
+}
+
+'Readme!';
+
+__END__
+
+#line 316
+
diff --git a/inc/Module/Install/Substitute.pm b/inc/Module/Install/Substitute.pm
new file mode 100644
index 0000000..56af7fe
--- /dev/null
+++ b/inc/Module/Install/Substitute.pm
@@ -0,0 +1,131 @@
+#line 1
+package Module::Install::Substitute;
+
+use strict;
+use warnings;
+use 5.008; # I don't care much about earlier versions
+
+use Module::Install::Base;
+our @ISA = qw(Module::Install::Base);
+
+our $VERSION = '0.03';
+
+require File::Temp;
+require File::Spec;
+require Cwd;
+
+#line 89
+
+sub substitute
+{
+	my $self = shift;
+	$self->{__subst} = shift;
+	$self->{__option} = {};
+	if( UNIVERSAL::isa( $_[0], 'HASH' ) ) {
+		my $opts = shift;
+		while( my ($k,$v) = each( %$opts ) ) {
+			$self->{__option}->{ lc( $k ) } = $v || '';
+		}
+	}
+	$self->_parse_options;
+
+	my @file = @_;
+	foreach my $f (@file) {
+		$self->_rewrite_file( $f );
+	}
+
+	return;
+}
+
+sub _parse_options
+{
+	my $self = shift;
+	my $cwd = Cwd::getcwd();
+	foreach my $t ( qw(from to) ) {
+        $self->{__option}->{$t} = $cwd unless $self->{__option}->{$t};
+		my $d = $self->{__option}->{$t};
+		die "Couldn't read directory '$d'" unless -d $d && -r _;
+	}
+}
+
+sub _rewrite_file
+{
+	my ($self, $file) = @_;
+	my $source = File::Spec->catfile( $self->{__option}{from}, $file );
+	$source .= $self->{__option}{sufix} if $self->{__option}{sufix};
+	unless( -f $source && -r _ ) {
+		print STDERR "Couldn't find file '$source'\n";
+		return;
+	}
+	my $dest = File::Spec->catfile( $self->{__option}{to}, $file );
+	return $self->__rewrite_file( $source, $dest );
+}
+
+sub __rewrite_file
+{
+	my ($self, $source, $dest) = @_;
+
+	my $mode = (stat($source))[2];
+
+	open my $sfh, "<$source" or die "Couldn't open '$source' for read";
+	print "Open input '$source' file for substitution\n";
+
+	my ($tmpfh, $tmpfname) = File::Temp::tempfile('mi-subst-XXXX', UNLINK => 1);
+	$self->__process_streams( $sfh, $tmpfh, ($source eq $dest)? 1: 0 );
+	close $sfh;
+
+	seek $tmpfh, 0, 0 or die "Couldn't seek in tmp file";
+
+	open my $dfh, ">$dest" or die "Couldn't open '$dest' for write";
+	print "Open output '$dest' file for substitution\n";
+
+	while( <$tmpfh> ) {
+		print $dfh $_;
+	}
+	close $dfh;
+	chmod $mode, $dest or "Couldn't change mode on '$dest'";
+}
+
+sub __process_streams
+{
+	my ($self, $in, $out, $replace) = @_;
+	
+	my @queue = ();
+	my $subst = $self->{'__subst'};
+	my $re_subst = join('|', map {"\Q$_"} keys %{ $subst } );
+
+	while( my $str = <$in> ) {
+		if( $str =~ /^###\s*(before|replace|after)\:\s?(.*)$/s ) {
+			my ($action, $nstr) = ($1,$2);
+			$nstr =~ s/\@($re_subst)\@/$subst->{$1}/ge;
+
+			die "Replace action is bad idea for situations when dest is equal to source"
+                if $replace && $action eq 'replace';
+			if( $action eq 'before' ) {
+				die "no line before 'before' action" unless @queue;
+				# overwrite prev line;
+				pop @queue;
+				push @queue, $nstr;
+				push @queue, $str;
+			} elsif( $action eq 'replace' ) {
+				push @queue, $nstr;
+			} elsif( $action eq 'after' ) {
+				push @queue, $str;
+				push @queue, $nstr;
+				# skip one line;
+				<$in>;
+			}
+		} else {
+			push @queue, $str;
+		}
+		while( @queue > 3 ) {
+			print $out shift(@queue);
+		}
+	}
+	while( scalar @queue ) {
+		print $out shift(@queue);
+	}
+}
+
+1;
+
diff --git a/inc/Module/Install/Win32.pm b/inc/Module/Install/Win32.pm
new file mode 100644
index 0000000..f7aa615
--- /dev/null
+++ b/inc/Module/Install/Win32.pm
@@ -0,0 +1,64 @@
+#line 1
+package Module::Install::Win32;
+
+use strict;
+use Module::Install::Base ();
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+	$VERSION = '1.19';
+	@ISA     = 'Module::Install::Base';
+	$ISCORE  = 1;
+}
+
+# determine if the user needs nmake, and download it if needed
+sub check_nmake {
+	my $self = shift;
+	$self->load('can_run');
+	$self->load('get_file');
+
+	require Config;
+	return unless (
+		$^O eq 'MSWin32'                     and
+		$Config::Config{make}                and
+		$Config::Config{make} =~ /^nmake\b/i and
+		! $self->can_run('nmake')
+	);
+
+	print "The required 'nmake' executable not found, fetching it...\n";
+
+	require File::Basename;
+	my $rv = $self->get_file(
+		url       => 'http://download.microsoft.com/download/vc15/Patch/1.52/W95/EN-US/Nmake15.exe',
+		ftp_url   => 'ftp://ftp.microsoft.com/Softlib/MSLFILES/Nmake15.exe',
+		local_dir => File::Basename::dirname($^X),
+		size      => 51928,
+		run       => 'Nmake15.exe /o > nul',
+		check_for => 'Nmake.exe',
+		remove    => 1,
+	);
+
+	die <<'END_MESSAGE' unless $rv;
+
+-------------------------------------------------------------------------------
+
+Since you are using Microsoft Windows, you will need the 'nmake' utility
+before installation. It's available at:
+
+  http://download.microsoft.com/download/vc15/Patch/1.52/W95/EN-US/Nmake15.exe
+      or
+  ftp://ftp.microsoft.com/Softlib/MSLFILES/Nmake15.exe
+
+Please download the file manually, save it to a directory in %PATH% (e.g.
+C:\WINDOWS\COMMAND\), then launch the MS-DOS command line shell, "cd" to
+that directory, and run "Nmake15.exe" from there; that will create the
+'nmake.exe' file needed by this module.
+
+You may then resume the installation process described in README.
+
+-------------------------------------------------------------------------------
+END_MESSAGE
+
+}
+
+1;
diff --git a/inc/Module/Install/WriteAll.pm b/inc/Module/Install/WriteAll.pm
new file mode 100644
index 0000000..2db861a
--- /dev/null
+++ b/inc/Module/Install/WriteAll.pm
@@ -0,0 +1,63 @@
+#line 1
+package Module::Install::WriteAll;
+
+use strict;
+use Module::Install::Base ();
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+	$VERSION = '1.19';
+	@ISA     = qw{Module::Install::Base};
+	$ISCORE  = 1;
+}
+
+sub WriteAll {
+	my $self = shift;
+	my %args = (
+		meta        => 1,
+		sign        => 0,
+		inline      => 0,
+		check_nmake => 1,
+		@_,
+	);
+
+	$self->sign(1)                if $args{sign};
+	$self->admin->WriteAll(%args) if $self->is_admin;
+
+	$self->check_nmake if $args{check_nmake};
+	unless ( $self->makemaker_args->{PL_FILES} ) {
+		# XXX: This still may be a bit over-defensive...
+		unless ($self->makemaker(6.25)) {
+			$self->makemaker_args( PL_FILES => {} ) if -f 'Build.PL';
+		}
+	}
+
+	# Until ExtUtils::MakeMaker support MYMETA.yml, make sure
+	# we clean it up properly ourself.
+	$self->realclean_files('MYMETA.yml');
+
+	if ( $args{inline} ) {
+		$self->Inline->write;
+	} else {
+		$self->Makefile->write;
+	}
+
+	# The Makefile write process adds a couple of dependencies,
+	# so write the META.yml files after the Makefile.
+	if ( $args{meta} ) {
+		$self->Meta->write;
+	}
+
+	# Experimental support for MYMETA
+	if ( $ENV{X_MYMETA} ) {
+		if ( $ENV{X_MYMETA} eq 'JSON' ) {
+			$self->Meta->write_mymeta_json;
+		} else {
+			$self->Meta->write_mymeta_yaml;
+		}
+	}
+
+	return 1;
+}
+
+1;
diff --git a/inc/YAML/Tiny.pm b/inc/YAML/Tiny.pm
new file mode 100644
index 0000000..fb157a6
--- /dev/null
+++ b/inc/YAML/Tiny.pm
@@ -0,0 +1,872 @@
+#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
diff --git a/lib/App/wsgetmail.pm b/lib/App/wsgetmail.pm
index af21e47..ba79cae 100644
--- a/lib/App/wsgetmail.pm
+++ b/lib/App/wsgetmail.pm
@@ -1,20 +1,17 @@
-package App::wsgetmail;
-
 use strict;
 use warnings;
+use 5.010001;
 
-=head1 NAME
+package App::wsgetmail;
 
-App::wsgetmail - Fetch mail from the cloud using webservices
+our $VERSION = '0.01';
 
-=head1 VERSION
+=head1 NAME
 
-Version 0.01
+App::wsgetmail - Fetch mail from the cloud using webservices
 
 =cut
 
-our $VERSION = '0.01';
-
 =head1 DESCRIPTION
 
 A simple command line application/script to fetch mail from the cloud
@@ -76,16 +73,16 @@ an example configuration file is included in the docs/ directory of this package
 
 =head1 AUTHOR
 
-Aaron Trevena, C<< <ast at bestpractical.com> >>
+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) 2015-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; # End of App::wsgetmail
+1;

commit 49b0d535c71c172650c10d9a177bcd391a0c67d3
Author: craig kaiser <craig at bestpractical.com>
Date:   Thu Jul 9 10:06:00 2020 -0400

    Fix typo in POD

diff --git a/lib/App/wsgetmail/MS365.pm b/lib/App/wsgetmail/MS365.pm
index 7c50ce4..d8afdf7 100644
--- a/lib/App/wsgetmail/MS365.pm
+++ b/lib/App/wsgetmail/MS365.pm
@@ -355,7 +355,7 @@ sub _build_client {
 
 Active Directory application configuration
 
-From Azure Active directory admin center..
+From Azure Active directory admin center.
 
 =over 4
 

commit 046450dd30aafc80529b041005c6e1bd1c7b8f62
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Mon Jun 22 14:33:57 2020 +0100

    Allow extra options to rt-mailgate handler

diff --git a/bin/wsgetmail365 b/bin/wsgetmail365
old mode 100644
new mode 100755
index 311ea63..da232e0
--- a/bin/wsgetmail365
+++ b/bin/wsgetmail365
@@ -1,4 +1,4 @@
-#!perl
+#!/usr/bin/env perl
 
 use strict;
 use JSON;
@@ -8,10 +8,10 @@ use File::Slurp;
 use Pod::Usage;
 use Getopt::Long;
 
-my $config_file;
+my ($config_file, $options);
 my ($verbose, $help) = (0,0);
 
-GetOptions('help|?' => \$help, "verbose" => \$verbose, "c|config|configuration=s" => \$config_file);
+GetOptions('help|?' => \$help, "verbose" => \$verbose, "c|config|configuration=s" => \$config_file, 'options' => \&options);
 
 pod2usage(1) if $help;
 pod2usage(1) unless ($config_file);
@@ -21,6 +21,13 @@ my $config_json = read_file($config_file);
 
 my $config = decode_json($config_json);
 
+# parse handler options, over-ride config if provided
+
+my $extra_options = (defined($options) && $options ) ? decode_json($options) : { };
+foreach my $option ( keys %$extra_options ) {
+    $config->{$option} = $extra_options->{$option};
+}
+
 my $getmail = App::wsgetmail::MS365->new($config);
 
 print "\nfetching mail using configuration $config_file\n";
@@ -65,7 +72,19 @@ get mail from microsoft365 Exchange Online
 
 =head1 SYNOPSIS
 
-wsgetmail365 --configuration path/to/file.json [--debug]
+wsgetmail365 --config[uration] path/to/file.json [--options "{..}"] [--debug]
+
+=head1 ARGUMENTS
+
+=over 4
+
+=item config - configuration file to be used
+
+=item options - json string of options over-riding or adding to configuration from filename (optional)
+
+=item debug - flag indicating that debug warnings should be output (optional)
+
+=back
 
 =head1 CONFIGURATION
 
@@ -84,6 +103,8 @@ 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"
 
+Any additional options in handler_options will be passed through to the command called
+
 example configuration :
 {
   "handler_options":{
diff --git a/lib/App/wsgetmail/Handler/RT/Mailgate.pm b/lib/App/wsgetmail/Handler/RT/Mailgate.pm
index 9e4c4d4..08a4e86 100644
--- a/lib/App/wsgetmail/Handler/RT/Mailgate.pm
+++ b/lib/App/wsgetmail/Handler/RT/Mailgate.pm
@@ -1,7 +1,40 @@
 package App::wsgetmail::Handler::RT::Mailgate;
+
+=head1 NAME
+
+App::wsgetmail::Handler::RT::Mailgate - forward mail via rt-mailgate
+
+=cut
+
 use Moo;
 use IPC::Run qw( run timeout );
 
+=head1 DESCRIPTION
+
+Forward mail to RT with rt-mailgate.
+
+=head1 ATTRIBUTES
+
+=over 4
+
+=item debug - default false, optional
+
+=item url - URI to RT, required
+
+=item request_timeout - timeout, optional
+
+=item extension - field to use for username extension, optional
+
+=item recipient - address to expect and parse for extension, required
+
+=item command_path - path to rt-mailgate
+
+=item extra_options - hashref of extra options to pass to rt-mailgate
+
+=back
+
+=cut
+
 has debug => (
     is => 'ro',
     lazy => 1,
@@ -45,6 +78,11 @@ has command_path  => (
     required => 0
 );
 
+has extra_options => (
+    is => 'ro',
+    required => 1,
+);
+
 my $option_configuration = {
     url => 'string',
     action => [qw(correspond comment)],
@@ -53,6 +91,25 @@ my $option_configuration = {
     queue => 'string'
 };
 
+my $standard_options = { %$option_configuration,
+                         debug => 1,
+                         recipient => 1,
+                         command_path => 1,
+                         action_on_fetched => 1
+                     };
+
+
+around BUILDARGS => sub {
+  my ( $orig, $class, $args ) = @_;
+  my $extra_options = { };
+  foreach my $extra_option (grep { ! exists $standard_options->{$_} } keys %$args) {
+      $extra_options->{$extra_option} = delete $args->{$extra_option};
+  }
+  $args->{extra_options} = $extra_options;
+  return $class->$orig($args);
+};
+
+
 sub forward {
     my ($self, $message, $filename) = @_;
     # build arguments
@@ -70,7 +127,6 @@ sub forward {
         }
     }
 
-    # force debug to true, use it to confirm success
     my $arguments = [ ];
     foreach my $option (keys %$option_configuration) {
         next unless ($self->$option);
@@ -83,6 +139,18 @@ sub forward {
     }
     push (@$arguments, "--debug") if ($self->debug);
 
+    my %extra = %{$self->extra_options};
+    foreach my $option ( keys %extra ) {
+        if ($extra{$option} eq '1' or (JSON::is_bool($extra{$option}) && $extra{$option})) {
+            push(@$arguments, "--$option");
+        } else {
+            push(@$arguments, "--$option", $extra{$option});
+        }
+    }
+
+    if ($self->debug) {
+        warn "calling $filename with arguments " . join (' ', @$arguments);
+    }
     # run command
     return $self->_run_command($filename, $arguments);
 }

commit 8540df0a196ea53b012a0fee4ca3ff56e37de1dd
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Mon Jun 22 14:33:25 2020 +0100

    refactor hardcoded urls out of wsgetmail MS365 client

diff --git a/lib/App/wsgetmail/MS365/Client.pm b/lib/App/wsgetmail/MS365/Client.pm
index 905f791..441d9dc 100644
--- a/lib/App/wsgetmail/MS365/Client.pm
+++ b/lib/App/wsgetmail/MS365/Client.pm
@@ -67,6 +67,16 @@ has global_access => (
     default => sub { return 0 }
 );
 
+has resource_url => (
+    is => 'ro',
+    default => sub { return 'https://graph.microsoft.com/' }
+);
+
+has resource_path => (
+    is => 'ro',
+    default => sub { return 'v1.0' }
+);
+
 has _ua => (
     builder   => '_build_authorised_ua',
     is => 'ro',
@@ -85,8 +95,6 @@ has _access_token => (
     builder => '_build__access_token',
 );
 
-my $graph_v1_url = 'https://graph.microsoft.com/v1.0';
-
 sub BUILD {
     my ($self, $args) = @_;
 
@@ -111,7 +119,8 @@ sub BUILD {
 
 sub build_rest_uri {
     my ($self, @endpoint_parts) = @_;
-    return join('/', $graph_v1_url, @endpoint_parts);
+    my $base_url = $self->resource_url . $self->resource_path;
+    return join('/', $base_url, @endpoint_parts);
 }
 
 =head2 get_request
@@ -193,7 +202,7 @@ sub _get_user_access_token {
     my $oauth_login_url = sprintf('https://login.windows.net/%s/oauth2/token', $self->tenant_id);
     my $response = $ua->post( $oauth_login_url,
                               {
-                                  resource=>'https://graph.microsoft.com/',
+                                  resource=> $self->resource_url,
                                   client_id => $self->client_id,
                                   grant_type=>'password',
                                   username=>$self->username,
@@ -217,7 +226,7 @@ sub _get_user_access_token {
 sub _build__credentials {
     my $self = shift;
     my $creds = Azure::AD::ClientCredentials->new(
-        resource_id => 'https://graph.microsoft.com/',
+        resource_id => $self->resource_url,
         client_id => $self->client_id,
         secret_id => $self->secret,
         tenant_id => $self->tenant_id

commit e9bcb9c5871ce748d4f670ef8a7ec778227d332a
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Mon Jun 22 14:32:21 2020 +0100

    fix wsgetmail package names in test

diff --git a/t/00-load.t b/t/00-load.t
index 7f55efc..ee3a0df 100644
--- a/t/00-load.t
+++ b/t/00-load.t
@@ -5,8 +5,8 @@ use Test::More;
 use lib qw(lib);
 
 use_ok( 'App::wsgetmail' );
-use_ok( 'App::wsgetmail::MS360' );
-use_ok( 'App::wsgetmail::MS360::Client' );
+use_ok( 'App::wsgetmail::MS365' );
+use_ok( 'App::wsgetmail::MS365::Client' );
 use_ok( 'App::wsgetmail::Handler::RT::Mailgate' );
 
 done_testing();

commit 972c281b9743660237d65adc99d76d4dcd7b237e
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Thu Jun 18 16:23:24 2020 +0100

    Added abbreviated options to wsgetmail script

diff --git a/bin/wsgetmail365 b/bin/wsgetmail365
index b76e16a..311ea63 100644
--- a/bin/wsgetmail365
+++ b/bin/wsgetmail365
@@ -11,7 +11,7 @@ use Getopt::Long;
 my $config_file;
 my ($verbose, $help) = (0,0);
 
-GetOptions('help|?' => \$help, "verbose" => \$verbose, "config=s" => \$config_file);
+GetOptions('help|?' => \$help, "verbose" => \$verbose, "c|config|configuration=s" => \$config_file);
 
 pod2usage(1) if $help;
 pod2usage(1) unless ($config_file);

commit 12033386e34f9f7b0fdc1646c6b8e7049865d5f2
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Thu Jun 11 15:36:41 2020 +0100

    Tidy and improve tests for wsgetmail

diff --git a/t/basic.t b/t/basic.t
index 516c030..545ae9d 100644
--- a/t/basic.t
+++ b/t/basic.t
@@ -4,31 +4,24 @@ use strict;
 use Test::More;
 use JSON;
 use File::Slurp;
-
 use Test::LWP::UserAgent;
 
-
 use App::wsgetmail::MS365::Client;
 use App::wsgetmail::MS365;
 
 
 my $test_config = get_test_config();
 my $oauth_login_url = sprintf('https://login.windows.net/%s/oauth2/token', $test_config->{tenant_id} );
-my $graph_v1_url = 'graph.microsoft.com/v1.0/';
-
-*App::wsgetmail::MS365::Client::_new_useragent = sub {
-    my $useragent = Test::LWP::UserAgent->new;
-    # map oauth request to get token
-    $useragent->map_response( qr{login.windows.net/} => get_mocked_token_response() );
-
-    # map folders list request/response
-    $useragent->map_response( qr{$graph_v1_url/me/mailFolders[^/]} => get_mocked_folders_list_response() );
+my $graph_v1_url = 'graph.microsoft.com/v1.0';
+my $useragent = Test::LWP::UserAgent->new;
+# map oauth request to get token
+$useragent->map_response( qr{login.windows.net/} => get_mocked_token_response() );
+# map folders list request/response
+$useragent->map_response( qr{$graph_v1_url/me/mailFolders[^/]} => get_mocked_folders_list_response() );
+# map message list request/response
+$useragent->map_response( qr{$graph_v1_url/me/mailFolders/} => get_mocked_messages_response() );
 
-    # map message list request/response
-    $useragent->map_response( qr{$graph_v1_url/me/mailFolders/} => get_mocked_messages_response() );
-
-    return $useragent;
-};
+*App::wsgetmail::MS365::Client::_new_useragent = sub { return $useragent };
 
 my $getmail = App::wsgetmail::MS365->new($test_config);
 isa_ok($getmail, 'App::wsgetmail::MS365');
@@ -36,11 +29,16 @@ isa_ok($getmail, 'App::wsgetmail::MS365');
 my $message1 = $getmail->get_next_message();
 isa_ok($message1, 'App::wsgetmail::MS365::Message');
 is($message1->id, "xxxxxxxxabc1", "first message fetched with correct id");
-
 my $message2 = $getmail->get_next_message();
 isa_ok($message2, 'App::wsgetmail::MS365::Message');
 is($message2->id, "xxxxxxxxxxxxxxxxxxxxabc2=", "next message fetched with correct id");
 
+my $message_req = $useragent->last_http_request_sent();
+is($message_req->method, 'GET', 'request method is correct');
+like($message_req->uri, qr{graph.microsoft.com/v1.0/me/mailFolders/AAAABBBBCCCCCDDDDDXXXXX=/messages}, 'correct uri for request');
+like($message_req->uri, qr{filter=isRead\+eq\+false}, 'filter is correct');
+is($message_req->header( 'Authorization' ) => 'Bearer xxxxxxxxN2tpxxxxxxxxxxxxxxxx', 'Authorisation token used correctly');
+
 done_testing();
 
 #####
@@ -49,10 +47,10 @@ sub get_mocked_folders_list_response {
     return HTTP::Response->new('200', 'OK', ['Content-Type' => 'application/json'], '{ "value": [ {
                          "displayName":"another_folder",
                          "totalItemCount":2,
-                         "parentFolderId":"AAMkADFmYTUxODNlLThiZWUtNGI5MS1iOGUyLTBkOGZhYmNhYWIyNgAuAAAAAAA4V3dDNK4_Rpd86g3XzTaIAQBY_b0kgtxhRpNG1KbQD9hUAAAAAAEIAAA=",
+                         "parentFolderId":"AABBBBBAAAAA=",
                          "childFolderCount":0,
                          "unreadItemCount":2,
-                         "id":"AAMkADFmYTUxODNlLThiZWUtNGI5MS1iOGUyLTBkOGZhYmNhYWIyNgAuAAAAAAA4V3dDNK4_Rpd86g3XzTaIAQBY_b0kgtxhRpNG1KbQD9hUAAAKstICAAA="
+                         "id":"AAAABBBBCCCCCDDDDDXXXXX="
                      } ]}');
 }
 

commit 0d9d2ed4888c28cba436de8c6ea2943400f7e1b9
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Thu Jun 11 15:36:25 2020 +0100

    Remove extra slash from MS graph uri in wsgetmail client

diff --git a/lib/App/wsgetmail/MS365/Client.pm b/lib/App/wsgetmail/MS365/Client.pm
index a0a5696..905f791 100644
--- a/lib/App/wsgetmail/MS365/Client.pm
+++ b/lib/App/wsgetmail/MS365/Client.pm
@@ -85,7 +85,7 @@ has _access_token => (
     builder => '_build__access_token',
 );
 
-my $graph_v1_url = 'https://graph.microsoft.com/v1.0/';
+my $graph_v1_url = 'https://graph.microsoft.com/v1.0';
 
 sub BUILD {
     my ($self, $args) = @_;

commit 77342301fb26882d60f2c2702aecba8dede39c58
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Fri Jun 5 14:01:01 2020 +0100

    Added unit tests for wsgetmail

diff --git a/Makefile.PL b/Makefile.PL
index 116e990..389d870 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -15,6 +15,7 @@ my %WriteMakefileArgs = (
     },
     TEST_REQUIRES => {
         'Test::More' => '0',
+	'Test::LWP::UserAgent' => 0,
     },
     PREREQ_PM => {
         'Azure::AD::ClientCredentials' => 0,
diff --git a/bin/wsgetmail365 b/bin/wsgetmail365
index e0b6dfd..b76e16a 100644
--- a/bin/wsgetmail365
+++ b/bin/wsgetmail365
@@ -39,6 +39,10 @@ if (lc($action) eq 'mark_as_read') {
 
 while (my $message = $getmail->get_next_message()) {
     my $file = $getmail->get_message_mime_content($message->id);
+    unless ($file) {
+        warn "failed to get mime content for message, skipping..";
+        next;
+    }
     my $forwarded_ok = $getmail->forward($message, $file);
     if ($forwarded_ok) {
         $getmail->$fetched_action_method($message->id);
diff --git a/t/basic.t b/t/basic.t
new file mode 100644
index 0000000..516c030
--- /dev/null
+++ b/t/basic.t
@@ -0,0 +1,88 @@
+#!perl
+
+use strict;
+use Test::More;
+use JSON;
+use File::Slurp;
+
+use Test::LWP::UserAgent;
+
+
+use App::wsgetmail::MS365::Client;
+use App::wsgetmail::MS365;
+
+
+my $test_config = get_test_config();
+my $oauth_login_url = sprintf('https://login.windows.net/%s/oauth2/token', $test_config->{tenant_id} );
+my $graph_v1_url = 'graph.microsoft.com/v1.0/';
+
+*App::wsgetmail::MS365::Client::_new_useragent = sub {
+    my $useragent = Test::LWP::UserAgent->new;
+    # map oauth request to get token
+    $useragent->map_response( qr{login.windows.net/} => get_mocked_token_response() );
+
+    # map folders list request/response
+    $useragent->map_response( qr{$graph_v1_url/me/mailFolders[^/]} => get_mocked_folders_list_response() );
+
+    # map message list request/response
+    $useragent->map_response( qr{$graph_v1_url/me/mailFolders/} => get_mocked_messages_response() );
+
+    return $useragent;
+};
+
+my $getmail = App::wsgetmail::MS365->new($test_config);
+isa_ok($getmail, 'App::wsgetmail::MS365');
+
+my $message1 = $getmail->get_next_message();
+isa_ok($message1, 'App::wsgetmail::MS365::Message');
+is($message1->id, "xxxxxxxxabc1", "first message fetched with correct id");
+
+my $message2 = $getmail->get_next_message();
+isa_ok($message2, 'App::wsgetmail::MS365::Message');
+is($message2->id, "xxxxxxxxxxxxxxxxxxxxabc2=", "next message fetched with correct id");
+
+done_testing();
+
+#####
+
+sub get_mocked_folders_list_response {
+    return HTTP::Response->new('200', 'OK', ['Content-Type' => 'application/json'], '{ "value": [ {
+                         "displayName":"another_folder",
+                         "totalItemCount":2,
+                         "parentFolderId":"AAMkADFmYTUxODNlLThiZWUtNGI5MS1iOGUyLTBkOGZhYmNhYWIyNgAuAAAAAAA4V3dDNK4_Rpd86g3XzTaIAQBY_b0kgtxhRpNG1KbQD9hUAAAAAAEIAAA=",
+                         "childFolderCount":0,
+                         "unreadItemCount":2,
+                         "id":"AAMkADFmYTUxODNlLThiZWUtNGI5MS1iOGUyLTBkOGZhYmNhYWIyNgAuAAAAAAA4V3dDNK4_Rpd86g3XzTaIAQBY_b0kgtxhRpNG1KbQD9hUAAAKstICAAA="
+                     } ]}');
+}
+
+sub get_mocked_token_response {
+    return HTTP::Response->new('200', 'OK', ['Content-Type' => 'application/json'],
+                               '{"token_type":"Bearer","scope":"email Mail.Read Mail.Read.Shared Mail.ReadWrite Mail.ReadWrite.Shared openid User.Read","expires_in":"3599","ext_expires_in":"3599","expires_on":"1591286563","not_before":"1591282663","resource":"https://graph.microsoft.com/","access_token":"xxxxxxxxN2tpxxxxxxxxxxxxxxxx","refresh_token":"ABBBAAAAAAAm-xxxxx-xxxxxxxxxxxxxxx-xxx","id_token":"xxxxxxxxxxxeyJ0eXAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}'
+                           );
+}
+
+
+sub get_mocked_messages_response {
+    my $json = read_file('t/mock_responses/messages.json');
+    return HTTP::Response->new('200', 'OK', ['Content-Type' => 'application/json'], $json);
+}
+
+sub get_test_config {
+    return {
+        "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/t/mock_responses/messages.json b/t/mock_responses/messages.json
new file mode 100644
index 0000000..80e454c
--- /dev/null
+++ b/t/mock_responses/messages.json
@@ -0,0 +1,98 @@
+{
+    "value": [
+        { 
+            "from": {
+                "emailAddress": {
+                    "address": "at at example.tlf",
+                    "name": "Aaron Testing"
+                }
+            },
+            "bodyPreview": "this is yet another test ticket again",
+            "flag": {
+                "flagStatus": "notFlagged"
+            },
+            "categories": [],
+            "replyTo": [],
+            "conversationIndex": "xxxxxx==",
+            "subject": "yet another test ticket again",
+            "conversationId": "xxxxxxxxxxxxxx=",
+            "ccRecipients": [],
+            "toRecipients": [
+                {
+                    "emailAddress": {
+                        "address": "rtdev at example.tld",
+                        "name": "rt dev"
+                    }
+                }
+            ],
+            "@odata.etag": "W/xxxx",
+            "receivedDateTime": "2020-06-01T15:10:57Z",
+            "id": "xxxxxxxxabc1",
+            "webLink": "https://outlook.office365.com/owa/?ItemID=zzzzzzzz&exvsurl=1&viewmodel=ReadMessageItem",
+            "sentDateTime": "2020-06-01T15:10:52Z",
+            "inferenceClassification": "focused",
+            "sender": {
+                "emailAddress": {
+                    "address": "at at example.tlf",
+                    "name": "Aaron Testing"
+                }
+            },
+            "body": {
+                "contentType": "text",
+                "content": "this is yet another test ticket again"
+            },
+
+            "createdDateTime": "2020-06-01T15:10:57Z",
+            "importance": "normal",
+            "lastModifiedDateTime": "2020-06-02T14:58:25Z",
+            "parentFolderId": "xxxxxxxxxx",
+            "bccRecipients": []
+        },
+        {
+            "bodyPreview": "this is a test ticket to test pagination",
+            "flag": {
+                "flagStatus": "notFlagged"
+            },
+            "from": {
+                "emailAddress": {
+                    "address": "at at example.tlf",
+                    "name": "Aaron Testing"
+                }
+            },
+            "conversationId": "xxxxxxxxxxxxxxxxxxxxxxxxxx=",
+            "categories": [],
+            "replyTo": [],
+            "conversationIndex": "xxxxxxxxxxxxxxxxx==",
+            "subject": "a test ticket",
+            "sentDateTime": "2020-06-01T15:10:27Z",
+            "inferenceClassification": "focused",
+            "receivedDateTime": "2020-06-01T15:10:33Z",
+            "id": "xxxxxxxxxxxxxxxxxxxxabc2=",
+            "toRecipients": [
+                {
+                    "emailAddress": {
+                        "name": "rt dev",
+                        "address": "rtdev at example.tld"
+                    }
+                }
+            ],
+            "@odata.etag": "W/xxxxxxxxxxxxxxxxxx",
+            "ccRecipients": [],
+            "bccRecipients": [],
+            "parentFolderId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=",
+            "importance": "normal",
+            "lastModifiedDateTime": "2020-06-01T15:10:33Z",
+            "createdDateTime": "2020-06-01T15:10:33Z",
+            "sender": {
+                "emailAddress": {
+                    "address": "at at example.tlf",
+                    "name": "Aaron Testing"
+                }
+            },
+            "body": {
+                "content": "this is a test ticket to test pagination",
+                "contentType": "text"
+            }
+        }
+    ]
+}

commit a7b1bb25c65ba5f1bb7fd894d2c409e836ffcb8d
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Fri Jun 5 14:00:48 2020 +0100

    Improved documentation and error handling for wsgetmail

diff --git a/lib/App/wsgetmail/MS365.pm b/lib/App/wsgetmail/MS365.pm
index 077277d..7c50ce4 100644
--- a/lib/App/wsgetmail/MS365.pm
+++ b/lib/App/wsgetmail/MS365.pm
@@ -169,7 +169,10 @@ sub get_message_mime_content {
     my @path_parts = ($self->global_access) ? ('users', $self->username, 'messages', $message_id, '$value') : ('me', 'messages', $message_id, '$value');
 
     my $response = $self->_client->get_request([@path_parts]);
-    # add error checking here!
+    unless ($response->is_success) {
+        warn "failed to fetch message $message_id " . $response->status_line;
+        return undef;
+    }
 
     # can we just write straight to file from response?
     my $tmp = File::Temp->new( UNLINK => 0, SUFFIX => '.mime' );
@@ -187,7 +190,10 @@ sub delete_message {
     my ($self, $message_id) = @_;
     my @path_parts = ($self->global_access) ? ('users', $self->username, 'messages', $message_id) : ('me', 'messages', $message_id);
     my $response = $self->_client->delete_request([@path_parts]);
-    # add error checking here!
+    unless ($response->is_success) {
+        warn "failed to mark message as read " . $response->status_line;
+    }
+
     return $response;
 }
 
@@ -203,7 +209,10 @@ sub mark_message_as_read {
     my $response = $self->_client->patch_request([@path_parts],
                                                  {'Content-type'=> 'application/json',
                                                   Content => encode_json({isRead => $JSON::true }) });
-    # add error checking here!
+    unless ($response->is_success) {
+        warn "failed to mark message as read " . $response->status_line;
+    }
+
     return $response;
 }
 
@@ -221,7 +230,11 @@ sub get_folder_details {
     my $response = $self->_client->get_request(
         [@path_parts], { '$filter' => "DisplayName eq '$folder_name'" }
     );
-    # add error handling
+    unless ($response->is_success) {
+        warn "failed to fetch folder detail " . $response->status_line;
+        return undef;
+    }
+
     my $folders = decode_json( $response->content );
     return $folders->{value}[0];
 }
@@ -237,6 +250,11 @@ sub _fetch_messages {
     my ($decoded_response);
     if ($self->_next_fetch_url) {
         my $response = $self->_client->get_request_by_url($self->_next_fetch_url);
+        unless ($response->is_success) {
+            warn "failed to fetch messages " . $response->status_line;
+            $self->_have_messages_to_fetch(0);
+            return 0;
+        }
         $decoded_response = decode_json( $response->content );
     } else {
         my $fields = [qw(id subject sender isRead sentDateTime toRecipients parentFolderId categories)];
@@ -258,12 +276,17 @@ sub _get_message_list {
     my ($self, $fields, $filter) = @_;
 
     my $folder = $self->get_folder_details;
+    unless ($folder) {
+        warn "unable to fetch messages, can't find folder " . $self->folder;
+        return { '@odata.count' => 0, value => [ ] };
+    }
 
     # don't request list if folder has no items
     unless ($folder->{totalItemCount} > 0) {
         return { '@odata.count' => 0, value => [ ] };
     }
     $filter ||= $self->_get_message_filters;
+
     #TODO: handle filtering multiple folders using filters
     my @path_parts = ($self->global_access) ? ( 'users', $self->username, 'mailFolders', $folder->{id}, 'messages' ) : ( 'me', 'mailFolders', $folder->{id}, 'messages' );
 
@@ -276,9 +299,13 @@ sub _get_message_list {
             ( $filter ? ('$filter' => $filter ) : ( ))
         }
     );
-    # add error checking here!
-    my $message_list = decode_json( $response->content );
-    return $message_list;
+
+    unless ($response->is_success) {
+        warn "failed to fetch messages " . $response->status_line;
+        return { value => [ ] };
+    }
+
+    return decode_json( $response->content );
 }
 
 sub _get_message_filters {
@@ -322,6 +349,54 @@ 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
+
+=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
@@ -330,6 +405,8 @@ sub _build_client {
 
 =item App::wsgetmail::MS365::Message
 
+=item L<https://docs.microsoft.com/en-gb/azure/active-directory/develop/quickstart-register-app>
+
 =back
 
 =head1 LICENSE AND COPYRIGHT
diff --git a/lib/App/wsgetmail/MS365/Client.pm b/lib/App/wsgetmail/MS365/Client.pm
index dfcbd24..a0a5696 100644
--- a/lib/App/wsgetmail/MS365/Client.pm
+++ b/lib/App/wsgetmail/MS365/Client.pm
@@ -169,7 +169,7 @@ sub patch_request {
 
 sub _build_authorised_ua {
     my $self = shift;
-    my $ua = LWP::UserAgent->new();
+    my $ua = $self->_new_useragent;
     $ua->default_header( Authorization => $self->_access_token() );
     return $ua;
 }
@@ -188,7 +188,7 @@ sub _build__access_token {
 
 sub _get_user_access_token {
     my $self = shift;
-    my $ua = LWP::UserAgent->new();
+    my $ua = $self->_new_useragent;
     my $access_token;
     my $oauth_login_url = sprintf('https://login.windows.net/%s/oauth2/token', $self->tenant_id);
     my $response = $ua->post( $oauth_login_url,
@@ -225,6 +225,9 @@ sub _build__credentials {
     return $creds;
 }
 
+sub _new_useragent {
+    return LWP::UserAgent->new();
+}
 
 =head1 SEE ALSO
 

commit 150f505e2cc008fbe7d83163d1549363cc2d092f
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Thu Jun 4 13:06:48 2020 +0100

    Documentation and pod2usage/cli options for wsgetmail365

diff --git a/bin/wsgetmail365 b/bin/wsgetmail365
index dbff7b3..e0b6dfd 100644
--- a/bin/wsgetmail365
+++ b/bin/wsgetmail365
@@ -5,10 +5,16 @@ use JSON;
 use App::wsgetmail::MS365;
 use Data::Dumper;
 use File::Slurp;
+use Pod::Usage;
+use Getopt::Long;
 
-my $config_file = $ARGV[0];
+my $config_file;
+my ($verbose, $help) = (0,0);
 
-die "No config filename provided" unless ($config_file);
+GetOptions('help|?' => \$help, "verbose" => \$verbose, "config=s" => \$config_file);
+
+pod2usage(1) if $help;
+pod2usage(1) unless ($config_file);
 die "Can't find config file $config_file" unless (-f $config_file);
 
 my $config_json = read_file($config_file);
@@ -42,3 +48,56 @@ while (my $message = $getmail->get_next_message()) {
 
 print "\nprocessed $count messages\n";
 
+
+__END__
+
+=head1 NAME
+
+wsgetmail365 - get mail from microsoft365 cloud
+
+=head1 DESCRIPTION
+
+get mail from microsoft365 Exchange Online
+
+=head1 SYNOPSIS
+
+wsgetmail365 --configuration path/to/file.json [--debug]
+
+=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 :
+{
+  "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"
+}
+
+an example configuration file is included in the docs/ directory of this package
+
+=cut
diff --git a/doc/activedirectory_setup.md b/doc/activedirectory_setup.md
new file mode 100644
index 0000000..4c4f187
--- /dev/null
+++ b/doc/activedirectory_setup.md
@@ -0,0 +1,62 @@
+# 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/register_api_app1.png b/doc/register_api_app1.png
new file mode 100644
index 0000000..ba16ae6
Binary files /dev/null and b/doc/register_api_app1.png differ
diff --git a/doc/register_api_app2.png b/doc/register_api_app2.png
new file mode 100644
index 0000000..affed24
Binary files /dev/null and b/doc/register_api_app2.png differ
diff --git a/doc/register_api_app3.png b/doc/register_api_app3.png
new file mode 100644
index 0000000..94bca17
Binary files /dev/null and b/doc/register_api_app3.png differ
diff --git a/doc/register_api_app4.png b/doc/register_api_app4.png
new file mode 100644
index 0000000..a2f5f73
Binary files /dev/null and b/doc/register_api_app4.png differ
diff --git a/doc/register_api_app5.png b/doc/register_api_app5.png
new file mode 100644
index 0000000..82ed701
Binary files /dev/null and b/doc/register_api_app5.png differ
diff --git a/lib/App/wsgetmail.pm b/lib/App/wsgetmail.pm
index 4cf9dd4..af21e47 100644
--- a/lib/App/wsgetmail.pm
+++ b/lib/App/wsgetmail.pm
@@ -1,6 +1,5 @@
 package App::wsgetmail;
 
-use 5.006;
 use strict;
 use warnings;
 
@@ -26,66 +25,58 @@ configurable action with the fetched email.
 
 =head1 SYNOPSIS
 
-    use App::wsgetmail;
-
-    my $foo = App::wsgetmail->new();
-    ...
-
-=head1 SUBROUTINES/METHODS
-
-=head2 function1
-
-=cut
-
-sub function1 {
+wsgetmail365 --configuration path/to/file.json [--debug]
+
+=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 :
+{
+  "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"
 }
 
-=head2 function2
-
-=cut
-
-=head1 AUTHOR
-
-Aaron Trevena, C<< <ast at bestpractical.com> >>
-
-=head1 BUGS
-
-Please report any bugs or feature requests to C<bug-app-wsgetmail at rt.cpan.org>, or through
-the web interface at L<https://rt.cpan.org/NoAuth/ReportBug.html?Queue=App-wsgetmail>.  I will be notified, and then you'll
-automatically be notified of progress on your bug as I make changes.
+an example configuration file is included in the docs/ directory of this package
 
-=head1 SUPPORT
-
-You can find documentation for this module with the perldoc command.
-
-    perldoc App::wsgetmail
-
-
-You can also look for information at:
+=head1 SEE ALSO
 
 =over 4
 
-=item * RT: CPAN's request tracker (report bugs here)
-
-L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=App-wsgetmail>
-
-=item * AnnoCPAN: Annotated CPAN documentation
-
-L<http://annocpan.org/dist/App-wsgetmail>
+=item App::wsgetmail::MS365
 
-=item * CPAN Ratings
-
-L<https://cpanratings.perl.org/d/App-wsgetmail>
-
-=item * Search CPAN
-
-L<https://metacpan.org/release/App-wsgetmail>
+=item wsgemail365
 
 =back
 
+=head1 AUTHOR
 
-=head1 ACKNOWLEDGEMENTS
-
+Aaron Trevena, C<< <ast at bestpractical.com> >>
 
 =head1 LICENSE AND COPYRIGHT
 
@@ -95,7 +86,6 @@ This is free software, licensed under:
 
   The Artistic License 2.0 (GPL Compatible)
 
-
 =cut
 
 1; # End of App::wsgetmail
diff --git a/lib/App/wsgetmail/Handler/RT/Mailgate.pm b/lib/App/wsgetmail/Handler/RT/Mailgate.pm
index 9f3a57d..9e4c4d4 100644
--- a/lib/App/wsgetmail/Handler/RT/Mailgate.pm
+++ b/lib/App/wsgetmail/Handler/RT/Mailgate.pm
@@ -45,12 +45,6 @@ has command_path  => (
     required => 0
 );
 
-# rt-mailgate --action (correspond|comment|...) --queue queuename
-#                 --url http://your.rt.server/
-#                 [ --debug ]
-#                 [ --extension (queue|action|ticket) ]
-#                 [ --timeout seconds ]
-
 my $option_configuration = {
     url => 'string',
     action => [qw(correspond comment)],
diff --git a/lib/App/wsgetmail/MS365.pm b/lib/App/wsgetmail/MS365.pm
index 587c451..077277d 100644
--- a/lib/App/wsgetmail/MS365.pm
+++ b/lib/App/wsgetmail/MS365.pm
@@ -4,10 +4,6 @@ package App::wsgetmail::MS365;
 
 App::wsgetmail::MS365 - Fetch mail from Microsoft 365
 
-=head1 DESCRIPTION
-
-Fetch mail from Microsoft 365 mailboxes using the Graph REST API
-
 =cut
 
 use Moo;
@@ -17,19 +13,35 @@ use App::wsgetmail::MS365::Client;
 use App::wsgetmail::MS365::Message;
 use File::Temp;
 
+=head1 DESCRIPTION
+
+Moo class providing methods to connect to and fetch mail from Microsoft 365
+ mailboxes using the Graph REST API.
+
 =head1 ATTRIBUTES
 
 =over 4
 
+=item client_id
+
+=item tenant_id
+
+=item username
+
+=item user_password
+
+=item global_access
+
+=item secret
+
+=item folder
+
+=item handler_options
+
 =back
 
 =cut
 
-has secret  => (
-    is => 'ro',
-    required => 0,
-);
-
 has client_id => (
     is => 'ro',
     required => 1,
@@ -61,10 +73,9 @@ has global_access => (
     default => sub { return 0 }
 );
 
-has _delivery_handler => (
+has secret  => (
     is => 'ro',
-    lazy => 1,
-    builder => '_build_handler'
+    required => 0,
 );
 
 has handler_options => (
@@ -72,6 +83,14 @@ has handler_options => (
     required => 1,
 );
 
+#
+
+has _delivery_handler => (
+    is => 'ro',
+    lazy => 1,
+    builder => '_build_handler'
+);
+
 has _client => (
     is => 'ro',
     lazy => 1,
@@ -104,24 +123,32 @@ has _handler => (
 );
 
 our $handlers = {
-    SimpleFile => 'App::wsgetmail::Handler::RT::SimpleFile',
     Mailgate => 'App::wsgetmail::Handler::RT::Mailgate',
 };
 
 =head1 METHODS
 
+=head2 new
+
+Class constructor method, returns new App::wsgetmail::MS365 object
+
 =head2 get_next_message
 
+Object method, returns the next message as an App::wsgetmail::MS365::Message object if there is one.
+
+Will lazily fetch messages until the list is exhausted.
+
 =cut
 
 sub get_next_message {
     my ($self) = @_;
     my $next_message;
-    # do we have messages
+
+    # check for already fetched messages, otherwise fetch more
     my $message_details = shift @{$self->_fetched_messages};
     unless ( $message_details ) {
         if ($self->_have_messages_to_fetch) {
-            $self->fetch_messages();
+            $self->_fetch_messages();
             $message_details = shift @{$self->_fetched_messages};
         }
     }
@@ -131,65 +158,10 @@ sub get_next_message {
     return $next_message;
 }
 
-
-sub fetch_messages {
-    my ($self, $filter) = @_;
-    my $messages = [ ];
-    my $fetched_count = 0;
-    # check if expecting to fetch more using result paging
-    my ($decoded_response);
-    if ($self->_next_fetch_url) {
-        my $response = $self->_client->get_request_by_url($self->_next_fetch_url);
-        $decoded_response = decode_json( $response->content );
-    } else {
-        my $fields = [qw(id subject sender isRead sentDateTime toRecipients parentFolderId categories)];
-        $decoded_response = $self->get_message_list($fields, $filter);
-    }
-
-    $messages = $decoded_response->{value};
-    if ($decoded_response->{'@odata.nextLink'}) {
-        $self->_next_fetch_url($decoded_response->{'@odata.nextLink'});
-        $self->_have_messages_to_fetch(1);
-    } else {
-        $self->_have_messages_to_fetch(0);
-    }
-    $self->_fetched_messages($messages);
-    return $fetched_count;
-}
-
-=head2 get_message_list
-
-=cut
-
-sub get_message_list {
-    my ($self, $fields, $filter) = @_;
-
-    my $folder = $self->get_folder_details;
-
-    # don't request list if folder has no items
-    unless ($folder->{totalItemCount} > 0) {
-        return { '@odata.count' => 0, value => [ ] };
-    }
-    $filter ||= $self->_get_message_filters;
-    #TODO: handle filtering multiple folders using filters
-    my @path_parts = ($self->global_access) ? ( 'users', $self->username, 'mailFolders', $folder->{id}, 'messages' ) : ( 'me', 'mailFolders', $folder->{id}, 'messages' );
-
-    # get oldest first, filter (i.e. unread) if filter provided
-    my $response = $self->_client->get_request(
-        [@path_parts],
-        {
-            '$count' => 'true', '$orderby' => 'sentDateTime',
-            ( $fields ? ('$select' => join(',',@$fields)  ) : ( )),
-            ( $filter ? ('$filter' => $filter ) : ( ))
-        }
-    );
-    # add error checking here!
-    my $message_list = decode_json( $response->content );
-    return $message_list;
-}
-
 =head2 get_message_mime_content
 
+Object method, takes message id and returns filename of fetched raw mime file for that message.
+
 =cut
 
 sub get_message_mime_content {
@@ -205,6 +177,12 @@ sub get_message_mime_content {
     return $tmp->filename;
 }
 
+=head2 delete_message
+
+Object method, takes message id and deletes that message from the outlook365 mailbox
+
+=cut
+
 sub delete_message {
     my ($self, $message_id) = @_;
     my @path_parts = ($self->global_access) ? ('users', $self->username, 'messages', $message_id) : ('me', 'messages', $message_id);
@@ -213,6 +191,12 @@ sub delete_message {
     return $response;
 }
 
+=head2 mark_message_as_read
+
+Object method, takes message id and marks that message as read in the outlook365 mailbox
+
+=cut
+
 sub mark_message_as_read {
     my ($self, $message_id) = @_;
     my @path_parts = ($self->global_access) ? ('users', $self->username, 'messages', $message_id) : ('me', 'messages', $message_id);
@@ -224,6 +208,12 @@ sub mark_message_as_read {
 }
 
 
+=head2 get_folder_details
+
+Object method, returns hashref of details of the configured mailbox folder.
+
+=cut
+
 sub get_folder_details {
     my $self = shift;
     my $folder_name = $self->folder;
@@ -239,6 +229,58 @@ sub get_folder_details {
 
 ##############
 
+sub _fetch_messages {
+    my ($self, $filter) = @_;
+    my $messages = [ ];
+    my $fetched_count = 0;
+    # check if expecting to fetch more using result paging
+    my ($decoded_response);
+    if ($self->_next_fetch_url) {
+        my $response = $self->_client->get_request_by_url($self->_next_fetch_url);
+        $decoded_response = decode_json( $response->content );
+    } else {
+        my $fields = [qw(id subject sender isRead sentDateTime toRecipients parentFolderId categories)];
+        $decoded_response = $self->_get_message_list($fields, $filter);
+    }
+
+    $messages = $decoded_response->{value};
+    if ($decoded_response->{'@odata.nextLink'}) {
+        $self->_next_fetch_url($decoded_response->{'@odata.nextLink'});
+        $self->_have_messages_to_fetch(1);
+    } else {
+        $self->_have_messages_to_fetch(0);
+    }
+    $self->_fetched_messages($messages);
+    return $fetched_count;
+}
+
+sub _get_message_list {
+    my ($self, $fields, $filter) = @_;
+
+    my $folder = $self->get_folder_details;
+
+    # don't request list if folder has no items
+    unless ($folder->{totalItemCount} > 0) {
+        return { '@odata.count' => 0, value => [ ] };
+    }
+    $filter ||= $self->_get_message_filters;
+    #TODO: handle filtering multiple folders using filters
+    my @path_parts = ($self->global_access) ? ( 'users', $self->username, 'mailFolders', $folder->{id}, 'messages' ) : ( 'me', 'mailFolders', $folder->{id}, 'messages' );
+
+    # get oldest first, filter (i.e. unread) if filter provided
+    my $response = $self->_client->get_request(
+        [@path_parts],
+        {
+            '$count' => 'true', '$orderby' => 'sentDateTime',
+            ( $fields ? ('$select' => join(',',@$fields)  ) : ( )),
+            ( $filter ? ('$filter' => $filter ) : ( ))
+        }
+    );
+    # add error checking here!
+    my $message_list = decode_json( $response->content );
+    return $message_list;
+}
+
 sub _get_message_filters {
     my $self = shift;
     #TODO: handle filtering multiple folders
@@ -286,6 +328,8 @@ sub _build_client {
 
 =item App::wsgetmail::MS365::Client
 
+=item App::wsgetmail::MS365::Message
+
 =back
 
 =head1 LICENSE AND COPYRIGHT
diff --git a/lib/App/wsgetmail/MS365/Client.pm b/lib/App/wsgetmail/MS365/Client.pm
index 3b03df9..dfcbd24 100644
--- a/lib/App/wsgetmail/MS365/Client.pm
+++ b/lib/App/wsgetmail/MS365/Client.pm
@@ -16,8 +16,6 @@ use Azure::AD::ClientCredentials;
 =head1 DESCRIPTION
 
 Fetch mail from Microsoft 365 mailboxes using the Graph REST API
-=head1 SYNOPSIS
-
 
 =head1 ATTRIBUTES
 
@@ -228,13 +226,11 @@ sub _build__credentials {
 }
 
 
-
-
 =head1 SEE ALSO
 
 =over 4
 
-=item App::wsgetmail::Client
+=item App::wsgetmail::MS365
 
 =back
 
diff --git a/lib/App/wsgetmail/MS365/Message.pm b/lib/App/wsgetmail/MS365/Message.pm
index 3a4fa79..331632d 100644
--- a/lib/App/wsgetmail/MS365/Message.pm
+++ b/lib/App/wsgetmail/MS365/Message.pm
@@ -1,6 +1,28 @@
 package App::wsgetmail::MS365::Message;
 use Moo;
 
+=head1 NAME
+
+App::wsgetmail::MS365::Message
+
+=head2 DESCRIPTION
+
+Simple Moo class representing an microsoft/outlook 365 message.
+
+=head2 ACCESSORS
+
+=over 4
+
+=item id
+
+=item status
+
+=item recipients
+
+=back
+
+=cut
+
 has id => (
     is => 'ro',
     required => 1
@@ -35,4 +57,27 @@ around BUILDARGS => sub {
   return $class->$orig($args);
 };
 
+=head1 SEE ALSO
+
+=over 4
+
+=item App::wsgetmail::MS365
+
+=back
+
+=head1 AUTHOR
+
+Aaron Trevena, C<< <ast 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)
+
+=cut
+
+
 1;

commit 95207a20e8782ef53165ed27ab73229bf12784ac
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Tue Jun 2 20:18:38 2020 +0100

    fixed naming of MS365 integration

diff --git a/bin/wsgetmail360 b/bin/wsgetmail365
similarity index 93%
rename from bin/wsgetmail360
rename to bin/wsgetmail365
index b532261..dbff7b3 100644
--- a/bin/wsgetmail360
+++ b/bin/wsgetmail365
@@ -2,7 +2,7 @@
 
 use strict;
 use JSON;
-use App::wsgetmail::MS360;
+use App::wsgetmail::MS365;
 use Data::Dumper;
 use File::Slurp;
 
@@ -15,7 +15,7 @@ my $config_json = read_file($config_file);
 
 my $config = decode_json($config_json);
 
-my $getmail = App::wsgetmail::MS360->new($config);
+my $getmail = App::wsgetmail::MS365->new($config);
 
 print "\nfetching mail using configuration $config_file\n";
 
diff --git a/lib/App/wsgetmail/MS360.pm b/lib/App/wsgetmail/MS365.pm
similarity index 94%
rename from lib/App/wsgetmail/MS360.pm
rename to lib/App/wsgetmail/MS365.pm
index 5527247..587c451 100644
--- a/lib/App/wsgetmail/MS360.pm
+++ b/lib/App/wsgetmail/MS365.pm
@@ -1,20 +1,20 @@
-package App::wsgetmail::MS360;
+package App::wsgetmail::MS365;
 
 =head1 NAME
 
-App::wsgetmail::MS360 - Fetch mail from Microsoft 360
+App::wsgetmail::MS365 - Fetch mail from Microsoft 365
 
 =head1 DESCRIPTION
 
-Fetch mail from Microsoft 360 mailboxes using the Graph REST API
+Fetch mail from Microsoft 365 mailboxes using the Graph REST API
 
 =cut
 
 use Moo;
 use JSON;
 
-use App::wsgetmail::MS360::Client;
-use App::wsgetmail::MS360::Message;
+use App::wsgetmail::MS365::Client;
+use App::wsgetmail::MS365::Message;
 use File::Temp;
 
 =head1 ATTRIBUTES
@@ -126,7 +126,7 @@ sub get_next_message {
         }
     }
     if (defined $message_details) {
-        $next_message = App::wsgetmail::MS360::Message->new($message_details);
+        $next_message = App::wsgetmail::MS365::Message->new($message_details);
     }
     return $next_message;
 }
@@ -267,7 +267,7 @@ sub _build__handler {
 
 sub _build_client {
     my $self = shift;
-    my $client = App::wsgetmail::MS360::Client->new( {
+    my $client = App::wsgetmail::MS365::Client->new( {
         client_id => $self->client_id,
         username => $self->username,
         user_password => $self->user_password,
@@ -284,7 +284,7 @@ sub _build_client {
 
 =over 4
 
-=item App::wsgetmail::MS360::Client
+=item App::wsgetmail::MS365::Client
 
 =back
 
diff --git a/lib/App/wsgetmail/MS360/Client.pm b/lib/App/wsgetmail/MS365/Client.pm
similarity index 96%
rename from lib/App/wsgetmail/MS360/Client.pm
rename to lib/App/wsgetmail/MS365/Client.pm
index e7b54dc..3b03df9 100644
--- a/lib/App/wsgetmail/MS360/Client.pm
+++ b/lib/App/wsgetmail/MS365/Client.pm
@@ -1,8 +1,8 @@
-package App::wsgetmail::MS360::Client;
+package App::wsgetmail::MS365::Client;
 
 =head1 NAME
 
-App::wsgetmail::MS360 - Fetch mail from Microsoft 360
+App::wsgetmail::MS365 - Fetch mail from Microsoft 365
 
 =cut
 
@@ -15,7 +15,7 @@ use Azure::AD::ClientCredentials;
 
 =head1 DESCRIPTION
 
-Fetch mail from Microsoft 360 mailboxes using the Graph REST API
+Fetch mail from Microsoft 365 mailboxes using the Graph REST API
 =head1 SYNOPSIS
 
 
diff --git a/lib/App/wsgetmail/MS360/Message.pm b/lib/App/wsgetmail/MS365/Message.pm
similarity index 92%
rename from lib/App/wsgetmail/MS360/Message.pm
rename to lib/App/wsgetmail/MS365/Message.pm
index af54af2..3a4fa79 100644
--- a/lib/App/wsgetmail/MS360/Message.pm
+++ b/lib/App/wsgetmail/MS365/Message.pm
@@ -1,4 +1,4 @@
-package App::wsgetmail::MS360::Message;
+package App::wsgetmail::MS365::Message;
 use Moo;
 
 has id => (

commit efa09cb304003bacd7ec4a7ab4ea0b88ef7dbd04
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Tue Jun 2 20:13:37 2020 +0100

    Improved filtering and added folder handling to outlook365 integration

diff --git a/bin/wsgetmail360 b/bin/wsgetmail360
index ce17355..b532261 100644
--- a/bin/wsgetmail360
+++ b/bin/wsgetmail360
@@ -21,18 +21,17 @@ print "\nfetching mail using configuration $config_file\n";
 
 my $count = 0;
 my $fetched_action_method;
-my $action = $config->{action_on_fetched};
+my $action = $config->{handler_options}{action_on_fetched};
 my $filter;
 if (lc($action) eq 'mark_as_read') {
     $fetched_action_method = 'mark_message_as_read';
-    $filter = 'isRead eq false';
 } elsif ( lc($action) eq "delete" ) {
     $fetched_action_method = 'delete_message';
 } else {
     warn "no recognised action for fetched mail, mailbox not updated";
 }
 
-while (my $message = $getmail->get_next_message($filter)) {
+while (my $message = $getmail->get_next_message()) {
     my $file = $getmail->get_message_mime_content($message->id);
     my $forwarded_ok = $getmail->forward($message, $file);
     if ($forwarded_ok) {
diff --git a/doc/example.conf b/doc/example.conf
index a5badc9..53a1b40 100644
--- a/doc/example.conf
+++ b/doc/example.conf
@@ -4,11 +4,13 @@
      "debug":"1",
      "class":"Mailgate",
      "command_path":"/path/to/rt/bin",
-     "recipient":"rt at example.tld"
+     "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"
+   "action_on_fetched":"mark_as_read",
+   "folder":"Inbox"
 }
diff --git a/lib/App/wsgetmail/MS360.pm b/lib/App/wsgetmail/MS360.pm
index 5e3526b..5527247 100644
--- a/lib/App/wsgetmail/MS360.pm
+++ b/lib/App/wsgetmail/MS360.pm
@@ -50,6 +50,12 @@ has user_password => (
     required => 0
 );
 
+has folder => (
+    is => 'ro',
+    required => 0,
+    default => sub { 'Inbox' }
+);
+
 has global_access => (
     is => 'ro',
     default => sub { return 0 }
@@ -109,13 +115,13 @@ our $handlers = {
 =cut
 
 sub get_next_message {
-    my ($self, $filter) = @_;
+    my ($self) = @_;
     my $next_message;
     # do we have messages
     my $message_details = shift @{$self->_fetched_messages};
     unless ( $message_details ) {
         if ($self->_have_messages_to_fetch) {
-            $self->fetch_messages($filter);
+            $self->fetch_messages();
             $message_details = shift @{$self->_fetched_messages};
         }
     }
@@ -157,7 +163,16 @@ sub fetch_messages {
 
 sub get_message_list {
     my ($self, $fields, $filter) = @_;
-    my @path_parts = ($self->global_access) ? ( 'users', $self->username, 'messages' ) : ( 'me', 'messages' );
+
+    my $folder = $self->get_folder_details;
+
+    # don't request list if folder has no items
+    unless ($folder->{totalItemCount} > 0) {
+        return { '@odata.count' => 0, value => [ ] };
+    }
+    $filter ||= $self->_get_message_filters;
+    #TODO: handle filtering multiple folders using filters
+    my @path_parts = ($self->global_access) ? ( 'users', $self->username, 'mailFolders', $folder->{id}, 'messages' ) : ( 'me', 'mailFolders', $folder->{id}, 'messages' );
 
     # get oldest first, filter (i.e. unread) if filter provided
     my $response = $self->_client->get_request(
@@ -208,8 +223,35 @@ sub mark_message_as_read {
     return $response;
 }
 
+
+sub get_folder_details {
+    my $self = shift;
+    my $folder_name = $self->folder;
+    my @path_parts = ($self->global_access) ? ('users', $self->username, 'mailFolders' ) : ('me', 'mailFolders');
+    my $response = $self->_client->get_request(
+        [@path_parts], { '$filter' => "DisplayName eq '$folder_name'" }
+    );
+    # add error handling
+    my $folders = decode_json( $response->content );
+    return $folders->{value}[0];
+}
+
+
 ##############
 
+sub _get_message_filters {
+    my $self = shift;
+    #TODO: handle filtering multiple folders
+    my $action = lc($self->handler_options->{action_on_fetched});
+    my $filters = [ ];
+    if (lc($action) eq 'mark_as_read') {
+        push(@$filters, 'isRead eq false');
+    }
+
+    my $filter = join(' ', @$filters);
+    return $filter;
+ }
+
 sub _build__handler {
     my $self = shift;
     my $handler_options = $self->handler_options;

commit 95430f1457eba3b2f6e516516646a92a6c4ea1c6
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Wed May 27 19:59:42 2020 +0100

    End to end import of mails from o360 into rt
    
    Simple implementation of Exchange Online mailgate integration

diff --git a/MANIFEST b/MANIFEST
index bb3809c..9e47df3 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -2,6 +2,8 @@ Changes
 lib/App/wsgetmail.pm
 lib/App/wsgetmail/MS360.pm
 lib/App/wsgetmail/MS360/Client.pm
+lib/App/wsgetmail/Handler/RT/Mailgate.pm
+bin/wsgetmail
 Makefile.PL
 MANIFEST			This list of files
 README
diff --git a/Makefile.PL b/Makefile.PL
index cf8bf2e..116e990 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -20,7 +20,7 @@ my %WriteMakefileArgs = (
         'Azure::AD::ClientCredentials' => 0,
         'LWP::UserAgent' => 0,
         'URI::Escape' => 0,
-        'URI::Query' => 0,
+	'URI' => 0,
         'Moo' => 0,
         'JSON' => -,
 	'IPC::Run => 0'
diff --git a/bin/wsgetmail360 b/bin/wsgetmail360
new file mode 100644
index 0000000..ce17355
--- /dev/null
+++ b/bin/wsgetmail360
@@ -0,0 +1,45 @@
+#!perl
+
+use strict;
+use JSON;
+use App::wsgetmail::MS360;
+use Data::Dumper;
+use File::Slurp;
+
+my $config_file = $ARGV[0];
+
+die "No config filename provided" unless ($config_file);
+die "Can't find config file $config_file" unless (-f $config_file);
+
+my $config_json = read_file($config_file);
+
+my $config = decode_json($config_json);
+
+my $getmail = App::wsgetmail::MS360->new($config);
+
+print "\nfetching mail using configuration $config_file\n";
+
+my $count = 0;
+my $fetched_action_method;
+my $action = $config->{action_on_fetched};
+my $filter;
+if (lc($action) eq 'mark_as_read') {
+    $fetched_action_method = 'mark_message_as_read';
+    $filter = 'isRead eq false';
+} elsif ( lc($action) eq "delete" ) {
+    $fetched_action_method = 'delete_message';
+} else {
+    warn "no recognised action for fetched mail, mailbox not updated";
+}
+
+while (my $message = $getmail->get_next_message($filter)) {
+    my $file = $getmail->get_message_mime_content($message->id);
+    my $forwarded_ok = $getmail->forward($message, $file);
+    if ($forwarded_ok) {
+        $getmail->$fetched_action_method($message->id);
+        $count++;
+    }
+}
+
+print "\nprocessed $count messages\n";
+
diff --git a/doc/example.conf b/doc/example.conf
new file mode 100644
index 0000000..a5badc9
--- /dev/null
+++ b/doc/example.conf
@@ -0,0 +1,14 @@
+{
+  "handler_options":{
+     "url":"http://rt.example.tld/",
+     "debug":"1",
+     "class":"Mailgate",
+     "command_path":"/path/to/rt/bin",
+     "recipient":"rt at example.tld"
+   },
+   "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"
+}
diff --git a/lib/App/wsgetmail/Handler/RT/Mailgate.pm b/lib/App/wsgetmail/Handler/RT/Mailgate.pm
index 43f3832..9f3a57d 100644
--- a/lib/App/wsgetmail/Handler/RT/Mailgate.pm
+++ b/lib/App/wsgetmail/Handler/RT/Mailgate.pm
@@ -25,11 +25,26 @@ has action => (
     default => sub { 'correspond' }
 );
 
+has queue => (
+    is => 'ro',
+    required => 0
+);
+
 has extension => (
     is => 'ro',
     required => 0
 );
 
+has recipient => (
+    is => 'ro',
+    required => 1,
+);
+
+has command_path  => (
+    is => 'ro',
+    required => 0
+);
+
 # rt-mailgate --action (correspond|comment|...) --queue queuename
 #                 --url http://your.rt.server/
 #                 [ --debug ]
@@ -37,7 +52,6 @@ has extension => (
 #                 [ --timeout seconds ]
 
 my $option_configuration = {
-    debug => 'boolean',
     url => 'string',
     action => [qw(correspond comment)],
     extension => [qw(queue action ticket)],
@@ -45,49 +59,60 @@ my $option_configuration = {
     queue => 'string'
 };
 
-sub forward_mail {
-    my ($self, $filename) = @_;
-
+sub forward {
+    my ($self, $message, $filename) = @_;
     # build arguments
     if ($self->extension) {
-        # parse email, get headers
-        # set env for extension from to header
-        my $entity = MIME::Entity->build(Path => $filename);
-        my ($to, $extension) = split(/\+/,$entity->head->get('To'));
-        if (defined($extension) && $extension =~ m/\W/) {
-            $ENV{EXTENSION} = $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;
+            }
         }
     }
+
+    # force debug to true, use it to confirm success
     my $arguments = [ ];
     foreach my $option (keys %$option_configuration) {
         next unless ($self->$option);
-        if ($option_configuration->{$option} eq 'boolean') {
-            push(@$arguments, "--$option");
-        }
-        elsif ( $option eq 'request_timeout' ) {
+        if ( $option eq 'request_timeout' ) {
             push(@$arguments, "--timeout", $self->request_timeout);
         }
         else {
             push(@$arguments, "--$option", $self->$option);
         }
     }
+    push (@$arguments, "--debug") if ($self->debug);
+
     # run command
-    $self->run_command($filename, $arguments);
+    return $self->_run_command($filename, $arguments);
 }
 
 
 sub _run_command {
     my ($self, $filename, $arguments) = @_;
-    my $ok = 1;
     my $command = 'rt-mailgate';
     if ($self->command_path) {
-        $command = $self->command_path . $command;
+        $command = join('/', $self->command_path, $command);
+    }
+    open my $fh, "<$filename"  or die $!;
+    my ($input,$output, $error);
+    my $ok = run ([ $command, @$arguments], $fh, \$output, \$error, timeout( $self->request_timeout + 5 ) );
+    close $fh;
+
+    unless ($ok) {
+        warn "failed to run rt-mailgate for file $filename : $! $?";
+        return 0;
     }
 
-    my ($output, $error);
-    run [ $command, @$arguments ], '<', $filename, \$output, \$error, timeout( $self->request_timeout + 5 ) or die "failed to run rt-mailgate : $! $?";
-
-    # check for errors
+    if ($self->debug or not $ok) {
+        warn $error;
+    }
 
     return $ok;
 }
diff --git a/lib/App/wsgetmail/MS360.pm b/lib/App/wsgetmail/MS360.pm
index 5f8c66e..5e3526b 100644
--- a/lib/App/wsgetmail/MS360.pm
+++ b/lib/App/wsgetmail/MS360.pm
@@ -14,6 +14,8 @@ use Moo;
 use JSON;
 
 use App::wsgetmail::MS360::Client;
+use App::wsgetmail::MS360::Message;
+use File::Temp;
 
 =head1 ATTRIBUTES
 
@@ -25,7 +27,7 @@ use App::wsgetmail::MS360::Client;
 
 has secret  => (
     is => 'ro',
-    required => 1,
+    required => 0,
 );
 
 has client_id => (
@@ -53,7 +55,7 @@ has global_access => (
     default => sub { return 0 }
 );
 
-has _handler => (
+has _delivery_handler => (
     is => 'ro',
     lazy => 1,
     builder => '_build_handler'
@@ -66,9 +68,35 @@ has handler_options => (
 
 has _client => (
     is => 'ro',
+    lazy => 1,
     builder => '_build_client',
 );
 
+has _fetched_messages => (
+    is => 'rw',
+    required => 0,
+    default => sub { [ ] }
+);
+
+has _have_messages_to_fetch => (
+    is => 'rw',
+    required => 0,
+    default => sub { 1 }
+);
+
+has _next_fetch_url => (
+    is => 'rw',
+    required => 0,
+    default => sub { '' }
+);
+
+has _handler => (
+    is => 'ro',
+    lazy => 1,
+    builder => '_build__handler',
+    handles => [qw/forward/]
+);
+
 our $handlers = {
     SimpleFile => 'App::wsgetmail::Handler::RT::SimpleFile',
     Mailgate => 'App::wsgetmail::Handler::RT::Mailgate',
@@ -76,15 +104,70 @@ our $handlers = {
 
 =head1 METHODS
 
+=head2 get_next_message
+
+=cut
+
+sub get_next_message {
+    my ($self, $filter) = @_;
+    my $next_message;
+    # do we have messages
+    my $message_details = shift @{$self->_fetched_messages};
+    unless ( $message_details ) {
+        if ($self->_have_messages_to_fetch) {
+            $self->fetch_messages($filter);
+            $message_details = shift @{$self->_fetched_messages};
+        }
+    }
+    if (defined $message_details) {
+        $next_message = App::wsgetmail::MS360::Message->new($message_details);
+    }
+    return $next_message;
+}
+
+
+sub fetch_messages {
+    my ($self, $filter) = @_;
+    my $messages = [ ];
+    my $fetched_count = 0;
+    # check if expecting to fetch more using result paging
+    my ($decoded_response);
+    if ($self->_next_fetch_url) {
+        my $response = $self->_client->get_request_by_url($self->_next_fetch_url);
+        $decoded_response = decode_json( $response->content );
+    } else {
+        my $fields = [qw(id subject sender isRead sentDateTime toRecipients parentFolderId categories)];
+        $decoded_response = $self->get_message_list($fields, $filter);
+    }
+
+    $messages = $decoded_response->{value};
+    if ($decoded_response->{'@odata.nextLink'}) {
+        $self->_next_fetch_url($decoded_response->{'@odata.nextLink'});
+        $self->_have_messages_to_fetch(1);
+    } else {
+        $self->_have_messages_to_fetch(0);
+    }
+    $self->_fetched_messages($messages);
+    return $fetched_count;
+}
+
 =head2 get_message_list
 
 =cut
 
 sub get_message_list {
-    my ($self) = @_;
-    # add error handling!
+    my ($self, $fields, $filter) = @_;
     my @path_parts = ($self->global_access) ? ( 'users', $self->username, 'messages' ) : ( 'me', 'messages' );
-    my $response = $self->get_request(@path_parts);
+
+    # get oldest first, filter (i.e. unread) if filter provided
+    my $response = $self->_client->get_request(
+        [@path_parts],
+        {
+            '$count' => 'true', '$orderby' => 'sentDateTime',
+            ( $fields ? ('$select' => join(',',@$fields)  ) : ( )),
+            ( $filter ? ('$filter' => $filter ) : ( ))
+        }
+    );
     # add error checking here!
     my $message_list = decode_json( $response->content );
     return $message_list;
@@ -98,29 +181,48 @@ sub get_message_mime_content {
     my ($self, $message_id) = @_;
     my @path_parts = ($self->global_access) ? ('users', $self->username, 'messages', $message_id, '$value') : ('me', 'messages', $message_id, '$value');
 
-    my $response = $self->get_request(@path_parts);
+    my $response = $self->_client->get_request([@path_parts]);
+    # add error checking here!
+
+    # can we just write straight to file from response?
+    my $tmp = File::Temp->new( UNLINK => 0, SUFFIX => '.mime' );
+    print $tmp $response->content;
+    return $tmp->filename;
+}
+
+sub delete_message {
+    my ($self, $message_id) = @_;
+    my @path_parts = ($self->global_access) ? ('users', $self->username, 'messages', $message_id) : ('me', 'messages', $message_id);
+    my $response = $self->_client->delete_request([@path_parts]);
     # add error checking here!
+    return $response;
+}
 
-    my $raw_message = $response->content;
-    return $raw_message;
+sub mark_message_as_read {
+    my ($self, $message_id) = @_;
+    my @path_parts = ($self->global_access) ? ('users', $self->username, 'messages', $message_id) : ('me', 'messages', $message_id);
+    my $response = $self->_client->patch_request([@path_parts],
+                                                 {'Content-type'=> 'application/json',
+                                                  Content => encode_json({isRead => $JSON::true }) });
+    # add error checking here!
+    return $response;
 }
 
+##############
 
-sub get_handler {
+sub _build__handler {
     my $self = shift;
     my $handler_options = $self->handler_options;
     my $handler_class = delete $handler_options->{class};
     unless ($handler_class && $handlers->{$handler_class}) {
         die "valid class required for handler, classes available " . join(', ', keys %$handlers);
     }
-    require $handlers->{$handler_class};
+    my $classname =  $handlers->{$handler_class};
+    eval "require $classname;";
     my $handler = $handlers->{$handler_class}->new($handler_options);
     return $handler;
 }
 
-
-##############
-
 sub _build_client {
     my $self = shift;
     my $client = App::wsgetmail::MS360::Client->new( {
diff --git a/lib/App/wsgetmail/MS360/Client.pm b/lib/App/wsgetmail/MS360/Client.pm
index 60d2d20..e7b54dc 100644
--- a/lib/App/wsgetmail/MS360/Client.pm
+++ b/lib/App/wsgetmail/MS360/Client.pm
@@ -8,7 +8,7 @@ App::wsgetmail::MS360 - Fetch mail from Microsoft 360
 
 use Moo;
 use URI::Escape;
-use URI::Query;
+use URI;
 use JSON;
 use LWP::UserAgent;
 use Azure::AD::ClientCredentials;
@@ -91,6 +91,7 @@ my $graph_v1_url = 'https://graph.microsoft.com/v1.0/';
 
 sub BUILD {
     my ($self, $args) = @_;
+
     if ($args->{global_access}) {
         unless ($args->{secret}) {
             die "secret is required when using global_access";
@@ -120,23 +121,51 @@ sub build_rest_uri {
 =cut
 
 sub get_request {
-    my ($self, $parts) = @_;
+    my ($self, $parts, $params) = @_;
     # add error handling!
-    my $url = $self->build_rest_uri(@$parts);
+    my $uri = URI->new($self->build_rest_uri(@$parts));
+    $uri->query_form($params) if ($params);
+    return $self->_ua->get($uri);
+}
+
+=head2 get_request_by_url
+
+=cut
+
+sub get_request_by_url {
+    my ($self, $url) = @_;
     return $self->_ua->get($url);
 }
 
-# sub post_request {
-#     my ($self, $path_parts, $post_data) = @_;
-#     my $url = $self->build_rest_uri(@$path_parts);
-#     return $self->_ua->post($url,$post_data);
-# }
+=head2 delete_request
+
+=cut
+
+sub delete_request {
+    my ($self, $parts, $params) = @_;
+    my $url = $self->build_rest_uri(@$parts);
+    return $self->_ua->delete($url);
+}
+
+=head2 post_request
+
+=cut
+
+sub post_request {
+    my ($self, $path_parts, $post_data) = @_;
+    my $url = $self->build_rest_uri(@$path_parts);
+    return $self->_ua->post($url,$post_data);
+}
+
+=head2 patch_request
+
+=cut
 
-# sub patch_request {
-#     my ($self, $path_parts, $patch_data) = @_;
-#     my $url = $self->build_rest_uri(@$path_parts);
-#     return $self->_ua->patch($url,$patch_data);
-# }
+sub patch_request {
+     my ($self, $path_parts, $patch_params) = @_;
+     my $url = $self->build_rest_uri(@$path_parts);
+     return $self->_ua->patch($url,%$patch_params);
+ }
 
 ######
 
@@ -147,7 +176,7 @@ sub _build_authorised_ua {
     return $ua;
 }
 
-sub _build_access_token {
+sub _build__access_token {
     my $self = shift;
     my $access_token;
     if ($self->global_access) {
diff --git a/lib/App/wsgetmail/MS360/Message.pm b/lib/App/wsgetmail/MS360/Message.pm
new file mode 100644
index 0000000..af54af2
--- /dev/null
+++ b/lib/App/wsgetmail/MS360/Message.pm
@@ -0,0 +1,38 @@
+package App::wsgetmail::MS360::Message;
+use Moo;
+
+has id => (
+    is => 'ro',
+    required => 1
+);
+
+has status => (
+    is => 'ro',
+    required => 1
+);
+
+has recipients => (
+    is => 'ro',
+    required => 1
+);
+
+has _details => (
+    is => 'ro',
+    required => 1
+);
+
+# have client
+around BUILDARGS => sub {
+  my ( $orig, $class, $details ) = @_;
+
+  my $args = {
+      id => $details->{id},
+      status => $details->{status},
+      recipients => $details->{toRecipients},
+      _details => $details
+  };
+
+  return $class->$orig($args);
+};
+
+1;

commit b98ebaa3f1f2113ebe119161e9dda63502751b0f
Author: Aaron Trevena <aaron at aarontrevena.co.uk>
Date:   Wed May 27 13:26:05 2020 +0100

    Initial framework for webservice getmail app

diff --git a/Changes b/Changes
new file mode 100644
index 0000000..c8fb631
--- /dev/null
+++ b/Changes
@@ -0,0 +1,5 @@
+Revision history for App-wsgetmail
+
+0.01    20/05/20
+        Initial Implementation
+
diff --git a/MANIFEST b/MANIFEST
new file mode 100644
index 0000000..bb3809c
--- /dev/null
+++ b/MANIFEST
@@ -0,0 +1,13 @@
+Changes
+lib/App/wsgetmail.pm
+lib/App/wsgetmail/MS360.pm
+lib/App/wsgetmail/MS360/Client.pm
+Makefile.PL
+MANIFEST			This list of files
+README
+ignore.txt
+t/00-load.t
+t/manifest.t
+t/pod-coverage.t
+t/pod.t
+
diff --git a/Makefile.PL b/Makefile.PL
new file mode 100644
index 0000000..cf8bf2e
--- /dev/null
+++ b/Makefile.PL
@@ -0,0 +1,50 @@
+use 5.006;
+use strict;
+use warnings;
+use ExtUtils::MakeMaker;
+
+my %WriteMakefileArgs = (
+    NAME             => 'App::wsgetmail',
+    AUTHOR           => q{Aaron Trevena <ast at bestpractical.com>},
+    VERSION_FROM     => 'lib/App/wsgetmail.pm',
+    ABSTRACT_FROM    => 'lib/App/wsgetmail.pm',
+    LICENSE          => 'artistic_2',
+    MIN_PERL_VERSION => '5.006',
+    CONFIGURE_REQUIRES => {
+        'ExtUtils::MakeMaker' => '0',
+    },
+    TEST_REQUIRES => {
+        'Test::More' => '0',
+    },
+    PREREQ_PM => {
+        'Azure::AD::ClientCredentials' => 0,
+        'LWP::UserAgent' => 0,
+        'URI::Escape' => 0,
+        'URI::Query' => 0,
+        'Moo' => 0,
+        'JSON' => -,
+	'IPC::Run => 0'
+    },
+    dist  => { COMPRESS => 'gzip -9f', SUFFIX => 'gz', },
+    clean => { FILES => 'App-wsgetmail-*' },
+);
+
+# Compatibility with old versions of ExtUtils::MakeMaker
+unless (eval { ExtUtils::MakeMaker->VERSION('6.64'); 1 }) {
+    my $test_requires = delete $WriteMakefileArgs{TEST_REQUIRES} || {};
+    @{$WriteMakefileArgs{PREREQ_PM}}{keys %$test_requires} = values %$test_requires;
+}
+
+unless (eval { ExtUtils::MakeMaker->VERSION('6.55_03'); 1 }) {
+    my $build_requires = delete $WriteMakefileArgs{BUILD_REQUIRES} || {};
+    @{$WriteMakefileArgs{PREREQ_PM}}{keys %$build_requires} = values %$build_requires;
+}
+
+delete $WriteMakefileArgs{CONFIGURE_REQUIRES}
+    unless eval { ExtUtils::MakeMaker->VERSION('6.52'); 1 };
+delete $WriteMakefileArgs{MIN_PERL_VERSION}
+    unless eval { ExtUtils::MakeMaker->VERSION('6.48'); 1 };
+delete $WriteMakefileArgs{LICENSE}
+    unless eval { ExtUtils::MakeMaker->VERSION('6.31'); 1 };
+
+WriteMakefile(%WriteMakefileArgs);
diff --git a/README b/README
new file mode 100644
index 0000000..43c68c3
--- /dev/null
+++ b/README
@@ -0,0 +1,47 @@
+App-wsgetmail
+
+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.
+
+INSTALLATION
+
+To install this module, run the following commands:
+
+	perl Makefile.PL
+	make
+	make test
+	make install
+
+SUPPORT AND DOCUMENTATION
+
+After installing, you can find documentation for this module with the
+perldoc command.
+
+    perldoc App::wsgetmail
+
+You can also look for information at:
+
+    RT, CPAN's request tracker (report bugs here)
+        https://rt.cpan.org/NoAuth/Bugs.html?Dist=App-wsgetmail
+
+    AnnoCPAN, Annotated CPAN documentation
+        http://annocpan.org/dist/App-wsgetmail
+
+    CPAN Ratings
+        https://cpanratings.perl.org/d/App-wsgetmail
+
+    Search CPAN
+        https://metacpan.org/release/App-wsgetmail
+
+
+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)
+
diff --git a/ignore.txt b/ignore.txt
new file mode 100644
index 0000000..91d3464
--- /dev/null
+++ b/ignore.txt
@@ -0,0 +1,18 @@
+Makefile
+Makefile.old
+Build
+Build.bat
+META.*
+MYMETA.*
+.build/
+_build/
+cover_db/
+blib/
+inc/
+.lwpcookies
+.last_cover_stats
+nytprof.out
+pod2htm*.tmp
+pm_to_blib
+App-wsgetmail-*
+App-wsgetmail-*.tar.gz
diff --git a/lib/App/wsgetmail.pm b/lib/App/wsgetmail.pm
new file mode 100644
index 0000000..4cf9dd4
--- /dev/null
+++ b/lib/App/wsgetmail.pm
@@ -0,0 +1,101 @@
+package App::wsgetmail;
+
+use 5.006;
+use strict;
+use warnings;
+
+=head1 NAME
+
+App::wsgetmail - Fetch mail from the cloud using webservices
+
+=head1 VERSION
+
+Version 0.01
+
+=cut
+
+our $VERSION = '0.01';
+
+=head1 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.
+
+=head1 SYNOPSIS
+
+    use App::wsgetmail;
+
+    my $foo = App::wsgetmail->new();
+    ...
+
+=head1 SUBROUTINES/METHODS
+
+=head2 function1
+
+=cut
+
+sub function1 {
+}
+
+=head2 function2
+
+=cut
+
+=head1 AUTHOR
+
+Aaron Trevena, C<< <ast at bestpractical.com> >>
+
+=head1 BUGS
+
+Please report any bugs or feature requests to C<bug-app-wsgetmail at rt.cpan.org>, or through
+the web interface at L<https://rt.cpan.org/NoAuth/ReportBug.html?Queue=App-wsgetmail>.  I will be notified, and then you'll
+automatically be notified of progress on your bug as I make changes.
+
+=head1 SUPPORT
+
+You can find documentation for this module with the perldoc command.
+
+    perldoc App::wsgetmail
+
+
+You can also look for information at:
+
+=over 4
+
+=item * RT: CPAN's request tracker (report bugs here)
+
+L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=App-wsgetmail>
+
+=item * AnnoCPAN: Annotated CPAN documentation
+
+L<http://annocpan.org/dist/App-wsgetmail>
+
+=item * CPAN Ratings
+
+L<https://cpanratings.perl.org/d/App-wsgetmail>
+
+=item * Search CPAN
+
+L<https://metacpan.org/release/App-wsgetmail>
+
+=back
+
+
+=head1 ACKNOWLEDGEMENTS
+
+
+=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)
+
+
+=cut
+
+1; # End of App::wsgetmail
diff --git a/lib/App/wsgetmail/Handler/RT/Mailgate.pm b/lib/App/wsgetmail/Handler/RT/Mailgate.pm
new file mode 100644
index 0000000..43f3832
--- /dev/null
+++ b/lib/App/wsgetmail/Handler/RT/Mailgate.pm
@@ -0,0 +1,97 @@
+package App::wsgetmail::Handler::RT::Mailgate;
+use Moo;
+use IPC::Run qw( run timeout );
+
+has debug => (
+    is => 'ro',
+    lazy => 1,
+    default => sub { 0 }
+);
+
+has url => (
+    is => 'ro',
+    required => 1,
+);
+
+has request_timeout => (
+    is => 'ro',
+    lazy => 1,
+    default => sub { 10 }
+);
+
+has action => (
+    is => 'ro',
+    lazy => 1,
+    default => sub { 'correspond' }
+);
+
+has extension => (
+    is => 'ro',
+    required => 0
+);
+
+# rt-mailgate --action (correspond|comment|...) --queue queuename
+#                 --url http://your.rt.server/
+#                 [ --debug ]
+#                 [ --extension (queue|action|ticket) ]
+#                 [ --timeout seconds ]
+
+my $option_configuration = {
+    debug => 'boolean',
+    url => 'string',
+    action => [qw(correspond comment)],
+    extension => [qw(queue action ticket)],
+    request_timeout => 'integer',
+    queue => 'string'
+};
+
+sub forward_mail {
+    my ($self, $filename) = @_;
+
+    # build arguments
+    if ($self->extension) {
+        # parse email, get headers
+        # set env for extension from to header
+        my $entity = MIME::Entity->build(Path => $filename);
+        my ($to, $extension) = split(/\+/,$entity->head->get('To'));
+        if (defined($extension) && $extension =~ m/\W/) {
+            $ENV{EXTENSION} = $extension;
+        }
+    }
+    my $arguments = [ ];
+    foreach my $option (keys %$option_configuration) {
+        next unless ($self->$option);
+        if ($option_configuration->{$option} eq 'boolean') {
+            push(@$arguments, "--$option");
+        }
+        elsif ( $option eq 'request_timeout' ) {
+            push(@$arguments, "--timeout", $self->request_timeout);
+        }
+        else {
+            push(@$arguments, "--$option", $self->$option);
+        }
+    }
+    # run command
+    $self->run_command($filename, $arguments);
+}
+
+
+sub _run_command {
+    my ($self, $filename, $arguments) = @_;
+    my $ok = 1;
+    my $command = 'rt-mailgate';
+    if ($self->command_path) {
+        $command = $self->command_path . $command;
+    }
+
+    my ($output, $error);
+    run [ $command, @$arguments ], '<', $filename, \$output, \$error, timeout( $self->request_timeout + 5 ) or die "failed to run rt-mailgate : $! $?";
+
+    # check for errors
+
+    return $ok;
+}
+
+
+
+1;
diff --git a/lib/App/wsgetmail/MS360.pm b/lib/App/wsgetmail/MS360.pm
new file mode 100644
index 0000000..5f8c66e
--- /dev/null
+++ b/lib/App/wsgetmail/MS360.pm
@@ -0,0 +1,159 @@
+package App::wsgetmail::MS360;
+
+=head1 NAME
+
+App::wsgetmail::MS360 - Fetch mail from Microsoft 360
+
+=head1 DESCRIPTION
+
+Fetch mail from Microsoft 360 mailboxes using the Graph REST API
+
+=cut
+
+use Moo;
+use JSON;
+
+use App::wsgetmail::MS360::Client;
+
+=head1 ATTRIBUTES
+
+=over 4
+
+=back
+
+=cut
+
+has secret  => (
+    is => 'ro',
+    required => 1,
+);
+
+has client_id => (
+    is => 'ro',
+    required => 1,
+);
+
+has tenant_id => (
+    is => 'ro',
+    required => 1,
+);
+
+has username => (
+    is => 'ro',
+    required => 0
+);
+
+has user_password => (
+    is => 'ro',
+    required => 0
+);
+
+has global_access => (
+    is => 'ro',
+    default => sub { return 0 }
+);
+
+has _handler => (
+    is => 'ro',
+    lazy => 1,
+    builder => '_build_handler'
+);
+
+has handler_options => (
+    is => 'ro',
+    required => 1,
+);
+
+has _client => (
+    is => 'ro',
+    builder => '_build_client',
+);
+
+our $handlers = {
+    SimpleFile => 'App::wsgetmail::Handler::RT::SimpleFile',
+    Mailgate => 'App::wsgetmail::Handler::RT::Mailgate',
+};
+
+=head1 METHODS
+
+=head2 get_message_list
+
+=cut
+
+sub get_message_list {
+    my ($self) = @_;
+    # add error handling!
+    my @path_parts = ($self->global_access) ? ( 'users', $self->username, 'messages' ) : ( 'me', 'messages' );
+    my $response = $self->get_request(@path_parts);
+    # add error checking here!
+    my $message_list = decode_json( $response->content );
+    return $message_list;
+}
+
+=head2 get_message_mime_content
+
+=cut
+
+sub get_message_mime_content {
+    my ($self, $message_id) = @_;
+    my @path_parts = ($self->global_access) ? ('users', $self->username, 'messages', $message_id, '$value') : ('me', 'messages', $message_id, '$value');
+
+    my $response = $self->get_request(@path_parts);
+    # add error checking here!
+
+    my $raw_message = $response->content;
+    return $raw_message;
+}
+
+
+sub get_handler {
+    my $self = shift;
+    my $handler_options = $self->handler_options;
+    my $handler_class = delete $handler_options->{class};
+    unless ($handler_class && $handlers->{$handler_class}) {
+        die "valid class required for handler, classes available " . join(', ', keys %$handlers);
+    }
+    require $handlers->{$handler_class};
+    my $handler = $handlers->{$handler_class}->new($handler_options);
+    return $handler;
+}
+
+
+##############
+
+sub _build_client {
+    my $self = shift;
+    my $client = App::wsgetmail::MS360::Client->new( {
+        client_id => $self->client_id,
+        username => $self->username,
+        user_password => $self->user_password,
+        secret => $self->secret,
+        client_id => $self->client_id,
+        tenant_id => $self->tenant_id,
+        global_access => $self->global_access
+    } );
+    return $client;
+
+}
+
+=head1 SEE ALSO
+
+=over 4
+
+=item App::wsgetmail::MS360::Client
+
+=back
+
+=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)
+
+
+=cut
+
+
+1;
diff --git a/lib/App/wsgetmail/MS360/Client.pm b/lib/App/wsgetmail/MS360/Client.pm
new file mode 100644
index 0000000..60d2d20
--- /dev/null
+++ b/lib/App/wsgetmail/MS360/Client.pm
@@ -0,0 +1,224 @@
+package App::wsgetmail::MS360::Client;
+
+=head1 NAME
+
+App::wsgetmail::MS360 - Fetch mail from Microsoft 360
+
+=cut
+
+use Moo;
+use URI::Escape;
+use URI::Query;
+use JSON;
+use LWP::UserAgent;
+use Azure::AD::ClientCredentials;
+
+=head1 DESCRIPTION
+
+Fetch mail from Microsoft 360 mailboxes using the Graph REST API
+=head1 SYNOPSIS
+
+
+=head1 ATTRIBUTES
+
+=over 4
+
+=item secret
+
+=item client_id
+
+=item tenant_id
+
+=item username
+
+=item user_password
+
+=item global_access
+
+=back
+
+=cut
+
+has secret  => (
+    is => 'ro',
+    required => 0,
+);
+
+has client_id => (
+    is => 'ro',
+    required => 1,
+);
+
+has tenant_id => (
+    is => 'ro',
+    required => 1,
+);
+
+has username => (
+    is => 'ro',
+    required => 0
+);
+
+has user_password => (
+    is => 'ro',
+    required => 0
+);
+
+has global_access => (
+    is => 'ro',
+    default => sub { return 0 }
+);
+
+has _ua => (
+    builder   => '_build_authorised_ua',
+    is => 'ro',
+    lazy => 1,
+);
+
+has _credentials => (
+    is => 'ro',
+    lazy => 1,
+    builder => '_build__credentials',
+);
+
+has _access_token => (
+    is => 'ro',
+    lazy => 1,
+    builder => '_build__access_token',
+);
+
+my $graph_v1_url = 'https://graph.microsoft.com/v1.0/';
+
+sub BUILD {
+    my ($self, $args) = @_;
+    if ($args->{global_access}) {
+        unless ($args->{secret}) {
+            die "secret is required when using global_access";
+        }
+    }
+    else {
+        unless ($args->{username} && $args->{user_password}) {
+            die "username and user_password are required when not using global_access";
+        }
+    }
+}
+
+
+=head1 METHODS
+
+=head2 build_rest_uir
+
+=cut
+
+sub build_rest_uri {
+    my ($self, @endpoint_parts) = @_;
+    return join('/', $graph_v1_url, @endpoint_parts);
+}
+
+=head2 get_request
+
+=cut
+
+sub get_request {
+    my ($self, $parts) = @_;
+    # add error handling!
+    my $url = $self->build_rest_uri(@$parts);
+    return $self->_ua->get($url);
+}
+
+# sub post_request {
+#     my ($self, $path_parts, $post_data) = @_;
+#     my $url = $self->build_rest_uri(@$path_parts);
+#     return $self->_ua->post($url,$post_data);
+# }
+
+# sub patch_request {
+#     my ($self, $path_parts, $patch_data) = @_;
+#     my $url = $self->build_rest_uri(@$path_parts);
+#     return $self->_ua->patch($url,$patch_data);
+# }
+
+######
+
+sub _build_authorised_ua {
+    my $self = shift;
+    my $ua = LWP::UserAgent->new();
+    $ua->default_header( Authorization => $self->_access_token() );
+    return $ua;
+}
+
+sub _build_access_token {
+    my $self = shift;
+    my $access_token;
+    if ($self->global_access) {
+        $access_token = $self->_credentials->access_token;
+    }
+    else {
+        $access_token = $self->_get_user_access_token;
+    }
+    return $access_token;
+}
+
+sub _get_user_access_token {
+    my $self = shift;
+    my $ua = LWP::UserAgent->new();
+    my $access_token;
+    my $oauth_login_url = sprintf('https://login.windows.net/%s/oauth2/token', $self->tenant_id);
+    my $response = $ua->post( $oauth_login_url,
+                              {
+                                  resource=>'https://graph.microsoft.com/',
+                                  client_id => $self->client_id,
+                                  grant_type=>'password',
+                                  username=>$self->username,
+                                  password=>$self->user_password,
+                                  scope=>'openid'
+                              }
+                          );
+    my $raw_message = $response->content;
+    # check details
+    if ($response->is_success) {
+        my $token_details = decode_json( $response->content );
+        $access_token = "Bearer " . $token_details->{access_token};
+    }
+    else {
+        # throw error
+        die sprintf('unable to get user access token for user %s request failed with status %s ', $self->username, $response->status_line);
+    }
+    return $access_token;
+}
+
+sub _build__credentials {
+    my $self = shift;
+    my $creds = Azure::AD::ClientCredentials->new(
+        resource_id => 'https://graph.microsoft.com/',
+        client_id => $self->client_id,
+        secret_id => $self->secret,
+        tenant_id => $self->tenant_id
+    );
+    return $creds;
+}
+
+
+
+
+=head1 SEE ALSO
+
+=over 4
+
+=item App::wsgetmail::Client
+
+=back
+
+=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)
+
+
+=cut
+
+
+1;
diff --git a/t/00-load.t b/t/00-load.t
new file mode 100644
index 0000000..7f55efc
--- /dev/null
+++ b/t/00-load.t
@@ -0,0 +1,12 @@
+#!perl 
+use strict;
+use Test::More;
+
+use lib qw(lib);
+
+use_ok( 'App::wsgetmail' );
+use_ok( 'App::wsgetmail::MS360' );
+use_ok( 'App::wsgetmail::MS360::Client' );
+use_ok( 'App::wsgetmail::Handler::RT::Mailgate' );
+
+done_testing();
diff --git a/t/manifest.t b/t/manifest.t
new file mode 100644
index 0000000..e0b558e
--- /dev/null
+++ b/t/manifest.t
@@ -0,0 +1,15 @@
+#!perl -T
+use 5.006;
+use strict;
+use warnings;
+use Test::More;
+
+unless ( $ENV{RELEASE_TESTING} ) {
+    plan( skip_all => "Author tests not required for installation" );
+}
+
+my $min_tcm = 0.9;
+eval "use Test::CheckManifest $min_tcm";
+plan skip_all => "Test::CheckManifest $min_tcm required" if $@;
+
+ok_manifest();
diff --git a/t/pod-coverage.t b/t/pod-coverage.t
new file mode 100644
index 0000000..f5728a5
--- /dev/null
+++ b/t/pod-coverage.t
@@ -0,0 +1,24 @@
+#!perl -T
+use 5.006;
+use strict;
+use warnings;
+use Test::More;
+
+unless ( $ENV{RELEASE_TESTING} ) {
+    plan( skip_all => "Author tests not required for installation" );
+}
+
+# Ensure a recent version of Test::Pod::Coverage
+my $min_tpc = 1.08;
+eval "use Test::Pod::Coverage $min_tpc";
+plan skip_all => "Test::Pod::Coverage $min_tpc required for testing POD coverage"
+    if $@;
+
+# Test::Pod::Coverage doesn't require a minimum Pod::Coverage version,
+# but older versions don't recognize some common documentation styles
+my $min_pc = 0.18;
+eval "use Pod::Coverage $min_pc";
+plan skip_all => "Pod::Coverage $min_pc required for testing POD coverage"
+    if $@;
+
+all_pod_coverage_ok();
diff --git a/t/pod.t b/t/pod.t
new file mode 100644
index 0000000..4d3a0ce
--- /dev/null
+++ b/t/pod.t
@@ -0,0 +1,16 @@
+#!perl -T
+use 5.006;
+use strict;
+use warnings;
+use Test::More;
+
+unless ( $ENV{RELEASE_TESTING} ) {
+    plan( skip_all => "Author tests not required for installation" );
+}
+
+# Ensure a recent version of Test::Pod
+my $min_tp = 1.22;
+eval "use Test::Pod $min_tp";
+plan skip_all => "Test::Pod $min_tp required for testing POD" if $@;
+
+all_pod_files_ok();
diff --git a/xt/boilerplate.t b/xt/boilerplate.t
new file mode 100644
index 0000000..5040b18
--- /dev/null
+++ b/xt/boilerplate.t
@@ -0,0 +1,57 @@
+#!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');
+
+
+}
+

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


hooks/post-receive
-- 
app-wsgetmail


More information about the Bps-public-commit mailing list