File Coverage

lib/App/wsgetmail/MDA.pm
Criterion Covered Total %
statement 8 54 14.8
branch 0 36 0.0
condition n/a
subroutine 3 6 50.0
pod 1 1 100.0
total 12 97 12.3


line stmt bran cond sub pod time code
1             # BEGIN BPS TAGGED BLOCK {{{
2             #
3             # COPYRIGHT:
4             #
5             # This software is Copyright (c) 2020-2022 Best Practical Solutions, LLC
6             #
7             #
8             # (Except where explicitly superseded by other copyright notices)
9             #
10             #
11             # LICENSE:
12             #
13             # This work is made available to you under the terms of Version 2 of
14             # the GNU General Public License. A copy of that license should have
15             # been provided with this software, but in any event can be snarfed
16             # from www.gnu.org.
17             #
18             # This work is distributed in the hope that it will be useful, but
19             # WITHOUT ANY WARRANTY; without even the implied warranty of
20             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21             # General Public License for more details.
22             #
23             # You should have received a copy of the GNU General Public License
24             # along with this program; if not, write to the Free Software
25             # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26             # 02110-1301 or visit their web page on the internet at
27             # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28             #
29             #
30             # CONTRIBUTION SUBMISSION POLICY:
31             #
32             # (The following paragraph is not intended to limit the rights granted
33             # to you to modify and distribute this software under the terms of
34             # the GNU General Public License and is only of importance to you if
35             # you choose to contribute your changes and enhancements to the
36             # community by submitting them to Best Practical Solutions, LLC.)
37             #
38             # By intentionally submitting any modifications, corrections or
39             # derivatives to this work, or any other work intended for use with
40             # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41             # you are the copyright holder for those contributions and you grant
42             # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43             # royalty-free, perpetual, license to use, copy, create derivative
44             # works based on those contributions, and sublicense and distribute
45             # those contributions and any derivatives thereof.
46             #
47             # END BPS TAGGED BLOCK }}}
48              
49             =head1 NAME
50              
51             App::wsgetmail::MDA - Deliver mail to another command's standard input
52              
53             =head1 SYNOPSIS
54              
55             my $mda = App::wsgetmail::MDA->new({
56             command => "/opt/rt5/bin/rt-mailgate",
57             command_args => "--url https://rt.example.com --queue General --action correspond",
58             command_timeout => 15,
59             debug => 0,
60             })
61             $mda->forward($message, $message_path);
62              
63             =head1 DESCRIPTION
64              
65             App::wsgetmail::MDA takes mail fetched from web services and routes it to
66             another command via standard input.
67              
68             =cut
69              
70 2     2   37 use v5.10;
  2         8  
71              
72             package App::wsgetmail::MDA;
73 2     2   13 use Moo;
  2         4  
  2         16  
74              
75 2     2   2846 use IPC::Run qw( run timeout );
  2         64437  
  2         1343  
