Putting 4.2.0 on top of 4.0.17
[usit-rt.git] / sbin / rt-validate-aliases
CommitLineData
dab09ea8
MKG
1#!/usr/bin/perl
2# BEGIN BPS TAGGED BLOCK {{{
3#
4# COPYRIGHT:
5#
403d7b0b 6# This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
dab09ea8
MKG
7# <sales@bestpractical.com>
8#
9# (Except where explicitly superseded by other copyright notices)
10#
11#
12# LICENSE:
13#
14# This work is made available to you under the terms of Version 2 of
15# the GNU General Public License. A copy of that license should have
16# been provided with this software, but in any event can be snarfed
17# from www.gnu.org.
18#
19# This work is distributed in the hope that it will be useful, but
20# WITHOUT ANY WARRANTY; without even the implied warranty of
21# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
22# General Public License for more details.
23#
24# You should have received a copy of the GNU General Public License
25# along with this program; if not, write to the Free Software
26# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
27# 02110-1301 or visit their web page on the internet at
28# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
29#
30#
31# CONTRIBUTION SUBMISSION POLICY:
32#
33# (The following paragraph is not intended to limit the rights granted
34# to you to modify and distribute this software under the terms of
35# the GNU General Public License and is only of importance to you if
36# you choose to contribute your changes and enhancements to the
37# community by submitting them to Best Practical Solutions, LLC.)
38#
39# By intentionally submitting any modifications, corrections or
40# derivatives to this work, or any other work intended for use with
41# Request Tracker, to Best Practical Solutions, LLC, you confirm that
42# you are the copyright holder for those contributions and you grant
43# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
44# royalty-free, perpetual, license to use, copy, create derivative
45# works based on those contributions, and sublicense and distribute
46# those contributions and any derivatives thereof.
47#
48# END BPS TAGGED BLOCK }}}
49use strict;
50use warnings;
51use Text::ParseWords qw//;
52use Getopt::Long;
53
54BEGIN { # BEGIN RT CMD BOILERPLATE
55 require File::Spec;
56 require Cwd;
57 my @libs = ("lib", "local/lib");
58 my $bin_path;
59
60 for my $lib (@libs) {
61 unless ( File::Spec->file_name_is_absolute($lib) ) {
62 $bin_path ||= ( File::Spec->splitpath(Cwd::abs_path(__FILE__)) )[1];
63 $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib );
64 }
65 unshift @INC, $lib;
66 }
af59614d 67
dab09ea8
MKG
68}
69
70require RT;
71RT::LoadConfig();
72RT::Init();
73
74my ($PREFIX, $URL, $HOST) = ("");
75GetOptions(
76 "prefix|p=s" => \$PREFIX,
77 "url|u=s" => \$URL,
78 "host|h=s" => \$HOST,
79);
80
81unless (@ARGV) {
82 @ARGV = grep {-f} ("/etc/aliases",
83 "/etc/mail/aliases",
84 "/etc/postfix/aliases");
85 die "Can't determine aliases file to parse!"
86 unless @ARGV;
87}
88
89my %aliases = parse_lines();
90unless (%aliases) {
91 warn "No mailgate aliases found in @ARGV";
92 exit;
93}
94
95my %seen;
96my $global_mailgate;
97for my $address (sort keys %aliases) {
98 my ($mailgate, $opts, $extra) = @{$aliases{$address}};
99 my %opts = %{$opts};
100
101 next if $opts{url} and $URL and $opts{url} !~ /\Q$URL\E/;
102
103 if ($mailgate !~ /^\|/) {
104 warn "Missing the leading | on alias $address\n";
105 $mailgate = "|$mailgate";
106 }
107 if (($global_mailgate ||= $mailgate) ne $mailgate) {
108 warn "Unexpected mailgate for alias $address -- expected $global_mailgate, got $mailgate\n";
109 }
110
111 if (not defined $opts{action}) {
112 warn "Missing --action parameter for alias $address\n";
113 } elsif ($opts{action} !~ /^(correspond|comment)$/) {
114 warn "Invalid --action parameter for alias $address: $opts{action}\n"
115 }
116
117 my $queue = RT::Queue->new( RT->SystemUser );
118 if (not defined $opts{queue}) {
119 warn "Missing --queue parameter for alias $address\n";
120 } else {
121 $queue->Load( $opts{queue} );
122 if (not $queue->id) {
123 warn "Invalid --queue parameter for alias $address: $opts{queue}\n";
124 } elsif ($queue->Disabled) {
125 warn "Disabled --queue given for alias $address: $opts{queue}\n";
126 }
127 }
128
129 if (not defined $opts{url}) {
130 warn "Missing --url parameter for alias $address\n";
131 } #XXX: Test connectivity and/or https certs?
132
133 if ($queue->id and $opts{action} =~ /^(correspond|comment)$/) {
134 push @{$seen{lc $queue->Name}{$opts{action}}}, $address;
135 }
136
137 warn "Unknown extra arguments for alias $address: @{$extra}\n"
138 if @{$extra};
139}
140
141# Check the global settings
142my %global;
143for my $action (qw/correspond comment/) {
144 my $setting = ucfirst($action) . "Address";
145 my $value = RT->Config->Get($setting);
146 if (not defined $value) {
147 warn "$setting is not set!\n";
148 next;
149 }
150 my ($local,$host) = lc($value) =~ /(.*?)\@(.*)/;
151 next if $HOST and $host !~ /\Q$HOST\E/;
152 $local = "$PREFIX$local" unless exists $aliases{$local};
153
154 $global{$setting} = $local;
155 if (not exists $aliases{$local}) {
156 warn "$setting $value does not exist in aliases!\n"
157 } elsif ($aliases{$local}[1]{action} ne $action) {
158 warn "$setting $value is a $aliases{$local}[1]{action} in aliases!"
159 }
160}
161warn "CorrespondAddress and CommentAddress are the same!\n"
162 if RT->Config->Get("CorrespondAddress") eq RT->Config->Get("CommentAddress");
163
164
165# Go through the queues, one at a time
166my $queues = RT::Queues->new( RT->SystemUser );
167$queues->UnLimit;
168while (my $q = $queues->Next) {
169 my $qname = $q->Name;
170 for my $action (qw/correspond comment/) {
171 my $setting = ucfirst($action) . "Address";
172 my $value = $q->$setting;
173
174 if (not $value) {
175 my @other = grep {$_ ne $global{$setting}} @{$seen{lc $q->Name}{$action} || []};
176 warn "CorrespondAddress not set on $qname, but in aliases as "
177 .join(" and ", @other) . "\n" if @other;
178 next;
179 }
180
181 if ($action eq "comment" and $q->CorrespondAddress
182 and $q->CorrespondAddress eq $q->CommentAddress) {
183 warn "CorrespondAddress and CommentAddress are set the same on $qname\n";
184 next;
185 }
186
187 my ($local, $host) = lc($value) =~ /(.*?)\@(.*)/;
188 next if $HOST and $host !~ /\Q$HOST\E/;
189 $local = "$PREFIX$local" unless exists $aliases{$local};
190
191 my @other = @{$seen{lc $q->Name}{$action} || []};
192 if (not exists $aliases{$local}) {
193 if (@other) {
194 warn "$setting $value on $qname does not exist in aliases -- typo'd as "
195 .join(" or ", @other) . "?\n";
196 } else {
197 warn "$setting $value on $qname does not exist in aliases!\n"
198 }
199 next;
200 }
201
202 my %opt = %{$aliases{$local}[1]};
203 if ($opt{action} ne $action) {
204 warn "$setting address $value on $qname is a $opt{action} in aliases!\n"
205 }
206 if (lc $opt{queue} ne lc $q->Name and $action ne "comment") {
207 warn "$setting address $value on $qname points to queue $opt{queue} in aliases!\n";
208 }
209
210 @other = grep {$_ ne $local} @other;
211 warn "Extra aliases for queue $qname: ".join(",",@other)."\n"
212 if @other;
213 }
214}
215
216
217sub parse_lines {
218 local @ARGV = @ARGV;
219
220 my %aliases;
221 my $line = "";
222 for (<>) {
223 next unless /\S/;
224 next if /^#/;
225 chomp;
226 if (/^\s+/) {
227 $line .= $_;
228 } else {
229 add_line($line, \%aliases);
230 $line = $_;
231 }
232 }
233 add_line($line, \%aliases);
234
235 expand(\%aliases);
236 filter_mailgate(\%aliases);
237
238 return %aliases;
239}
240
241sub expand {
242 my ($data) = @_;
243
244 for (1..100) {
245 my $expanded = 0;
246 for my $address (sort keys %{$data}) {
247 my @new;
248 for my $part (@{$data->{$address}}) {
249 if (m!^[|/]! or not $data->{$part}) {
250 push @new, $part;
251 } else {
252 $expanded++;
253 push @new, @{$data->{$part}};
254 }
255 }
256 $data->{$address} = \@new;
257 }
258 return unless $expanded;
259 }
260 warn "Recursion limit exceeded -- cycle in aliases?\n";
261}
262
263sub filter_mailgate {
264 my ($data) = @_;
265
266 for my $address (sort keys %{$data}) {
267 my @parts = @{delete $data->{$address}};
268
269 my @pipes = grep {m!^\|?.*?/rt-mailgate\b!} @parts;
270 next unless @pipes;
271
272 my $pipe = shift @pipes;
273 warn "More than one rt-mailgate pipe for alias: $address\n"
274 if @pipes;
275
276 my @args = Text::ParseWords::shellwords($pipe);
277
278 # We allow "|/random-other-command /opt/rt4/bin/rt-mailgate ...",
279 # we just need to strip off enough
280 my $index = 0;
281 $index++ while $args[$index] !~ m!/rt-mailgate!;
282 my $mailgate = join(' ', splice(@args,0,$index+1));
283
284 my %opts;
285 local @ARGV = @args;
286 Getopt::Long::Configure( "pass_through" ); # Allow unknown options
287 my $ret = eval {
288 GetOptions( \%opts, "queue=s", "action=s", "url=s",
289 "jar=s", "debug", "extension=s",
290 "timeout=i", "verify-ssl!", "ca-file=s",
291 );
292 1;
293 };
294 warn "Failed to parse options for $address: $@" unless $ret;
295 next unless %opts;
296
297 $data->{lc $address} = [$mailgate, \%opts, [@ARGV]];
298 }
299}
300
301sub add_line {
302 my ($line, $data) = @_;
303 return unless $line =~ /\S/;
304
305 my ($name, $parts) = parse_line($line);
306 return unless defined $name;
307
308 if (defined $data->{$name}) {
309 warn "Duplicate definition for alias $name\n";
310 return;
311 }
312
313 $data->{lc $name} = $parts;
314}
315
316sub parse_line {
317 my $re_name = qr/\S+/;
318 # Intentionally accept pipe-like aliases with a missing | -- we deal with them later
319 my $re_quoted_pipe = qr/"\|?[^\\"]*(?:\\[\\"][^\\"]*)*"/;
320 my $re_nonquoted_pipe = qr/\|[^\s,]+/;
321 my $re_pipe = qr/(?:$re_quoted_pipe|$re_nonquoted_pipe)/;
322 my $re_path = qr!/[^,\s]+!;
323 my $re_address = qr![^|/,\s][^,\s]*!;
324 my $re_value = qr/(?:$re_pipe|$re_path|$re_address)/;
325 my $re_values = qr/(?:$re_value(?:\s*,\s*$re_value)*)/;
326
327 my ($line) = @_;
328 if ($line =~ /^($re_name):\s*($re_values)/) {
329 my ($name, $all_parts) = ($1, $2);
330 my @parts;
331 while ($all_parts =~ s/^(?:\s*,\s*)?($re_value)//) {
332 my $part = $1;
333 if ($part =~ /^"/) {
334 $part =~ s/^"//; $part =~ s/"$//;
335 $part =~ s/\\(.)/$1/g;
336 }
337 push @parts, $part;
338 }
339 return $name, [@parts];
340 } else {
341 warn "Parse failure, line $. of $ARGV: $line\n";
342 return ();
343 }
344}