App-wsgetmail

 view release on metacpan or  search on metacpan

lib/App/wsgetmail/MDA.pm  view on Meta::CPAN

# BEGIN BPS TAGGED BLOCK {{{
#
# COPYRIGHT:
#
# This software is Copyright (c) 2020-2026 Best Practical Solutions, LLC
#                                          <sales@bestpractical.com>
#
# (Except where explicitly superseded by other copyright notices)
#
#
# LICENSE:
#
# This work is made available to you under the terms of Version 2 of
# the GNU General Public License. A copy of that license should have
# been provided with this software, but in any event can be snarfed
# from www.gnu.org.
#
# This work is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 or visit their web page on the internet at
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
#
#
# CONTRIBUTION SUBMISSION POLICY:
#
# (The following paragraph is not intended to limit the rights granted
# to you to modify and distribute this software under the terms of
# the GNU General Public License and is only of importance to you if
# you choose to contribute your changes and enhancements to the
# community by submitting them to Best Practical Solutions, LLC.)
#
# By intentionally submitting any modifications, corrections or
# derivatives to this work, or any other work intended for use with
# Request Tracker, to Best Practical Solutions, LLC, you confirm that
# you are the copyright holder for those contributions and you grant
# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
# royalty-free, perpetual, license to use, copy, create derivative
# works based on those contributions, and sublicense and distribute
# those contributions and any derivatives thereof.
#
# END BPS TAGGED BLOCK }}}

=head1 NAME

App::wsgetmail::MDA - Deliver mail to another command's standard input

=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

use v5.10;

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 are currently unused. See pod below.
has extension => (
    is => 'ro',
    required => 0
);

has recipient => (
    is => 'ro',
    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 }
);


# this sets the attributes in the object using values from the config.
# if no value is defined in the config, the attribute's "default" is used
# instead (if defined).
around BUILDARGS => sub {
    my ( $orig, $class, $config ) = @_;

    my $attributes = {
        map {
            $_ => $config->{$_}
        }
        grep {
            defined $config->{$_}
        }
        qw(command command_args command_timeout debug)
    };

    return $class->$orig($attributes);
};


=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) = @_;
    return $self->_run_command($filename);
}


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 ) );
    unless ($ok) {
        warn sprintf('failed to run command "%s %s" for file %s : %s',
                     $self->command,
                     ($self->debug ? join(' ', _split_command_args($self->command_args)) : '' ),
                     $filename, $?);
        warn "output : $output\nerror:$error\n" if ($self->debug);
    }
    close $fh;
    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);



( run in 1.150 second using v1.01-cache-2.11-cpan-524268b4103 )