76              
77             =head1 ATTRIBUTES
78              
79             You can initialize a new App::wsgetmail::MDA object with the attributes
80             below. C and C are required; the rest are
81             optional. All attributes are read-only.
82              
83             =head2 command
84              
85             A string with the executable to run. You can specify an absolute path, or a
86             plain command name which will be found from C<$PATH>.
87              
88             =cut
89              
90             has command => (
91             is => 'ro',
92             required => 1,
93             );
94              
95             =head2 command_args
96              
97             A string with additional arguments to call C with. These arguments
98             follow shell quoting rules: you can escape characters with a backslash, and
99             denote a single string argument with single or double quotes.
100              
101             =cut
102              
103             has command_args => (
104             is => 'ro',
105             required => 1,
106             );
107              
108             =head2 command_timeout
109              
110             A number. The run command will be terminated if it takes longer than this many
111             seconds.
112              
113             =cut
114              
115             has command_timeout => (
116             is => 'ro',
117             default => sub { 30; }
118             );
119              
120             # extension and recipient are currently unused. See pod below.
121             has extension => (
122             is => 'ro',
123             required => 0
124             );
125              
126             has recipient => (
127             is => 'ro',
128             required => 0,
129             );
130              
131             =head2 debug
132              
133             A boolean. If true, the object will issue additional diagnostic warnings if it
134             encounters any trouble.
135              
136             =head2 Unused Attributes
137              
138             These attributes were used in previous versions of the module. They are
139             currently unimplemented and always return undef. You cannot initialize them.
140              
141             =over 4
142              
143             =item * extension
144              
145             =item * recipient
146              
147             =back
148              
149             =cut
150              
151             has debug => (
152             is => 'ro',
153             default => sub { 0 }
154             );
155              
156              
157             # this sets the attributes in the object using values from the config.
158             # if no value is defined in the config, the attribute's "default" is used
159             # instead (if defined).
160             around BUILDARGS => sub {
161             my ( $orig, $class, $config ) = @_;
162              
163             my $attributes = {
164             map {
165             $_ => $config->{$_}
166             }
167             grep {
168             defined $config->{$_}
169             }
170             qw(command command_args command_timeout debug)
171             };
172              
173             return $class->$orig($attributes);
174             };
175              
176              
177             =head1 METHODS
178              
179             =head2 forward($message, $filename)
180              
181             Invokes the configured command to deliver the given message. C<$message> is
182             an object like L. C<$filename> is the path
183             to a file with the raw message content.
184              
185             =cut
186              
187             sub forward {
188 0     0 1   my ($self, $message, $filename) = @_;
189 0           return $self->_run_command($filename);
190             }
191              
192              
193             sub _run_command {
194 0     0     my ($self, $filename) = @_;
195 0 0         open my $fh, "<$filename" or die $!;
196 0           my ($input, $output, $error);
197 0 0         unless ($self->command) {
198 0 0         warn "no action to delivery message, command option is empty or null" if ($self->debug);
199 0           return 1;
200             }
201              
202 0           my $ok = run ([ $self->command, _split_command_args($self->command_args, 1)], $fh, \$output, \$error, timeout( $self->command_timeout ) );
203 0 0         unless ($ok) {
204 0 0         warn sprintf('failed to run command "%s %s" for file %s : %s',
205             $self->command,
206             ($self->debug ? join(' ', _split_command_args($self->command_args)) : '' ),
207             $filename, $?);
208 0 0         warn "output : $output\nerror:$error\n" if ($self->debug);
209             }
210 0           close $fh;
211 0           return $ok;
212             }
213              
214              
215             #TODO: make into a simple cpan module
216             # Loosely based on https://metacpan.org/pod/Parse::CommandLine
217             sub _split_command_args {
218 0     0     my ($line, $strip_quotes) = @_;
219              
220             # strip leading/trailing spaces
221 0           $line =~ s/^\s+//;
222 0           $line =~ s/\s+$//;
223              
224 0           my (@args, $quoted, $escape_next, $next_arg);
225 0           foreach my $character (split('', $line) ) {
226 0 0         if ($escape_next) {
227 0           $next_arg .= $character;
228 0           $escape_next = undef;
229 0           next;
230             }
231              
232 0 0         if ($character =~ m|\\|) {
233 0           $next_arg .= $character;
234 0 0         if ($quoted) {
235 0           $escape_next = 1;
236             }
237 0           next;
238             }
239              
240 0 0         if ($character =~ m/\s/) {
241 0 0         if ($quoted) {
242 0           $next_arg .= $character;
243             }
244             else {
245 0 0         push @args, $next_arg if defined $next_arg;
246 0           undef $next_arg;
247             }
248 0           next;
249             }
250              
251 0 0         if ($character =~ m/['"]/) {
252 0 0         if ($quoted) {
253 0 0         if ($character eq $quoted) {
254 0           $quoted = undef;
255 0 0         $next_arg .= $character unless ($strip_quotes);
256             } else {
257 0           $next_arg .= $character;
258             }
259             }
260             else {
261 0           $quoted = $character;
262 0 0         $next_arg .= $character unless ($strip_quotes);
263             }
264 0           next;
265             }
266 0           $next_arg .= $character;
267             }
268 0 0         push @args, $next_arg if defined $next_arg;
269 0           return @args;
270             }
271              
272             =head1 AUTHOR
273              
274             Best Practical Solutions, LLC
275              
276             =head1 LICENSE AND COPYRIGHT
277              
278             This software is Copyright (c) 2015-2020 by Best Practical Solutions, LLC.
279              
280             This is free software, licensed under:
281              
282             The GNU General Public License, Version 2, June 1991
283              
284             =cut
285              
286             1;