File Coverage

lib/Mail/Make/Stream.pm
Criterion Covered Total %
statement 102 146 69.8
branch 25 64 39.0
condition 8 24 33.3
subroutine 23 30 76.6
pod 1 3 33.3
total 159 267 59.5


line stmt bran cond sub pod time code
1             ##----------------------------------------------------------------------------
2             ## MIME Email Builder - ~/lib/Mail/Make/Stream.pm
3             ## Version v0.1.0
4             ## Copyright(c) 2026 DEGUEST Pte. Ltd.
5             ## Author: Jacques Deguest <jack@deguest.jp>
6             ## Created 2026/03/05
7             ## Modified 2026/03/05
8             ## All rights reserved.
9             ##
10             ## This program is free software; you can redistribute it and/or modify it
11             ## under the same terms as Perl itself.
12             ##----------------------------------------------------------------------------
13             package Mail::Make::Stream;
14             BEGIN
15             {
16 8     8   56 use strict;
  8         17  
  8         334  
17 8     8   69 use warnings;
  8         16  
  8         554  
18 8     8   2997 warnings::register_categories( 'Mail::Make' );
19 8     8   59 use parent qw( Module::Generic );
  8         14  
  8         60  
20 8     8   740 use vars qw( $VERSION $EXCEPTION_CLASS );
  8         34  
  8         596  
21 8     8   54 use Mail::Make::Exception;
  8         27  
  8         124  
22 8         19 our $EXCEPTION_CLASS = 'Mail::Make::Exception';
23 8         245 our $VERSION = 'v0.1.0';
24             };
25              
26 8     8   53 use strict;
  8         16  
  8         255  
27 8     8   51 use warnings;
  8         17  
  8         1940  
