| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package Mail::Alias::LocalFile; |
|
2
|
|
|
|
|
|
|
|
|
3
|
|
|
|
|
|
|
our $VERSION = '0.01'; |
|
4
|
|
|
|
|
|
|
|
|
5
|
5
|
|
|
5
|
|
1745861
|
use 5.012; |
|
|
5
|
|
|
|
|
22
|
|
|
6
|
5
|
|
|
5
|
|
34
|
use strict; |
|
|
5
|
|
|
|
|
10
|
|
|
|
5
|
|
|
|
|
162
|
|
|
7
|
5
|
|
|
5
|
|
29
|
use warnings; |
|
|
5
|
|
|
|
|
12
|
|
|
|
5
|
|
|
|
|
335
|
|
|
8
|
5
|
|
|
5
|
|
3250
|
use Moo; |
|
|
5
|
|
|
|
|
45296
|
|
|
|
5
|
|
|
|
|
32
|
|
|
9
|
5
|
|
|
5
|
|
13683
|
use namespace::autoclean; |
|
|
5
|
|
|
|
|
115567
|
|
|
|
5
|
|
|
|
|
22
|
|
|
10
|
5
|
|
|
5
|
|
3448
|
use Email::Valid; |
|
|
5
|
|
|
|
|
619533
|
|
|
|
5
|
|
|
|
|
633
|
|
|
11
|
5
|
|
|
5
|
|
58
|
use Scalar::Util qw(reftype); |
|
|
5
|
|
|
|
|
18
|
|
|
|
5
|
|
|
|
|
452
|
|
|
12
|
5
|
|
|
5
|
|
3618
|
use Types::Standard qw(ArrayRef HashRef Str); |
|
|
5
|
|
|
|
|
717581
|
|
|
|
5
|
|
|
|
|
62
|
|
|
13
|
5
|
|
|
5
|
|
18003
|
use Data::Dumper::Concise; |
|
|
5
|
|
|
|
|
45274
|
|
|
|
5
|
|
|
|
|
12723
|
|
|
14
|
|
|
|
|
|
|
|
|
15
|
|
|
|
|
|
|
# Class attributes with type constraints |
|
16
|
|
|
|
|
|
|
|
|
17
|
|
|
|
|
|
|
has 'warning' => ( |
|
18
|
|
|
|
|
|
|
is => 'rw', |
|
19
|
|
|
|
|
|
|
isa => ArrayRef, |
|
20
|
|
|
|
|
|
|
default => sub { [] }, |
|
21
|
|
|
|
|
|
|
); |
|
22
|
|
|
|
|
|
|
|
|
23
|
|
|
|
|
|
|
has 'aliases' => ( |
|
24
|
|
|
|
|
|
|
is => 'ro', |
|
25
|
|
|
|
|
|
|
isa => HashRef, |
|
26
|
|
|
|
|
|
|
required => 1, |
|
27
|
|
|
|
|
|
|
); |
|
28
|
|
|
|
|
|
|
|
|
29
|
|
|
|
|
|
|
has 'expanded_addresses' => ( |
|
30
|
|
|
|
|
|
|
is => 'rw', |
|
31
|
|
|
|
|
|
|
isa => ArrayRef, |
|
32
|
|
|
|
|
|
|
default => sub { [] }, |
|
33
|
|
|
|
|
|
|
); |
|
34
|
|
|
|
|
|
|
|
|
35
|
|
|
|
|
|
|
has 'addresses_and_aliases' => ( |
|
36
|
|
|
|
|
|
|
is => 'rw', |
|
37
|
|
|
|
|
|
|
isa => ArrayRef, |
|
38
|
|
|
|
|
|
|
default => sub { [] }, |
|
39
|
|
|
|
|
|
|
); |
|
40
|
|
|
|
|
|
|
|
|
41
|
|
|
|
|
|
|
has 'original_input' => ( |
|
42
|
|
|
|
|
|
|
is => 'rw', |
|
43
|
|
|
|
|
|
|
isa => ArrayRef, |
|
44
|
|
|
|
|
|
|
default => sub { [] }, |
|
45
|
|
|
|
|
|
|
); |
|
46
|
|
|
|
|
|
|
|
|
47
|
|
|
|
|
|
|
has 'processed_aliases' => ( |
|
48
|
|
|
|
|
|
|
is => 'rw', |
|
49
|
|
|
|
|
|
|
isa => HashRef, |
|
50
|
|
|
|
|
|
|
default => sub { {} }, |
|
51
|
|
|
|
|
|
|
); |
|
52
|
|
|
|
|
|
|
|
|
53
|
|
|
|
|
|
|
has 'uniq_email_addresses' => ( |
|
54
|
|
|
|
|
|
|
is => 'rw', |
|
55
|
|
|
|
|
|
|
isa => ArrayRef, |
|
56
|
|
|
|
|
|
|
default => sub { [] }, |
|
57
|
|
|
|
|
|
|
); |
|
58
|
|
|
|
|
|
|
|
|
59
|
|
|
|
|
|
|
has 'mta_aliases' => ( |
|
60
|
|
|
|
|
|
|
is => 'rw', |
|
61
|
|
|
|
|
|
|
isa => ArrayRef, |
|
62
|
|
|
|
|
|
|
default => sub { [] }, |
|
63
|
|
|
|
|
|
|
); |
|
64
|
|
|
|
|
|
|
|
|
65
|
|
|
|
|
|
|
# Methods |
|
66
|
|
|
|
|
|
|
sub resolve_recipients { |
|
67
|
45
|
|
|
45
|
1
|
5976
|
my ( $self, $addresses_and_aliases_ref ) = @_; |
|
68
|
|
|
|
|
|
|
|
|
69
|
|
|
|
|
|
|
# Initialize all data structures |
|
70
|
45
|
|
|
|
|
995
|
$self->addresses_and_aliases($addresses_and_aliases_ref); |
|
71
|
45
|
|
|
|
|
1156
|
my @values = @{$addresses_and_aliases_ref}; |
|
|
45
|
|
|
|
|
142
|
|
|
72
|
45
|
|
|
|
|
921
|
$self->original_input( \@values ); |
|
73
|
45
|
|
|
|
|
1824
|
$self->expanded_addresses( [] ); |
|
74
|
45
|
|
|
|
|
1897
|
$self->processed_aliases( {} ); |
|
75
|
45
|
|
|
|
|
1799
|
$self->mta_aliases( [] ); |
|
76
|
|
|
|
|
|
|
|
|
77
|
|
|
|
|
|
|
# Process all addresses and aliases |
|
78
|
45
|
|
|
|
|
1004
|
while ( @{ $self->addresses_and_aliases } ) { |
|
|
108
|
|
|
|
|
2139
|
|
|
79
|
63
|
|
|
|
|
410
|
my $item = shift @{ $self->addresses_and_aliases }; |
|
|
63
|
|
|
|
|
1096
|
|
|
80
|
63
|
100
|
|
|
|
514
|
if ( $item =~ /^mta_/ ) { |
|
81
|
11
|
|
|
|
|
22
|
my @warning = @{ $self->warning }; |
|
|
11
|
|
|
|
|
205
|
|
|
82
|
11
|
|
|
|
|
80
|
push @warning, "ERROR: Alias keys with 'mta_' prefix are not allowed, skipping alias '$item'"; |
|
83
|
11
|
|
|
|
|
29
|
push @warning, "ERROR: Alias values may contain aliases with the 'mta_' prefix but not keys like '$item'"; |
|
84
|
11
|
|
|
|
|
229
|
$self->warning( \@warning ); |
|
85
|
11
|
|
|
|
|
266
|
next; |
|
86
|
|
|
|
|
|
|
} |
|
87
|
|
|
|
|
|
|
|
|
88
|
52
|
|
|
|
|
157
|
$self->extract_addresses_from_list($item); |
|
89
|
|
|
|
|
|
|
} |
|
90
|
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
# Remove duplicates and build the final comma-separated list |
|
92
|
45
|
|
|
|
|
405
|
my $uniq_email_recipients = $self->remove_duplicate_email_addresses(); |
|
93
|
|
|
|
|
|
|
|
|
94
|
|
|
|
|
|
|
# Combine email recipients with MTA aliases |
|
95
|
45
|
|
|
|
|
73
|
my @all_recipients = ( @{$uniq_email_recipients}, @{$self->mta_aliases} ); |
|
|
45
|
|
|
|
|
74
|
|
|
|
45
|
|
|
|
|
845
|
|
|
96
|
45
|
|
|
|
|
374
|
my $recipients = join( ',', @all_recipients ); |
|
97
|
|
|
|
|
|
|
|
|
98
|
|
|
|
|
|
|
# warn if there are no recipients (all were bad email addresses) |
|
99
|
45
|
100
|
|
|
|
146
|
if ($recipients eq "" ) { |
|
100
|
17
|
|
|
|
|
4289
|
push @{ $self->warning }, "ERROR, There are no valid email addresses"; |
|
|
17
|
|
|
|
|
341
|
|
|
101
|
|
|
|
|
|
|
} |
|
102
|
|
|
|
|
|
|
|
|
103
|
45
|
|
|
|
|
170
|
my %result; |
|
104
|
45
|
|
|
|
|
1069
|
$result{expanded_addresses} = $self->expanded_addresses; |
|
105
|
45
|
|
|
|
|
966
|
$result{uniq_email_addresses} = $self->uniq_email_addresses; |
|
106
|
45
|
|
|
|
|
321
|
$result{recipients} = $recipients; |
|
107
|
45
|
|
|
|
|
807
|
$result{original_input} = $self->original_input; |
|
108
|
45
|
|
|
|
|
323
|
$result{aliases} = $self->aliases; |
|
109
|
45
|
|
|
|
|
790
|
$result{processed_aliases} = $self->processed_aliases; |
|
110
|
45
|
|
|
|
|
953
|
$result{mta_aliases} = $self->mta_aliases; |
|
111
|
|
|
|
|
|
|
|
|
112
|
45
|
|
|
|
|
303
|
my $circular_references = $self->detect_circular_references($result{aliases}); |
|
113
|
|
|
|
|
|
|
|
|
114
|
45
|
|
|
|
|
121
|
$result{circular_references} = $circular_references; |
|
115
|
45
|
|
|
|
|
968
|
$result{warning} = $self->warning; |
|
116
|
45
|
|
|
|
|
453
|
return \%result; |
|
117
|
|
|
|
|
|
|
} |
|
118
|
|
|
|
|
|
|
|
|
119
|
|
|
|
|
|
|
sub extract_addresses_from_list { |
|
120
|
103
|
|
|
103
|
1
|
228
|
my ( $self, $element ) = @_; |
|
121
|
|
|
|
|
|
|
|
|
122
|
|
|
|
|
|
|
# Skip empty elements |
|
123
|
103
|
100
|
100
|
|
|
465
|
return unless defined $element && length $element; |
|
124
|
|
|
|
|
|
|
|
|
125
|
|
|
|
|
|
|
# Handle elements that contain multiple items (comma or space separated) |
|
126
|
97
|
100
|
66
|
|
|
369
|
if ( ( $element =~ /,/ ) || ( $element =~ / / ) ) { |
|
127
|
|
|
|
|
|
|
|
|
128
|
|
|
|
|
|
|
# Normalize spaces and commas |
|
129
|
16
|
|
|
|
|
55
|
my $multi_spaces = qr/\s+/x; # one or more consecutive spaces |
|
130
|
16
|
|
|
|
|
52
|
my $multi_commas = qr/,+/x; # one or more consecutive commas |
|
131
|
16
|
|
|
|
|
31
|
my $single_comma = ','; # a single comma |
|
132
|
|
|
|
|
|
|
|
|
133
|
16
|
|
|
|
|
138
|
$element =~ s{$multi_spaces}{$single_comma}g; |
|
134
|
16
|
|
|
|
|
108
|
$element =~ s{$multi_commas}{$single_comma}g; |
|
135
|
|
|
|
|
|
|
|
|
136
|
|
|
|
|
|
|
# Split the element into individual items |
|
137
|
16
|
|
|
|
|
52
|
my @items = split( /,/x, $element ); |
|
138
|
16
|
|
|
|
|
36
|
foreach my $single_item (@items) { |
|
139
|
39
|
|
|
|
|
177
|
$single_item =~ s/^\s+|\s+$//g; # Trim whitespace |
|
140
|
|
|
|
|
|
|
# Process each individual item if it's not empty |
|
141
|
39
|
100
|
|
|
|
95
|
if ( length $single_item ) { |
|
142
|
38
|
|
|
|
|
78
|
$self->process_single_item($single_item); |
|
143
|
|
|
|
|
|
|
} |
|
144
|
|
|
|
|
|
|
} |
|
145
|
|
|
|
|
|
|
} |
|
146
|
|
|
|
|
|
|
else { |
|
147
|
|
|
|
|
|
|
# Process a simple element (not comma/space separated) |
|
148
|
81
|
|
|
|
|
401
|
$element =~ s/^\s+|\s+$//g; # Trim whitespace |
|
149
|
81
|
50
|
|
|
|
183
|
if ( length $element ) { |
|
150
|
81
|
|
|
|
|
191
|
$self->process_single_item($element); |
|
151
|
|
|
|
|
|
|
} |
|
152
|
|
|
|
|
|
|
} |
|
153
|
97
|
|
|
|
|
182
|
return; |
|
154
|
|
|
|
|
|
|
} |
|
155
|
|
|
|
|
|
|
|
|
156
|
|
|
|
|
|
|
sub process_single_item { |
|
157
|
119
|
|
|
119
|
1
|
215
|
my ( $self, $single_item ) = @_; |
|
158
|
|
|
|
|
|
|
|
|
159
|
|
|
|
|
|
|
# Check if this is an MTA-delegated alias (starts with mta_) |
|
160
|
119
|
100
|
|
|
|
371
|
if ( $single_item =~ /^mta_(.+)$/x ) { |
|
|
|
100
|
|
|
|
|
|
|
161
|
11
|
|
|
|
|
30
|
$self->process_mta_alias($1); |
|
162
|
|
|
|
|
|
|
} |
|
163
|
|
|
|
|
|
|
# Process based on whether it looks like an email address |
|
164
|
|
|
|
|
|
|
elsif ( $single_item =~ /@/x ) { |
|
165
|
45
|
|
|
|
|
105
|
$self->process_potential_email($single_item); |
|
166
|
|
|
|
|
|
|
} |
|
167
|
|
|
|
|
|
|
else { |
|
168
|
63
|
|
|
|
|
159
|
$self->process_potential_alias($single_item); |
|
169
|
|
|
|
|
|
|
} |
|
170
|
119
|
|
|
|
|
251
|
return; |
|
171
|
|
|
|
|
|
|
} |
|
172
|
|
|
|
|
|
|
|
|
173
|
|
|
|
|
|
|
sub process_mta_alias { |
|
174
|
11
|
|
|
11
|
1
|
33
|
my ( $self, $alias ) = @_; |
|
175
|
|
|
|
|
|
|
|
|
176
|
|
|
|
|
|
|
# Add the alias to the list of MTA aliases (without the mta_ prefix) |
|
177
|
11
|
|
|
|
|
18
|
push @{ $self->mta_aliases }, $alias; |
|
|
11
|
|
|
|
|
246
|
|
|
178
|
11
|
|
|
|
|
82
|
return; |
|
179
|
|
|
|
|
|
|
} |
|
180
|
|
|
|
|
|
|
|
|
181
|
|
|
|
|
|
|
sub process_potential_email { |
|
182
|
48
|
|
|
48
|
1
|
2298
|
my ( $self, $item ) = @_; |
|
183
|
|
|
|
|
|
|
|
|
184
|
|
|
|
|
|
|
# Normalize and validate the email address |
|
185
|
48
|
|
|
|
|
252
|
$item = lc($item); |
|
186
|
|
|
|
|
|
|
|
|
187
|
48
|
|
|
|
|
274
|
my $address = Email::Valid->address($item); |
|
188
|
|
|
|
|
|
|
# if it was a bad email format, $address is not defined |
|
189
|
48
|
100
|
|
|
|
31022
|
if ( !defined $address ) { |
|
190
|
3
|
|
|
|
|
6
|
push @{ $self->warning}, |
|
|
3
|
|
|
|
|
56
|
|
|
191
|
|
|
|
|
|
|
"ERROR: $item is not a correctly formatted email address, skipping"; |
|
192
|
|
|
|
|
|
|
} |
|
193
|
|
|
|
|
|
|
else { |
|
194
|
45
|
50
|
|
|
|
119
|
if ($address) { |
|
195
|
45
|
|
|
|
|
66
|
push @{ $self->expanded_addresses }, $address; |
|
|
45
|
|
|
|
|
1481
|
|
|
196
|
|
|
|
|
|
|
} |
|
197
|
|
|
|
|
|
|
} |
|
198
|
48
|
|
|
|
|
366
|
return; |
|
199
|
|
|
|
|
|
|
} |
|
200
|
|
|
|
|
|
|
|
|
201
|
|
|
|
|
|
|
sub process_potential_alias { |
|
202
|
64
|
|
|
64
|
1
|
116
|
my ( $self, $alias ) = @_; |
|
203
|
64
|
|
|
|
|
1341
|
my $processed_aliases = $self->processed_aliases; |
|
204
|
|
|
|
|
|
|
|
|
205
|
|
|
|
|
|
|
# Check if this alias exists |
|
206
|
64
|
100
|
|
|
|
495
|
if ( !exists $self->aliases->{$alias} ) { |
|
207
|
13
|
|
|
|
|
23
|
push @{ $self->warning }, "ERROR: The alias $alias was not found, skipping."; |
|
|
13
|
|
|
|
|
233
|
|
|
208
|
13
|
|
|
|
|
86
|
return; |
|
209
|
|
|
|
|
|
|
} |
|
210
|
|
|
|
|
|
|
|
|
211
|
|
|
|
|
|
|
# Check if we've already processed this alias |
|
212
|
51
|
100
|
|
|
|
109
|
if ( $processed_aliases->{$alias} ) { |
|
213
|
|
|
|
|
|
|
|
|
214
|
|
|
|
|
|
|
# Skip it - we've already processed it completely |
|
215
|
|
|
|
|
|
|
# prevents duplicate inclusion of and alias |
|
216
|
2
|
|
|
|
|
6
|
return; |
|
217
|
|
|
|
|
|
|
} |
|
218
|
|
|
|
|
|
|
|
|
219
|
49
|
100
|
100
|
|
|
191
|
if ( ( defined reftype( $self->aliases->{$alias} ) ) |
|
220
|
|
|
|
|
|
|
&& ( reftype( $self->aliases->{$alias} ) eq 'ARRAY' ) ) |
|
221
|
|
|
|
|
|
|
{ |
|
222
|
|
|
|
|
|
|
# Handle array of values, convert to string of values |
|
223
|
1
|
|
|
|
|
1
|
my @values = @{ $self->aliases->{$alias} }; |
|
|
1
|
|
|
|
|
5
|
|
|
224
|
1
|
|
|
|
|
3
|
my $string = join( ",", @values ); |
|
225
|
|
|
|
|
|
|
|
|
226
|
1
|
|
|
|
|
3
|
$processed_aliases->{$alias} = 'Processed'; |
|
227
|
|
|
|
|
|
|
} |
|
228
|
|
|
|
|
|
|
else { |
|
229
|
|
|
|
|
|
|
# already a string, just use it as the value |
|
230
|
48
|
|
|
|
|
135
|
$processed_aliases->{$alias} = 'Processed'; |
|
231
|
|
|
|
|
|
|
} |
|
232
|
|
|
|
|
|
|
|
|
233
|
49
|
|
|
|
|
4706
|
$self->processed_aliases($processed_aliases); |
|
234
|
|
|
|
|
|
|
|
|
235
|
|
|
|
|
|
|
# Expand the alias |
|
236
|
49
|
|
|
|
|
1397
|
$self->expand_alias($alias); |
|
237
|
49
|
|
|
|
|
102
|
return; |
|
238
|
|
|
|
|
|
|
} |
|
239
|
|
|
|
|
|
|
|
|
240
|
|
|
|
|
|
|
sub expand_alias { |
|
241
|
49
|
|
|
49
|
1
|
89
|
my ( $self, $alias ) = @_; |
|
242
|
|
|
|
|
|
|
|
|
243
|
49
|
|
|
|
|
274
|
my $alias_items = $self->aliases->{$alias}; |
|
244
|
|
|
|
|
|
|
|
|
245
|
|
|
|
|
|
|
# Handle different types of alias values |
|
246
|
49
|
100
|
100
|
|
|
154
|
if ( ( defined reftype($alias_items) ) |
|
247
|
|
|
|
|
|
|
&& ( reftype($alias_items) eq 'ARRAY' ) ) |
|
248
|
|
|
|
|
|
|
{ |
|
249
|
|
|
|
|
|
|
# Handle array of values |
|
250
|
1
|
|
|
|
|
2
|
foreach my $element (@$alias_items) { |
|
251
|
|
|
|
|
|
|
|
|
252
|
|
|
|
|
|
|
# Process each element directly to avoid re-adding to the queue |
|
253
|
2
|
|
|
|
|
3
|
$self->extract_addresses_from_list($element); |
|
254
|
|
|
|
|
|
|
} |
|
255
|
|
|
|
|
|
|
} |
|
256
|
|
|
|
|
|
|
else { |
|
257
|
|
|
|
|
|
|
# Handle scalar value |
|
258
|
48
|
50
|
66
|
|
|
287
|
if ( ( $alias_items =~ /,/x ) || ( $alias_items =~ / /x ) ) { |
|
|
|
0
|
|
|
|
|
|
|
259
|
|
|
|
|
|
|
|
|
260
|
|
|
|
|
|
|
# Multiple items in the scalar value |
|
261
|
48
|
|
|
|
|
119
|
$self->extract_addresses_from_list($alias_items); |
|
262
|
|
|
|
|
|
|
} |
|
263
|
|
|
|
|
|
|
elsif ( $alias_items =~ /@/x ) { |
|
264
|
|
|
|
|
|
|
|
|
265
|
|
|
|
|
|
|
# Looks like an email address, validate it |
|
266
|
0
|
|
|
|
|
0
|
$self->process_potential_email($alias_items); |
|
267
|
|
|
|
|
|
|
} |
|
268
|
|
|
|
|
|
|
else { |
|
269
|
|
|
|
|
|
|
# Probably an alias, process directly |
|
270
|
0
|
|
|
|
|
0
|
$self->process_potential_alias($alias_items); |
|
271
|
|
|
|
|
|
|
} |
|
272
|
|
|
|
|
|
|
} |
|
273
|
49
|
|
|
|
|
80
|
return; |
|
274
|
|
|
|
|
|
|
} |
|
275
|
|
|
|
|
|
|
|
|
276
|
|
|
|
|
|
|
sub remove_duplicate_email_addresses { |
|
277
|
45
|
|
|
45
|
1
|
116
|
my ($self) = @_; |
|
278
|
|
|
|
|
|
|
|
|
279
|
|
|
|
|
|
|
# Use a hash to track unique addresses |
|
280
|
45
|
|
|
|
|
76
|
my @uniq_email_addresses; |
|
281
|
|
|
|
|
|
|
my %found_once; |
|
282
|
|
|
|
|
|
|
|
|
283
|
45
|
|
|
|
|
62
|
foreach my $recipient ( @{ $self->expanded_addresses } ) { |
|
|
45
|
|
|
|
|
979
|
|
|
284
|
42
|
100
|
|
|
|
222
|
if ( !exists $found_once{$recipient} ) { |
|
285
|
40
|
|
|
|
|
99
|
push @uniq_email_addresses, $recipient; |
|
286
|
40
|
|
|
|
|
107
|
$found_once{$recipient} = 'true'; |
|
287
|
|
|
|
|
|
|
} |
|
288
|
|
|
|
|
|
|
} |
|
289
|
|
|
|
|
|
|
|
|
290
|
45
|
|
|
|
|
5671
|
$self->uniq_email_addresses( \@uniq_email_addresses ); |
|
291
|
45
|
|
|
|
|
1283
|
return \@uniq_email_addresses; |
|
292
|
|
|
|
|
|
|
} |
|
293
|
|
|
|
|
|
|
|
|
294
|
|
|
|
|
|
|
# Function to detect circular references |
|
295
|
|
|
|
|
|
|
sub detect_circular_references { |
|
296
|
48
|
|
|
48
|
1
|
154
|
my ($self, $aliases) = @_; |
|
297
|
48
|
|
|
|
|
76
|
my %seen_paths = (); |
|
298
|
48
|
|
|
|
|
74
|
my @circular_references = (); |
|
299
|
|
|
|
|
|
|
|
|
300
|
48
|
|
|
|
|
197
|
foreach my $key ( keys %$aliases ) { |
|
301
|
|
|
|
|
|
|
# Skip checking aliases with mta_ prefix |
|
302
|
|
|
|
|
|
|
# Should not exist, create warning |
|
303
|
131
|
100
|
|
|
|
4469
|
if ( $key =~ /^mta_/ ) { |
|
304
|
6
|
|
|
|
|
12
|
my @warning = @{ $self->warning }; |
|
|
6
|
|
|
|
|
119
|
|
|
305
|
6
|
|
|
|
|
49
|
push @warning, "ERROR: Alias keys with 'mta_' prefix are not allowed, skipping alias '$key'"; |
|
306
|
6
|
|
|
|
|
119
|
$self->warning( \@warning ); |
|
307
|
6
|
|
|
|
|
124
|
next; |
|
308
|
|
|
|
|
|
|
} |
|
309
|
|
|
|
|
|
|
|
|
310
|
125
|
|
|
|
|
251
|
my @path = ($key); |
|
311
|
125
|
|
|
|
|
285
|
check_circular( $key, $aliases, \@path, \%seen_paths, |
|
312
|
|
|
|
|
|
|
\@circular_references ); |
|
313
|
|
|
|
|
|
|
} |
|
314
|
|
|
|
|
|
|
|
|
315
|
48
|
100
|
|
|
|
121
|
if ( $circular_references[0] ) { |
|
316
|
6
|
|
|
|
|
12
|
my @warning = @{ $self->warning }; |
|
|
6
|
|
|
|
|
220
|
|
|
317
|
6
|
|
|
|
|
69
|
push @warning, "ERROR: The aliases file contains entries that are circular references"; |
|
318
|
6
|
|
|
|
|
156
|
$self->warning( \@warning ); |
|
319
|
|
|
|
|
|
|
} |
|
320
|
48
|
|
|
|
|
304
|
return \@circular_references; |
|
321
|
|
|
|
|
|
|
} |
|
322
|
|
|
|
|
|
|
|
|
323
|
|
|
|
|
|
|
# Recursive function to check for circular references |
|
324
|
|
|
|
|
|
|
sub check_circular { |
|
325
|
199
|
|
|
199
|
1
|
372
|
my ( $current_key, $aliases, $path, $seen_paths, $circular_references ) = @_; |
|
326
|
199
|
|
|
|
|
325
|
my $value = $aliases->{$current_key}; |
|
327
|
|
|
|
|
|
|
|
|
328
|
|
|
|
|
|
|
# If value is a reference to an array, process each element |
|
329
|
199
|
100
|
|
|
|
483
|
if ( ref($value) eq 'ARRAY' ) { |
|
|
|
100
|
|
|
|
|
|
|
330
|
16
|
|
|
|
|
30
|
foreach my $item (@$value) { |
|
331
|
44
|
|
|
|
|
83
|
process_item( $item, $aliases, $path, $seen_paths, |
|
332
|
|
|
|
|
|
|
$circular_references ); |
|
333
|
|
|
|
|
|
|
} |
|
334
|
|
|
|
|
|
|
} |
|
335
|
|
|
|
|
|
|
|
|
336
|
|
|
|
|
|
|
# If value is a scalar (string), process it directly |
|
337
|
|
|
|
|
|
|
elsif ( !ref($value) ) { |
|
338
|
181
|
|
|
|
|
342
|
process_item( $value, $aliases, $path, $seen_paths, |
|
339
|
|
|
|
|
|
|
$circular_references ); |
|
340
|
|
|
|
|
|
|
} |
|
341
|
|
|
|
|
|
|
} |
|
342
|
|
|
|
|
|
|
|
|
343
|
|
|
|
|
|
|
# Process individual items and check for circular references |
|
344
|
|
|
|
|
|
|
sub process_item { |
|
345
|
225
|
|
|
225
|
1
|
411
|
my ( $item, $aliases, $path, $seen_paths, $circular_references ) = @_; |
|
346
|
|
|
|
|
|
|
|
|
347
|
|
|
|
|
|
|
# Split comma-separated values and trim whitespace |
|
348
|
225
|
|
|
|
|
540
|
my @items = split( /,/, $item ); |
|
349
|
225
|
|
|
|
|
398
|
foreach my $subitem (@items) { |
|
350
|
305
|
|
|
|
|
1538
|
$subitem =~ s/^\s+|\s+$//g; # Trim whitespace |
|
351
|
305
|
100
|
|
|
|
628
|
next unless $subitem; # Skip empty items |
|
352
|
|
|
|
|
|
|
|
|
353
|
|
|
|
|
|
|
# Skip items with mta_ prefix |
|
354
|
293
|
100
|
|
|
|
625
|
next if $subitem =~ /^mta_/x; |
|
355
|
|
|
|
|
|
|
|
|
356
|
|
|
|
|
|
|
# Check if this is a reference to another alias |
|
357
|
278
|
100
|
|
|
|
825
|
if ( exists $aliases->{$subitem} ) { |
|
358
|
|
|
|
|
|
|
|
|
359
|
|
|
|
|
|
|
# Check for circular reference |
|
360
|
104
|
100
|
|
|
|
178
|
if ( grep { $_ eq $subitem } @$path ) { |
|
|
180
|
|
|
|
|
401
|
|
|
361
|
|
|
|
|
|
|
|
|
362
|
|
|
|
|
|
|
# Found a circular reference |
|
363
|
30
|
|
|
|
|
88
|
my @circular_path = ( @$path, $subitem ); |
|
364
|
30
|
|
|
|
|
135
|
my $path_str = join( " -> ", @circular_path ); |
|
365
|
30
|
|
|
|
|
208
|
push @$circular_references, $path_str; |
|
366
|
|
|
|
|
|
|
} |
|
367
|
|
|
|
|
|
|
else { |
|
368
|
|
|
|
|
|
|
# Continue tracing the path |
|
369
|
74
|
|
|
|
|
183
|
my @new_path = ( @$path, $subitem ); |
|
370
|
74
|
|
|
|
|
172
|
check_circular( $subitem, $aliases, \@new_path, $seen_paths, |
|
371
|
|
|
|
|
|
|
$circular_references ); |
|
372
|
|
|
|
|
|
|
} |
|
373
|
|
|
|
|
|
|
} |
|
374
|
|
|
|
|
|
|
} |
|
375
|
|
|
|
|
|
|
} |
|
376
|
|
|
|
|
|
|
|
|
377
|
|
|
|
|
|
|
# Clean up with namespace::autoclean |
|
378
|
|
|
|
|
|
|
__PACKAGE__->meta->make_immutable; |
|
379
|
|
|
|
|
|
|
|
|
380
|
|
|
|
|
|
|
1; |
|
381
|
|
|
|
|
|
|
|
|
382
|
|
|
|
|
|
|
=head1 NAME |
|
383
|
|
|
|
|
|
|
|
|
384
|
|
|
|
|
|
|
Mail::Alias::LocalFile - A module for resolving email aliases and building recipient lists |
|
385
|
|
|
|
|
|
|
from a locally maintained aliases file. The MTA shared aliases file may stll be |
|
386
|
|
|
|
|
|
|
used when and if desired. |
|
387
|
|
|
|
|
|
|
|
|
388
|
|
|
|
|
|
|
=head1 SYNOPSIS |
|
389
|
|
|
|
|
|
|
|
|
390
|
|
|
|
|
|
|
use Mail::Alias::LocalFile; |
|
391
|
|
|
|
|
|
|
|
|
392
|
|
|
|
|
|
|
$resolver = Mail::Alias::LocalFile->new(aliases => $alias_file_href); |
|
393
|
|
|
|
|
|
|
$result = $resolver->resolve_recipients($intended_recipients_aref); |
|
394
|
|
|
|
|
|
|
|
|
395
|
|
|
|
|
|
|
# Get the final comma-separated list of recipients |
|
396
|
|
|
|
|
|
|
my $recipients = $result->{recipients}; |
|
397
|
|
|
|
|
|
|
|
|
398
|
|
|
|
|
|
|
# Check for any warnings |
|
399
|
|
|
|
|
|
|
if (@{$result->{warning}}) { |
|
400
|
|
|
|
|
|
|
print "Warnings: ", join("\n", @{$result->{warning}}), "\n"; |
|
401
|
|
|
|
|
|
|
} |
|
402
|
|
|
|
|
|
|
|
|
403
|
|
|
|
|
|
|
|
|
404
|
|
|
|
|
|
|
You can also detect all circular references in the local aliases file: |
|
405
|
|
|
|
|
|
|
|
|
406
|
|
|
|
|
|
|
$resolver = Mail::Alias::LocalFile->new(aliases => $alias_file_href); |
|
407
|
|
|
|
|
|
|
$circular = $resolver->detect_circular_references($alias_file_ref); |
|
408
|
|
|
|
|
|
|
my @circular_references = @{$circular}; |
|
409
|
|
|
|
|
|
|
if ( $circular_references[0] ) { |
|
410
|
|
|
|
|
|
|
print "Circular references detected: ", join("\n", @circular_references), "\n"; |
|
411
|
|
|
|
|
|
|
} |
|
412
|
|
|
|
|
|
|
|
|
413
|
|
|
|
|
|
|
=head1 DESCRIPTION |
|
414
|
|
|
|
|
|
|
|
|
415
|
|
|
|
|
|
|
The C module provides functionality to resolve email addresses and aliases into a |
|
416
|
|
|
|
|
|
|
unique list of email recipients. It handles nested aliases, validates email addresses, and |
|
417
|
|
|
|
|
|
|
detects circular references in alias definitions. |
|
418
|
|
|
|
|
|
|
|
|
419
|
|
|
|
|
|
|
This module is particularly useful for applications that need to expand distribution lists |
|
420
|
|
|
|
|
|
|
or group aliases into actual email addresses while ensuring uniqueness and validity. |
|
421
|
|
|
|
|
|
|
|
|
422
|
|
|
|
|
|
|
Use of the system aliases file when desired is supported via the special alias prefix 'mta_'. |
|
423
|
|
|
|
|
|
|
Values with the prefix 'mta_' will not be expanded locally but will be passed to the MTA for |
|
424
|
|
|
|
|
|
|
expansion. The 'mta_' prefix will be stripped before passing to the MTA. |
|
425
|
|
|
|
|
|
|
|
|
426
|
|
|
|
|
|
|
Alias keys with the 'mta_' prefix are not allowed and will be skipped with a warning. |
|
427
|
|
|
|
|
|
|
|
|
428
|
|
|
|
|
|
|
my $aliases = { |
|
429
|
|
|
|
|
|
|
'group2' => 'Mary@example.com, Joe@example.com' |
|
430
|
|
|
|
|
|
|
'system' => 'mta_postmaster', |
|
431
|
|
|
|
|
|
|
'normal' => 'normal@example.com', |
|
432
|
|
|
|
|
|
|
}; |
|
433
|
|
|
|
|
|
|
|
|
434
|
|
|
|
|
|
|
use Mail::Alias::LocalFile; |
|
435
|
|
|
|
|
|
|
|
|
436
|
|
|
|
|
|
|
my $resolver = Mail::Alias::LocalFile->new(aliases => $aliases); |
|
437
|
|
|
|
|
|
|
my $result = $resolver->resolve_recipients([ bill@example.com group2 system ]); |
|
438
|
|
|
|
|
|
|
my $recipients = $result->{recipients}; |
|
439
|
|
|
|
|
|
|
|
|
440
|
|
|
|
|
|
|
The recipients email addresses to pass to your email client code is: |
|
441
|
|
|
|
|
|
|
'bill@example.com,mary@example,com,joe@example.com,postmaster' |
|
442
|
|
|
|
|
|
|
|
|
443
|
|
|
|
|
|
|
group2 and mta_postmaster are expanded from the local aliases file |
|
444
|
|
|
|
|
|
|
and the postmaster alias will expand from the system wide aliases file |
|
445
|
|
|
|
|
|
|
|
|
446
|
|
|
|
|
|
|
=head1 OUTPUT |
|
447
|
|
|
|
|
|
|
Returns a hash_ref: |
|
448
|
|
|
|
|
|
|
|
|
449
|
|
|
|
|
|
|
$result{recipients} |
|
450
|
|
|
|
|
|
|
$result{warning} |
|
451
|
|
|
|
|
|
|
$result{original_input} |
|
452
|
|
|
|
|
|
|
$result{aliases} |
|
453
|
|
|
|
|
|
|
$result{processed_aliases} |
|
454
|
|
|
|
|
|
|
$result{expanded_addresses} |
|
455
|
|
|
|
|
|
|
$result{uniq_email_addresses} |
|
456
|
|
|
|
|
|
|
$result{mta_aliases} |
|
457
|
|
|
|
|
|
|
|
|
458
|
|
|
|
|
|
|
Where $result{recipients} is the comma separated expanded email addresses and MTA aliases |
|
459
|
|
|
|
|
|
|
suitable for use in the To: field of your email code |
|
460
|
|
|
|
|
|
|
|
|
461
|
|
|
|
|
|
|
Always check $result{warning} to identify problems encountered, if any. |
|
462
|
|
|
|
|
|
|
The other available result values are useful for troubleshooting |
|
463
|
|
|
|
|
|
|
|
|
464
|
|
|
|
|
|
|
=head1 ATTRIBUTES |
|
465
|
|
|
|
|
|
|
|
|
466
|
|
|
|
|
|
|
=head2 warning |
|
467
|
|
|
|
|
|
|
|
|
468
|
|
|
|
|
|
|
An array reference storing warning messages generated during processing. |
|
469
|
|
|
|
|
|
|
|
|
470
|
|
|
|
|
|
|
$resolver->warning(["Warning message"]); |
|
471
|
|
|
|
|
|
|
my $warnings = $resolver->warning; |
|
472
|
|
|
|
|
|
|
|
|
473
|
|
|
|
|
|
|
=head2 aliases |
|
474
|
|
|
|
|
|
|
|
|
475
|
|
|
|
|
|
|
A hash reference mapping alias names to their values (either strings or array |
|
476
|
|
|
|
|
|
|
references). This attribute is required when creating a new instance. It is |
|
477
|
|
|
|
|
|
|
provided from your application after your application loads a locally |
|
478
|
|
|
|
|
|
|
maintained aliases file. |
|
479
|
|
|
|
|
|
|
|
|
480
|
|
|
|
|
|
|
my $resolver = Mail::Alias::LocalFile->new(aliases => $aliases); |
|
481
|
|
|
|
|
|
|
my $aliases = $resolver->aliases; |
|
482
|
|
|
|
|
|
|
|
|
483
|
|
|
|
|
|
|
This is how Perl sees your local alias file data for parsing. |
|
484
|
|
|
|
|
|
|
|
|
485
|
|
|
|
|
|
|
=head2 expanded_addresses |
|
486
|
|
|
|
|
|
|
|
|
487
|
|
|
|
|
|
|
An array reference containing the cumulative expanded email addresses (including |
|
488
|
|
|
|
|
|
|
duplicates as each item from the input is expanded |
|
489
|
|
|
|
|
|
|
|
|
490
|
|
|
|
|
|
|
my $all_addresses = $resolver->expanded_addresses; |
|
491
|
|
|
|
|
|
|
|
|
492
|
|
|
|
|
|
|
For troubleshooting if your result is not as expected. |
|
493
|
|
|
|
|
|
|
|
|
494
|
|
|
|
|
|
|
=head2 addresses_and_aliases |
|
495
|
|
|
|
|
|
|
|
|
496
|
|
|
|
|
|
|
An array reference. A working copy of the original_input that is consumed |
|
497
|
|
|
|
|
|
|
by shift, to provide each item of the array for analysis. |
|
498
|
|
|
|
|
|
|
|
|
499
|
|
|
|
|
|
|
|
|
500
|
|
|
|
|
|
|
=head2 original_input |
|
501
|
|
|
|
|
|
|
|
|
502
|
|
|
|
|
|
|
An array reference containing the original input provided to C. |
|
503
|
|
|
|
|
|
|
|
|
504
|
|
|
|
|
|
|
my $result = $resolver->resolve_recipients([ bill@example.com group2 system ]); |
|
505
|
|
|
|
|
|
|
my $original = $resolver->original_input; |
|
506
|
|
|
|
|
|
|
|
|
507
|
|
|
|
|
|
|
Stored for troubleshooting purposes, if needed. |
|
508
|
|
|
|
|
|
|
|
|
509
|
|
|
|
|
|
|
=head2 processed_aliases |
|
510
|
|
|
|
|
|
|
|
|
511
|
|
|
|
|
|
|
A hash reference tracking which aliases have been processed and used to avoid |
|
512
|
|
|
|
|
|
|
duplicate processing and suppress circular references (if any). |
|
513
|
|
|
|
|
|
|
|
|
514
|
|
|
|
|
|
|
my $processed = $resolver->processed_aliases; |
|
515
|
|
|
|
|
|
|
|
|
516
|
|
|
|
|
|
|
=head2 uniq_email_addresses |
|
517
|
|
|
|
|
|
|
|
|
518
|
|
|
|
|
|
|
An array reference containing the final list of unique email addresses after |
|
519
|
|
|
|
|
|
|
expansion and deduplication. |
|
520
|
|
|
|
|
|
|
|
|
521
|
|
|
|
|
|
|
my $unique = $resolver->uniq_email_addresses; |
|
522
|
|
|
|
|
|
|
|
|
523
|
|
|
|
|
|
|
=head2 mta_aliases |
|
524
|
|
|
|
|
|
|
|
|
525
|
|
|
|
|
|
|
An array reference containing aliases that should be passed to the MTA for |
|
526
|
|
|
|
|
|
|
expansion after the 'mta_' prefix has been removed. Not used unless the local |
|
527
|
|
|
|
|
|
|
alias has a value containing an alias with the mta_ prefix. The mta_ prefix |
|
528
|
|
|
|
|
|
|
must be used in order to pass an alias through for expansion by the MTA alias |
|
529
|
|
|
|
|
|
|
file. |
|
530
|
|
|
|
|
|
|
|
|
531
|
|
|
|
|
|
|
my $mta_aliases = $resolver->mta_aliases; |
|
532
|
|
|
|
|
|
|
|
|
533
|
|
|
|
|
|
|
=head1 METHODS |
|
534
|
|
|
|
|
|
|
|
|
535
|
|
|
|
|
|
|
=head2 resolve_recipients |
|
536
|
|
|
|
|
|
|
|
|
537
|
|
|
|
|
|
|
Expands a list of addresses and aliases into a unique list of email addresses. |
|
538
|
|
|
|
|
|
|
|
|
539
|
|
|
|
|
|
|
my $result = $resolver->resolve_recipients(['team', 'john@example.com']); |
|
540
|
|
|
|
|
|
|
|
|
541
|
|
|
|
|
|
|
Returns a hash reference with the following keys: |
|
542
|
|
|
|
|
|
|
|
|
543
|
|
|
|
|
|
|
=over 4 |
|
544
|
|
|
|
|
|
|
|
|
545
|
|
|
|
|
|
|
=item * C: All expanded addresses (including duplicates) |
|
546
|
|
|
|
|
|
|
|
|
547
|
|
|
|
|
|
|
=item * C: Unique email addresses after deduplication |
|
548
|
|
|
|
|
|
|
|
|
549
|
|
|
|
|
|
|
=item * C: Comma-separated string of unique addresses and MTA aliases |
|
550
|
|
|
|
|
|
|
|
|
551
|
|
|
|
|
|
|
=item * C: Original input provided |
|
552
|
|
|
|
|
|
|
|
|
553
|
|
|
|
|
|
|
=item * C: Any warnings generated during processing |
|
554
|
|
|
|
|
|
|
|
|
555
|
|
|
|
|
|
|
=item * C: Reference to the original aliases hash |
|
556
|
|
|
|
|
|
|
|
|
557
|
|
|
|
|
|
|
=item * C: Aliases that were processed |
|
558
|
|
|
|
|
|
|
|
|
559
|
|
|
|
|
|
|
=item * C: Aliases designated to be processed by the MTA |
|
560
|
|
|
|
|
|
|
|
|
561
|
|
|
|
|
|
|
=back |
|
562
|
|
|
|
|
|
|
|
|
563
|
|
|
|
|
|
|
=head2 extract_addresses_from_list |
|
564
|
|
|
|
|
|
|
|
|
565
|
|
|
|
|
|
|
Processes a single element that might contain multiple addresses or aliases. |
|
566
|
|
|
|
|
|
|
|
|
567
|
|
|
|
|
|
|
$resolver->extract_addresses_from_list('john@example.com, team'); |
|
568
|
|
|
|
|
|
|
|
|
569
|
|
|
|
|
|
|
=head2 process_single_item |
|
570
|
|
|
|
|
|
|
|
|
571
|
|
|
|
|
|
|
Processes a single item, determining if it's an email address, an alias, or an MTA-delegated alias. |
|
572
|
|
|
|
|
|
|
|
|
573
|
|
|
|
|
|
|
$resolver->process_single_item('john@example.com'); |
|
574
|
|
|
|
|
|
|
|
|
575
|
|
|
|
|
|
|
=head2 process_mta_alias |
|
576
|
|
|
|
|
|
|
|
|
577
|
|
|
|
|
|
|
Processes an MTA-delegated alias (with 'mta_' prefix). |
|
578
|
|
|
|
|
|
|
|
|
579
|
|
|
|
|
|
|
$resolver->process_mta_alias('postmaster'); |
|
580
|
|
|
|
|
|
|
|
|
581
|
|
|
|
|
|
|
=head2 process_potential_email |
|
582
|
|
|
|
|
|
|
|
|
583
|
|
|
|
|
|
|
Validates and normalizes a potential email address. |
|
584
|
|
|
|
|
|
|
|
|
585
|
|
|
|
|
|
|
$resolver->process_potential_email('John@Example.COM'); |
|
586
|
|
|
|
|
|
|
|
|
587
|
|
|
|
|
|
|
=head2 process_potential_alias |
|
588
|
|
|
|
|
|
|
|
|
589
|
|
|
|
|
|
|
Processes an alias name, expanding it if found. |
|
590
|
|
|
|
|
|
|
|
|
591
|
|
|
|
|
|
|
$resolver->process_potential_alias('team'); |
|
592
|
|
|
|
|
|
|
|
|
593
|
|
|
|
|
|
|
=head2 expand_alias |
|
594
|
|
|
|
|
|
|
|
|
595
|
|
|
|
|
|
|
Expands an alias into its constituent addresses and/or other aliases. |
|
596
|
|
|
|
|
|
|
|
|
597
|
|
|
|
|
|
|
$resolver->expand_alias('team'); |
|
598
|
|
|
|
|
|
|
|
|
599
|
|
|
|
|
|
|
=head2 remove_duplicate_email_addresses |
|
600
|
|
|
|
|
|
|
|
|
601
|
|
|
|
|
|
|
Removes duplicate email addresses from the expanded list. |
|
602
|
|
|
|
|
|
|
|
|
603
|
|
|
|
|
|
|
my $unique_addresses = $resolver->remove_duplicate_email_addresses(); |
|
604
|
|
|
|
|
|
|
|
|
605
|
|
|
|
|
|
|
=head2 detect_circular_references |
|
606
|
|
|
|
|
|
|
|
|
607
|
|
|
|
|
|
|
Detects circular references in the alias definitions. |
|
608
|
|
|
|
|
|
|
|
|
609
|
|
|
|
|
|
|
my @circular = $resolver->detect_circular_references($aliases); |
|
610
|
|
|
|
|
|
|
|
|
611
|
|
|
|
|
|
|
Returns an array of strings describing any circular references found, with each string |
|
612
|
|
|
|
|
|
|
showing the path of the circular reference (e.g., "team -> dev-team -> team"). |
|
613
|
|
|
|
|
|
|
|
|
614
|
|
|
|
|
|
|
=head2 check_circular |
|
615
|
|
|
|
|
|
|
|
|
616
|
|
|
|
|
|
|
Internal recursive function to check for circular references. |
|
617
|
|
|
|
|
|
|
|
|
618
|
|
|
|
|
|
|
=head2 process_item |
|
619
|
|
|
|
|
|
|
|
|
620
|
|
|
|
|
|
|
Internal function to process individual items when checking for circular references. |
|
621
|
|
|
|
|
|
|
|
|
622
|
|
|
|
|
|
|
=head1 DEPENDENCIES |
|
623
|
|
|
|
|
|
|
|
|
624
|
|
|
|
|
|
|
=over 4 |
|
625
|
|
|
|
|
|
|
|
|
626
|
|
|
|
|
|
|
=item * Moo |
|
627
|
|
|
|
|
|
|
|
|
628
|
|
|
|
|
|
|
=item * namespace::autoclean |
|
629
|
|
|
|
|
|
|
|
|
630
|
|
|
|
|
|
|
=item * Email::Valid |
|
631
|
|
|
|
|
|
|
|
|
632
|
|
|
|
|
|
|
=item * Scalar::Util |
|
633
|
|
|
|
|
|
|
|
|
634
|
|
|
|
|
|
|
=item * Data::Dumper::Concise |
|
635
|
|
|
|
|
|
|
|
|
636
|
|
|
|
|
|
|
=item * Types::Standard |
|
637
|
|
|
|
|
|
|
|
|
638
|
|
|
|
|
|
|
=back |
|
639
|
|
|
|
|
|
|
|
|
640
|
|
|
|
|
|
|
=head1 AUTHOR |
|
641
|
|
|
|
|
|
|
|
|
642
|
|
|
|
|
|
|
Russ Brewer (RBREW) rbrew@cpan.org |
|
643
|
|
|
|
|
|
|
|
|
644
|
|
|
|
|
|
|
=head1 VERSION |
|
645
|
|
|
|
|
|
|
|
|
646
|
|
|
|
|
|
|
0.01 |
|
647
|
|
|
|
|
|
|
|
|
648
|
|
|
|
|
|
|
=head1 SEE ALSO |
|
649
|
|
|
|
|
|
|
|
|
650
|
|
|
|
|
|
|
=over 4 |
|
651
|
|
|
|
|
|
|
|
|
652
|
|
|
|
|
|
|
=item * Email::Valid->address |
|
653
|
|
|
|
|
|
|
|
|
654
|
|
|
|
|
|
|
=item * Moo |
|
655
|
|
|
|
|
|
|
|
|
656
|
|
|
|
|
|
|
=back |
|
657
|
|
|
|
|
|
|
|
|
658
|
|
|
|
|
|
|
=cut |