28              
29             sub init
30             {
31 0     0 1 0 my $self = shift( @_ );
32 0         0 $self->{_exception_class} = $EXCEPTION_CLASS;
33 0         0 $self->{_init_strict_use_sub} = 1;
34 0 0       0 $self->SUPER::init( @_ ) || return( $self->pass_error );
35 0         0 return( $self );
36             }
37              
38             # NOTE: STORABLE support
39              
40             # NOTE: FREEZE is inherited
41              
42 0     0 0 0 sub STORABLE_freeze { CORE::return( CORE::shift->FREEZE( @_ ) ); }
43              
44 0     0 0 0 sub STORABLE_thaw { CORE::return( CORE::shift->THAW( @_ ) ); }
45              
46             # NOTE: THAW is inherited
47              
48             1;
49              
50             # NOTE: Mail::Make::Stream::Generic inline class
51             {
52             package
53             Mail::Make::Stream::Generic;
54             BEGIN
55             {
56 8     8   73 use strict;
  8         17  
  8         306  
57 8     8   48 use warnings;
  8         16  
  8         433  
58 8     8   43 use parent qw( Module::Generic );
  8         23  
  8         79  
59 8     8   725 use vars qw( $VERSION $EXCEPTION_CLASS );
  8         15  
  8         571  
60 8     8   51 use Mail::Make::Exception;
  8         32  
  8         41  
61 8     8   4786 use Module::Generic::File::IO;
  8         35768  
  8         108  
62 8     8   6507 use Module::Generic::Scalar::IO;
  8         33241  
  8         111  
63 8     8   699 our $EXCEPTION_CLASS = 'Mail::Make::Exception';
64 8         188 our $VERSION = $Mail::Make::Stream::VERSION;
65             };
66              
67 8     8   50 use strict;
  8         16  
  8         180  
68 8     8   36 use warnings;
  8         12  
  8         657  
69              
70             sub init
71             {
72 64     64   335046 my $self = shift( @_ );
73 64   33     325 my $class = ( ref( $self ) || $self );
74 64         994 $self->{_init_strict_use_sub} = 1;
75 8     8   42 no strict 'refs';
  8         17  
  8         10817  
76 64         509 $self->{_exception_class} = defined( ${"${class}::EXCEPTION_CLASS"} )
77 64 50       105 ? ${"${class}::EXCEPTION_CLASS"}
  64         473  
78             : $EXCEPTION_CLASS;
79 64 50       416 $self->SUPER::init( @_ ) || return( $self->pass_error );
80 64         5686 return( $self );
81             }
82              
83             # _get_glob_from_arg( $source_or_dest [, write => 1] )
84             #
85             # Normalises any of the following into a ( $fh, $op ) pair:
86             #
87             # - A native Perl glob / IO object → used directly
88             # - A scalar reference (\$scalar) → Module::Generic::Scalar::IO
89             # - A plain string (file path) → Module::Generic::File opened via new_file()
90             #
91             # $op is a closure that:
92             # Read mode: $op->( $buf, $len ) - returns bytes read (0 at EOF, undef on error)
93             # Write mode: $op->( $data ) - returns true on success, undef on error
94             sub _get_glob_from_arg
95             {
96 142     142   244 my $self = shift( @_ );
97 142         220 my $this = shift( @_ );
98 142 0 33     822 if( !defined( $this ) || ( !ref( $this ) && !length( $this ) ) )
      33        
99             {
100 0         0 return( $self->error( "No argument was provided." ) );
101             }
102 142         394 my $opts = $self->_get_args_as_hash( @_ );
103 142 100       88381 $opts->{write} = 0 if( !exists( $opts->{write} ) );
104 142 100       682 my $mode = $opts->{write} ? '+>' : '<';
105 142         213 my $fh;
106 142         198 my $is_native_glob = 0;
107              
108 142 100       547 if( $self->_is_glob( $this ) )
    100          
109             {
110 58         709 $fh = $this;
111             # Even if this is an in-memory scalar handle, fileno() returns -1, which is true
112 58 50       242 $is_native_glob++ if( fileno( $this ) );
113             }
114             elsif( $self->_is_scalar( $this ) )
115             {
116 72   50     2834 $fh = Module::Generic::Scalar::IO->new( $this, $mode ) ||
117             return( $self->pass_error( Module::Generic::Scalar::IO->error ) );
118 72         32364 $is_native_glob++;
119             }
120             else
121             {
122 12   50     460 my $f = $self->new_file( "$this" ) || return( $self->pass_error );
123 12 0 33     1847072 if( !$f->exists && !$opts->{write} )
124             {
125 0         0 return( $self->error( "File '$this' does not exist." ) );
126             }
127 12   50     1583 $fh = $f->open( $mode, { binmode => 'raw', ( $opts->{write} ? ( autoflush => 1 ) : () ) } ) ||
128             return( $self->pass_error( $f->error ) );
129 12         71267 $is_native_glob++;
130             }
131              
132 142         114734 my $flags;
133 142 100       831 if( $self->_can( $fh => 'fcntl' ) )
134             {
135 96         3274 $flags = $fh->fcntl( F_GETFL, 0 );
136             }
137             else
138             {
139 46         733 $flags = fcntl( $fh, F_GETFL, 0 );
140             }
141              
142 142 100       2585 if( defined( $flags ) )
143             {
144 96 100       426 if( $opts->{write} )
145             {
146 71 50       236 unless( $flags & ( O_RDWR | O_WRONLY | O_APPEND ) )
147             {
148 0         0 return( $self->error( "Filehandle provided does not have write permission enabled." ) );
149             }
150             }
151             else
152             {
153 25 50 33     287 unless( ( ( $flags & O_RDONLY ) == O_RDONLY ) || ( $flags & O_RDWR ) )
154             {
155 0         0 return( $self->error( "Filehandle provided does not have read permission enabled. File handle flags value is '$flags'" ) );
156             }
157             }
158             }
159              
160             # We check if the file handle is an object, because calling core read() or print()
161             # on it would not work unless the glob has implemented a tie. See perltie.
162 142         334 my $op;
163             my $meth;
164 142 100       406 if( $opts->{write} )
165             {
166 71 50 0     151 if( $is_native_glob )
    0          
167             {
168             $op = sub
169             {
170 77     77   1032 my $rv = print( $fh @_ );
171 77 50       212 return( $self->error( "Error writing ", CORE::length( $_[0] ), " bytes of data to output: $!" ) )
172             if( !defined( $rv ) );
173 77         187 return( $rv );
174 71         587 };
175             }
176             elsif( ( $meth = ( $self->_can( $fh => 'print' ) || $self->_can( $fh => 'write' ) ) ) )
177             {
178             $op = sub
179             {
180 0     0   0 local $@;
181 0         0 my $rv = eval{ $fh->$meth( @_ ) };
  0         0  
182 0 0       0 if( $@ )
183             {
184 0         0 return( $self->error( "Error writing ", CORE::length( $_[0] ), " bytes of data to output: $@" ) );
185             }
186 0 0       0 if( !defined( $rv ) )
187             {
188 0         0 my $err;
189 0 0       0 if( defined( $! ) )
    0          
    0          
190             {
191 0         0 $err = $!;
192             }
193             elsif( $self->_can( $fh => 'error' ) )
194             {
195 0         0 $err = $fh->error;
196             }
197             elsif( $self->_can( $fh => 'errstr' ) )
198             {
199 0         0 $err = $fh->errstr;
200             }
201 0         0 return( $self->error( "Error writing ", CORE::length( $_[0] ), " bytes of data to output: $err" ) );
202             }
203 0         0 return( $rv );
204 0         0 };
205             }
206             else
207             {
208 0         0 return( $self->error( "The file handle provided is not a native opened one and does not support the print() or write() methods." ) );
209             }
210             }
211             else
212             {
213 71 50       165 if( $is_native_glob )
    0          
214             {
215             $op = sub
216             {
217 47     47   851 my $n = read( $fh, $_[0], $_[1] );
218 47 50       117 if( !defined( $n ) )
219             {
220 0         0 return( $self->error( "Error reading ", $_[1], " bytes of data from input: $!" ) );
221             }
222 47         155 return( $n );
223 71         937 };
224             }
225             elsif( $self->_can( $fh => 'read' ) )
226             {
227             $op = sub
228             {
229 0     0   0 local $@;
230 0         0 my $n = eval{ $fh->read( @_ ) };
  0         0  
231 0 0       0 if( $@ )
232             {
233 0         0 return( $self->error( "Error reading ", $_[1], " bytes of data from input: $@" ) );
234             }
235 0 0       0 if( !defined( $n ) )
236             {
237 0         0 my $err;
238 0 0       0 if( defined( $! ) )
    0          
    0          
239             {
240 0         0 $err = $!;
241             }
242             elsif( $self->_can( $fh => 'error' ) )
243             {
244 0         0 $err = $fh->error;
245             }
246             elsif( $self->_can( $fh => 'errstr' ) )
247             {
248 0         0 $err = $fh->errstr;
249             }
250 0         0 return( $self->error( "Error reading ", $_[1], " bytes of data from input: $err" ) );
251             }
252 0         0 return( $n );
253 0         0 };
254             }
255             else
256             {
257 0         0 return( $self->error( "The file handle provided is not a native opened one and does not support the read() method." ) );
258             }
259             }
260 142         1088 return( $fh, $op );
261             }
262              
263             # NOTE: sub FREEZE is inherited
264              
265 0     0     sub STORABLE_freeze { CORE::return( CORE::shift->FREEZE( @_ ) ); }
266              
267 0     0     sub STORABLE_thaw { CORE::return( CORE::shift->THAW( @_ ) ); }
268              
269             # NOTE: sub THAW is inherited
270             }
271              
272             1;
273             # NOTE: POD
274             __END__
275              
276             =encoding utf-8
277              
278             =head1 NAME
279              
280             Mail::Make::Stream - Stream Infrastructure for Mail::Make Encoders
281              
282             =head1 SYNOPSIS
283              
284             # Used internally by Mail::Make::Stream::Base64 and
285             # Mail::Make::Stream::QuotedPrint. Not normally instantiated directly.
286              
287             =head1 VERSION
288              
289             v0.1.0
290              
291             =head1 DESCRIPTION
292              
293             C<Mail::Make::Stream> is the namespace root for stream-oriented encode/decode helpers. It also defines the inline class C<Mail::Make::Stream::Generic>, which is the parent of all concrete stream encoders.
294              
295             Its primary method is L</_get_glob_from_arg>, which normalises any of the following argument types into a C<( $fh, $op )> pair:
296              
297             =over 4
298              
299             =item * A native Perl glob or IO object
300              
301             =item * A scalar reference - opened via L<Module::Generic::Scalar::IO>
302              
303             =item * A plain string - treated as a file path and opened via C<new_file()>
304              
305             =back
306              
307             The returned C<$op> closure abstracts over the underlying handle type:
308              
309             =over 4
310              
311             =item * Read mode: C<$op-E<gt>( $buf, $len )> - bytes read (0 at EOF, undef on error)
312              
313             =item * Write mode: C<$op-E<gt>( $data )> - true on success, undef on error
314              
315             =back
316              
317             =head1 AUTHOR
318              
319             Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>
320              
321             =head1 SEE ALSO
322              
323             L<Mail::Make::Stream::Base64>, L<Mail::Make::Stream::QuotedPrint>, L<Mail::Make>
324              
325             =head1 COPYRIGHT & LICENSE
326              
327             Copyright(c) 2026 DEGUEST Pte. Ltd.
328              
329             All rights reserved.
330              
331             This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
332              
333             =cut