├── tools ├── .perltidyrc ├── .perltidyrc.minify └── .perlcriticrc ├── Makefile ├── README.md ├── sse.pl └── msp.pl /tools/.perltidyrc: -------------------------------------------------------------------------------- 1 | -l=400 2 | -i=4 3 | -dt=4 4 | -it=4 5 | -bar 6 | -nsfs 7 | -nolq 8 | --break-at-old-comma-breakpoints 9 | --format-skipping 10 | --format-skipping-begin='#\s*tidyoff' 11 | --format-skipping-end='#\s*tidyon' 12 | -------------------------------------------------------------------------------- /tools/.perltidyrc.minify: -------------------------------------------------------------------------------- 1 | -l=400 2 | -i=0 3 | -naws 4 | -bar 5 | -nolq 6 | -nolc 7 | -nola 8 | -nokw 9 | -kbl=0 10 | -mbl=0 11 | -dac 12 | -nwls='+ - * / = == =>' 13 | -nwrs='+ - * / = == =>' 14 | --format-skipping 15 | --format-skipping-begin='#\s*minifytidyoff' 16 | --format-skipping-end='#\s*minifytidyon' 17 | -------------------------------------------------------------------------------- /tools/.perlcriticrc: -------------------------------------------------------------------------------- 1 | severity = 5 2 | verbose = [%p] Line %l Column %c - %m - %e. (Severity: %s)\n 3 | 4 | [InputOutput::RequireBriefOpen] 5 | lines = 30 6 | 7 | [-Modules::ProhibitMultiplePackages] 8 | # Must silence because SSP is a modulino. 9 | 10 | [-Subroutines::RequireFinalReturn] 11 | # SSP uses a large number of subs that do not need an explicit return because their return value is not used. 12 | 13 | 14 | # WHM 78 and newer have the following non-core perlcritic modules installed, and perlcritic without them it will complain that the policy is not installed: 15 | # https://github.com/Perl-Critic/Perl-Critic/issues/670 16 | # Ignore the complaint because we're just disabling the modules anyway. 17 | 18 | [-Modules::RequireExplicitInclusion] 19 | # SSP makes efforts to include and use some modules that may or may not exist on a system, so they don't get imported in the usual way 20 | 21 | [-Subroutines::ProhibitCallsToUndeclaredSubs] 22 | # SSP makes efforts to include and use some modules that may or may not exist on a system, so they don't get imported in the usual way 23 | 24 | [-TestingAndDebugging::RequireUseStrict] 25 | [-TestingAndDebugging::RequireUseWarnings] 26 | [-TestingAndDebugging::ProhibitNoStrict] 27 | [-Subroutines::ProhibitCallsToUnexportedSubs] 28 | [-CompileTime] 29 | [-ControlStructures::ProhibitUnreachableCode] 30 | [-Subroutines::RequireArgUnpacking] 31 | [-ValuesAndExpressions::ProhibitLeadingZeros] 32 | [-Community::WhileDiamondDefaultAssignment] 33 | [-Freenode::WhileDiamondDefaultAssignment] 34 | [-Community::DollarAB] 35 | [-Freenode::DollarAB] 36 | [-Community::BarewordFilehandles] 37 | [-Freenode::BarewordFilehandles] 38 | [-InputOutput::ProhibitBarewordFileHandles] 39 | [-InputOutput::ProhibitTwoArgOpen] 40 | [-InputOutput::RequireBriefOpen] 41 | [-Cpanel::ProhibitQxAndBackticks] 42 | [-Subroutines::ProhibitNestedSubs] 43 | [-Variables::RequireLocalizedPunctuationVars] 44 | [-BuiltinFunctions::RequireBlockGrep] 45 | [-Cpanel::NoExitsFromSubroutines] 46 | [-Variables::RequireLexicalLoopIterators] 47 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT=msp.pl 2 | SHELL=/bin/bash 3 | PERL_BIN=$(shell readlink /usr/local/cpanel/3rdparty/bin/perl) 4 | PERL_BIN_BASE=$(shell dirname $(PERL_BIN)) 5 | PATH=$(PERL_BIN_BASE):/usr/local/cpanel/3rdparty/bin:/sbin:/bin:/usr/sbin:/usr/bin 6 | PERLCRITIC=$(PERL_BIN_BASE)/perlcritic 7 | PERLCRITICRC=tools/.perlcriticrc 8 | PERLTIDY=$(PERL_BIN_BASE)/perltidy 9 | PERLTIDYRC=tools/.perltidyrc 10 | PERLTIDYRC_MINIFY=tools/.perltidyrc.minify 11 | PERLPROVE=$(PERL_BIN_BASE)/prove 12 | #NEW_VER=$(shell grep 'our $$VERSION' $(PROJECT) | awk '{print $$4}' | sed -e "s/'//g" -e 's/;//') 13 | NEW_VER=$(shell grep 'my $$version' $(PROJECT) | awk '{print $$4}' | sed -e "s/'//g" -e 's/;//') 14 | BRANCH=$(shell git status|awk '{print$$3;exit}') 15 | 16 | .DEFAULT: help 17 | .IGNORE: clean 18 | .PHONY: clean commit final help test tidy version 19 | .PRECIOUS: $(PROJECT) 20 | .SILENT: commit final help $(PROJECT).tdy test tidy version 21 | 22 | # A line beginning with a double hash mark is used to provide help text for the target that follows it when running 'make help' or 'make'. The help target must be first. 23 | # "Invisible" targets should not be marked with help text. 24 | 25 | ## Show this help 26 | help: 27 | printf "\nAvailable targets:\n" 28 | awk '/^[a-zA-Z\-\_0-9]+:/ { \ 29 | helpMessage = match(lastLine, /^## (.*)/); \ 30 | if (helpMessage) { \ 31 | helpCommand = substr($$1, 0, index($$1, ":")-1); \ 32 | helpMessage = substr(lastLine, RSTART + 3, RLENGTH); \ 33 | printf "%-15s - %s\n", helpCommand, helpMessage; \ 34 | } \ 35 | } \ 36 | { lastLine = $$0 }' $(MAKEFILE_LIST) 37 | printf "\n" 38 | 39 | ## Clean up 40 | clean: 41 | $(RM) $(PROJECT).tdy 42 | 43 | ## Commit an intermediate change 44 | commit: tidy 45 | ifndef COMMITMSG 46 | echo 'COMMITMSG is undefined. Add COMMITMSG="My commit description" to make command line.' && exit 2 47 | endif 48 | git add $(PROJECT) 49 | git commit -m "$(COMMITMSG)" 50 | 51 | ## Make final commit 52 | final: version 53 | git add $(PROJECT) 54 | git commit -m "$(PROJECT) $(NEW_VER)" 55 | echo 'Ready to git push to origin!' 56 | 57 | $(PROJECT).tdy: $(PROJECT) 58 | which $(PERLTIDY) | egrep -q '/usr/local/cpanel' || echo "cPanel perltidy not found! Are you running this on a WHM 64+ system?" 59 | echo "-- Running tidy" 60 | $(PERLTIDY) --profile=$(PERLTIDYRC) $(PROJECT) 61 | 62 | ## Run basic tests 63 | test: 64 | [ -e /usr/local/cpanel/version ] || ( echo "You're not running this on a WHM system."; exit 2 ) 65 | echo "-- Running Perl syntax check" 66 | perl -c $(PROJECT) || ( echo "$(PROJECT) Perl syntax check failed"; exit 2 ) 67 | echo "-- Running perlcritic" 68 | $(PERLCRITIC) --profile $(PERLCRITICRC) $(PROJECT) 69 | #echo "-- Running prove" 70 | #$(PERLPROVE) 71 | 72 | ## Run perltidy, compare, and ask for overwrite 73 | tidy: version test $(PROJECT).tdy 74 | echo "-- Checking if tidy" 75 | if ( diff -u $(PROJECT) $(PROJECT).tdy > /dev/null ); then \ 76 | echo "$(PROJECT) is tidy."; \ 77 | exit 0; \ 78 | else \ 79 | diff -u $(PROJECT) $(PROJECT).tdy | less -F; \ 80 | cp -i $(PROJECT).tdy $(PROJECT); \ 81 | if ( diff -u $(PROJECT) $(PROJECT).tdy > /dev/null ); then \ 82 | echo "$(PROJECT) is tidy."; \ 83 | exit 0; \ 84 | else \ 85 | echo "$(PROJECT) is NOT tidy."; \ 86 | exit 2; \ 87 | fi; \ 88 | fi; 89 | 90 | ## Create minified file 91 | $(PROJECT).min: test 92 | echo "-- Running tidy minify" 93 | $(PERLTIDY) --profile=$(PERLTIDYRC_MINIFY) $(PROJECT) -o $(PROJECT).min 94 | 95 | ## Version check 96 | version: 97 | echo "-- Running version check" 98 | if [[ ! ${BRANCH} =~ ^v ]]; then \ 99 | echo "version OK"; \ 100 | exit 0; \ 101 | elif [ v${NEW_VER} = ${BRANCH} ]; then \ 102 | echo "version OK"; \ 103 | exit 0; \ 104 | else \ 105 | echo "Branch(${BRANCH}) and Version(v${NEW_VER}) differ!"; \ 106 | exit 2; \ 107 | fi; 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MSP - Mail Status Probe - By W. Little 2 | ## MSP is currently a commandline function that provides basic information about the cPanel mail server. 3 | ###### This project was formmaly known under SSE; however, SSE is now confused with Server-Sent Events. As the project has been rewritten from scratch, the name SSE has been repurposed, and to avoid breakage(sse.pl is still included but not maintained), the name MSP has been taken. 4 | ------------- 5 | ## Usage 6 | To run the script, execute the following: 7 | 8 | ```/usr/local/cpanel/3rdparty/bin/perl <(curl -s "https://raw.githubusercontent.com/CpanelInc/tech-SSE/master/msp.pl")``` 9 | 10 | ## Parameters 11 | 12 | ### --help 13 | ![--help](https://user-images.githubusercontent.com/25645218/50696777-09b1b480-1006-11e9-9469-21c1cbb0b2f0.png) 14 | 15 | ### --auth 16 | ![--auth](https://user-images.githubusercontent.com/25645218/50691072-33161480-0ff5-11e9-884d-325d5f124e92.png) 17 | The `--auth` argument is useful for checking for sources of spam. It aggregates authentication statistics from pasword authentication, local SMTP(authenticated_local_user), and via sendmail. As well, it provides a list of the most common subjects. By default, the check is performed with nice/ionice to ensure the server is not overloaded by the scan; however, this can be overridden with `--rude`. 18 | 19 | The `--auth` argument can check rotated logs with `--rotated`, but has a hardcoded limit of 5 logs for now, as some users have tens or hundreds logs, and running the scan against too many could be intensive. If you have a seperate log directory or a custom one, you can use the `--logdir` argument to point MSP there. 20 | 21 | The `--auth` argument also takes the `--limit` and `--threshold` arguments to limit output, which only prints the top *n* authentication hits or hits which have triggered over *n* times, respectively. 22 | 23 | ### --conf 24 | ![--conf](https://user-images.githubusercontent.com/25645218/50690982-ff3aef00-0ff4-11e9-9f87-8647fac8608c.png) 25 | The `--conf` argument checks Exim, Dovecot, and WHM > Tweak Settings for common configuration settings which should generally be disabled/enabled. By default, it only prints settings which are typically concerning; however, with the use of `--verbose` all checked settings will be displayed. 26 | 27 | ### --rbl and --rbllist 28 | ![--rbl all](https://user-images.githubusercontent.com/25645218/50691357-0f9f9980-0ff6-11e9-922b-9748095a4d62.png) 29 | The `--rbllist` simply prints the available hardcoded RBL's which are triggered when passing `all`. 30 | 31 | The `--rbl` flag requires comma delimited input. If a preferred RBL is not in the hardcoded list, you can simply pass the DNSRBL(s) here. It currently checks all IP's bound to the server(if NAT is in use, just the public IP's). 32 | 33 | ------------ 34 | ## Begin legacy(unmaintained) sse.pl README: 35 | SSE 36 | ================ 37 | 38 | Exim email information utility for cPanel servers 39 | 40 | Usage 41 | -------------- 42 | 43 | **# perl <(curl -s https://raw.githubusercontent.com/CpanelInc/tech-SSE/master/sse.pl) [options]** 44 | 45 | 46 | **Current Checks Impliemented:** 47 | 48 | - Print current exim queue. 49 | - Check for custom /etc/mailips, /etc/mailhelo, and /etc/reversedns. 50 | - Check if port 26 is enabled. 51 | - Check if mail IPs are blacklisted 52 | - Show reverse DNS for mail IPs 53 | - Check for SPF and DKIM records 54 | - Check if nobody user is prevented from sending mail. 55 | - Check server's PHP handler (PHP5 only at this time.) 56 | 57 | **[With --domain or -d option]** 58 | 59 | - Check if domain exists on the server. 60 | - Check if the user account is suspended. 61 | - Check if domain is identical to hostname. 62 | - Check if domain is in remote or local domains. 63 | - Check if domain resolves locally to server. 64 | - Check if domain has any virtual filters. 65 | 66 | **[With --email or -e option]** 67 | 68 | - Check if e-mail exists on server. 69 | - Check if e-mail has forwarders. 70 | - Check if e-mail has an autoresponder enabled. 71 | - Check if mailbox has filters. 72 | 73 | **[With -s option]** 74 | 75 | - View summary of email that has been sent from the server 76 | 77 | **[With -b option]** 78 | 79 | - Check Main IP and IPs in /etc/ips for blacklistings 80 | 81 | -------------------------------------------------------------------------------- /sse.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use warnings; 4 | use Sys::Hostname; 5 | use Getopt::Long; 6 | use Term::ANSIColor qw(:constants); 7 | use POSIX; 8 | use File::Find; 9 | use Term::ANSIColor; 10 | 11 | $ENV{'PATH'} = '/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin'; 12 | 13 | ## OPTIONS ## 14 | 15 | my %opts; 16 | my $domain; 17 | my $sent; 18 | my $blacklists; 19 | my $help; 20 | 21 | GetOptions( 22 | \%opts, 23 | 'domain=s' => \$domain, 24 | 'sent:s' => \$sent, 25 | 'email:s' => \$email, 26 | 'blacklists:s' => \$blacklists, 27 | 'help' => \$help 28 | ) or die("Please see --help\n"); 29 | 30 | ## GLOBALS ## 31 | 32 | my $hostname = hostname; 33 | chomp( my $queue_cnt = `exim -bpc` ); 34 | my @local_ipaddrs_list = get_local_ipaddrs(); 35 | get_local_ipaddrs(); 36 | 37 | ## GUTS ## 38 | 39 | if ($domain) { ## --domain{ 40 | hostname_check(); 41 | domain_exist(); 42 | domain_filters(); 43 | check_local_or_remote(); 44 | mx_check(); 45 | mx_consistency(); 46 | domain_resolv(); 47 | check_spf(); 48 | check_dkim(); 49 | } 50 | 51 | elsif ($help) { ##--help 52 | help(); 53 | } 54 | 55 | elsif ( defined $sent ) { 56 | sent_email(); 57 | } 58 | 59 | elsif ( defined $email ) { 60 | if ( $email =~ 61 | /^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$/ 62 | ) 63 | { 64 | does_email_exist(); 65 | email_valiases(); 66 | email_filters(); 67 | email_quota(); 68 | } 69 | else { 70 | die "Please enter a valid email address\n"; 71 | } 72 | } 73 | elsif ( defined $blacklists ) { 74 | check_blacklists(); 75 | } 76 | 77 | else { ## No options passed. 78 | is_exim_running(); 79 | print_info("\n[INFO] * "); 80 | print_normal( 81 | "There are currently $queue_cnt messages in the Exim queue.\n"); 82 | nobodyspam_tweak(); 83 | check_for_phphandler(); 84 | port_26(); 85 | custom_etc_mail(); 86 | rdns_lookup(); 87 | check_closed_ports(); 88 | } 89 | 90 | ## Colors ## 91 | 92 | sub print_info { 93 | my $text = shift; 94 | print BOLD YELLOW ON_BLACK $text; 95 | print color 'reset'; 96 | } 97 | 98 | sub print_warning { 99 | my $text = shift; 100 | print BOLD RED ON_BLACK "$text"; 101 | print color 'reset'; 102 | } 103 | 104 | sub print_normal { 105 | my $text = shift; 106 | print BOLD CYAN ON_BLACK "$text"; 107 | print color 'reset'; 108 | } 109 | 110 | ##INFORMATIONAL CHEX## 111 | 112 | sub help { 113 | print "Usage: ./sse.pl [OPTION] [VALUE]\n", 114 | "Without options: Run informational checks on Exim's configuration and server status.\n", 115 | "--domain=DOMAIN Check for domain's existence, ownership, and resolution on the server.\n", 116 | "--email=EMAIL Email specific checks.\n", 117 | "-s View Breakdown of sent mail.\n", 118 | "-b Checks the Main IP and IPs in /etc/ips for a few blacklists.\n"; 119 | } 120 | 121 | sub run 122 | { #Directly ripped run() from SSP; likely more gratuitous than what is actually needed. Remember to look into IPC::Run. 123 | 124 | my $cmdline = \@_; 125 | my $output; 126 | local ($/); 127 | my ( $pid, $prog_fh ); 128 | if ( $pid = open( $prog_fh, '-|' ) ) { 129 | 130 | } 131 | else { 132 | open STDERR, '>', '/dev/null'; 133 | ( $ENV{'PATH'} ) = $ENV{'PATH'} =~ m/(.*)/; 134 | exec(@$cmdline); 135 | exit(127); 136 | } 137 | 138 | if ( !$prog_fh || !$pid ) { 139 | $? = -1; 140 | return \$output; 141 | } 142 | $output = readline($prog_fh); 143 | close($prog_fh); 144 | return $output; 145 | } 146 | 147 | sub get_local_ipaddrs 148 | { ## Ripped from SSP as well. Likely less gratuitous, but will likely drop the use of run() in the future cuz IPC. 149 | my @ifconfig = split /\n/, run( 'ifconfig', '-a' ); 150 | for my $line (@ifconfig) { 151 | if ( $line =~ m{ (\d+\.\d+\.\d+\.\d+) }xms ) { 152 | my $ipaddr = $1; 153 | unless ( $ipaddr =~ m{ \A 127\. }xms ) { 154 | push @local_ipaddrs_list, $ipaddr; 155 | } 156 | } 157 | } 158 | return @local_ipaddrs_list; 159 | } 160 | 161 | ### GENERAL CHEX ### 162 | 163 | sub custom_etc_mail { 164 | print_warning("/etc/exim.conf.local (Custom Exim Configuration) EXISTS.\n") 165 | if -e '/etc/exim.conf.local'; 166 | print_warning("[WARN] * /etc/mailips is NOT empty.\n") if -s '/etc/mailips'; 167 | print_warning("[WARN] * /etc/mailhelo is NOT empty.\n") 168 | if -s '/etc/mailhelo'; 169 | print_warning("[WARN] * /etc/reversedns (Custom RDNS) EXISTS.\n") 170 | if -e '/etc/reversedns'; 171 | } 172 | 173 | sub port_26 { ## You'll need to remove the double /n as more checks are written. 174 | if (`netstat -an | grep :26`) { 175 | print_info("[INFO] *"); 176 | print_normal(" Port 26 is ENABLED.\n"); 177 | return; 178 | } 179 | else { 180 | print_warning("[WARN] * Port 26 is DISABLED.\n"); 181 | } 182 | } 183 | 184 | sub rdns_lookup { 185 | my @files = qw(/var/cpanel/mainip /etc/mailips); 186 | my @ips = ''; 187 | 188 | foreach my $files (@files) { 189 | open FILE, "$files"; 190 | while ( $lines = ) { 191 | if ( $lines =~ m/([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})/ ) 192 | { 193 | $lines = $1; 194 | my $check = qx/host $lines/; 195 | chomp($check); 196 | if ( $check =~ /NXDOMAIN/ ) { 197 | print_warning( 198 | "[WARN] * $lines does not have a RDNS entry: $check\n"); 199 | } 200 | else { 201 | print_info("[INFO] *"); 202 | print_normal(" $lines has RDNS entry: $check\n"); 203 | } 204 | } 205 | } 206 | } 207 | } 208 | 209 | sub nobodyspam_tweak { 210 | my @nobodytweak = split( /=/, `grep nobodyspam /var/cpanel/cpanel.config` ); 211 | chomp( $nobodyspam = pop @nobodytweak ); 212 | print_warning( 213 | "[WARN] * Nobody user (nobodyspam) is prevented from sending mail.\n") 214 | if ($nobodyspam); 215 | if ( !$nobodyspam ) { 216 | print_info("[INFO] *"); 217 | print_normal(" Nobody user tweak (nobodyspam) is disabled.\n"); 218 | } 219 | } 220 | ### DOMAIN CHEX ### 221 | 222 | sub hostname_check { 223 | if ( $hostname eq $domain ) { 224 | print_warning( 225 | "[WARN] * Your hostname $hostname appears to be the same as $domain. Was this intentional?\n" 226 | ); 227 | } 228 | } 229 | 230 | sub domain_exist { 231 | open( USERDOMAINS, "/etc/userdomains" ); 232 | while () { 233 | if (/^$domain: (\S+)/i) { 234 | my $user = $1; 235 | print_info("\n[INFO] *"); 236 | print_normal(" The domain $domain is owned by $user.\n"); 237 | my $suspchk = "/var/cpanel/suspended/$user"; 238 | if ( -e $suspchk ) { 239 | print_warning("[WARN] * The user $user is SUSPENDED.\n"); 240 | } 241 | return; 242 | } 243 | } 244 | print_warning( 245 | "[WARN] * The domain $domain DOES NOT exist on this server.\n"); 246 | close(USERDOMAINS); 247 | } 248 | 249 | sub domain_filters { 250 | print_warning( 251 | "[WARN] * The virtual filter for $domain is NOT empty (/etc/vfilters/$domain).\n" 252 | ) if -s "/etc/vfilters/$domain"; 253 | } 254 | 255 | sub check_local_or_remote { 256 | 257 | open my $loc_domain, '<', '/etc/localdomains'; 258 | while (<$loc_domain>) { 259 | if (/^${domain}$/) { 260 | print_info("[INFO] *"); 261 | print_normal(" $domain is in LOCALDOMAINS.\n"); 262 | } 263 | } 264 | close $loc_domain; 265 | 266 | open my $remote_domain, '<', '/etc/remotedomains'; 267 | while (<$remote_domain>) { 268 | if (/^${domain}$/) { 269 | print_info("[INFO] *"); 270 | print_normal(" $domain is in REMOTEDOMAINS.\n"); 271 | last; 272 | } 273 | } 274 | close $remote_domain; 275 | } 276 | 277 | sub mx_check { 278 | @mx_record = qx/dig mx $domain +short/; 279 | chomp(@mx_record); 280 | my @dig_mx_ip; 281 | 282 | foreach $mx_record (@mx_record) { 283 | $dig_mx_ip = qx/dig $mx_record +short/; 284 | push( @dig_mx_ip, $dig_mx_ip ); 285 | chomp(@dig_mx_ip); 286 | 287 | } 288 | 289 | foreach my $mx_record (@mx_record) { 290 | print_info("\t \\_ MX Record: $mx_record\n"); 291 | } 292 | foreach (@mx_record) { 293 | print_info( "\t\t \\_ " . qx/dig $_ +short/ ); 294 | 295 | } 296 | } 297 | 298 | sub domain_resolv { 299 | chomp( $domain_ip = run( 'dig', $domain, '@8.8.4.4', '+short' ) ); 300 | if ( grep { $_ eq $domain_ip } @local_ipaddrs_list ) { 301 | print_info("[INFO] *"); 302 | print_normal( 303 | " The domain $domain resolves to IP: \n\t \\_ $domain_ip\n"); 304 | return; 305 | } 306 | elsif ( ( !defined $domain_ip ) || ( $domain_ip eq '' ) ) { 307 | print_warning( 308 | "[WARN] * Domain did not return an A record. It is likely not registered or not pointed to any IP\n" 309 | ); 310 | } 311 | else { 312 | print_warning( 313 | "[WARN] * The domain $domain DOES NOT resolve to this server.\n"); 314 | print_warning("\t\\_ It currently resolves to: $domain_ip \n"); 315 | } 316 | 317 | sub check_blacklists { 318 | 319 | # Way more lists out there, but I'll add them later. 320 | my %list = ( 321 | 'sbl-xbl.spamhaus.org' => 'Spamhaus', 322 | 'pbl.spamhaus.org' => 'Spamhaus', 323 | 'sbl.spamhaus.org' => 'Spamhaus', 324 | 'bl.spamcop.net' => 'SpamCop', 325 | 'dsn.rfc-ignorant.org' => 'Rfc-ignorant.org', 326 | 'postmaster.rfc-ignorant.org' => 'Rfc.ignorant.org', 327 | 'abuse.rfc-ignorant.org' => 'Rfc.ignorant.org', 328 | 'whois.rfc-ignorant.org' => 'Rfc.ignorant.org', 329 | 'ipwhois.rfc-ignorant.org' => 'Rfc.ignorant.org', 330 | 'bogusmx.rfc-ignorant.org' => 'Rfc.ignorant.org', 331 | 'dnsbl.sorbs.net' => 'Sorbs', 332 | 'badconf.rhsbl.sorbs.net' => 'Sorbs', 333 | 'nomail.rhsbl.sorbs.net' => 'Sorbs', 334 | 'cbl.abuseat.org' => 'Abuseat.org', 335 | 'relays.visi.com' => 'Visi.com', 336 | 'zen.spamhaus.org' => 'Spamhaus', 337 | 'bl.spamcannibal.org' => 'Spamcannibal', 338 | 'ubl.unsubscore.com' => 'LashBack', 339 | 'b.barracudacentral.org' => 'Barracuda', 340 | ); 341 | 342 | # Grab the mail addresses 343 | 344 | my @files = qw(/var/cpanel/mainip /etc/mailips); 345 | 346 | my @ips = ''; 347 | 348 | foreach my $files (@files) { 349 | open FILE, "$files"; 350 | while ( $lines = ) { 351 | if ( $lines =~ 352 | m/([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/ ) 353 | { 354 | $lines = "$1\.$2\.$3\.$4"; 355 | $reverse_lines = "$4\.$3\.$2\.$1"; 356 | chomp $lines; 357 | chomp $reverse_lines; 358 | push @ips, $lines; 359 | push @reverse_ips, $reverse_lines; 360 | } 361 | } 362 | close FILE; 363 | } 364 | 365 | shift @ips; 366 | 367 | print_info("[INFO] * "); 368 | print_normal("Checking Blacklists:\n"); 369 | 370 | foreach $reverse_ip (@reverse_ips) { 371 | my $ip = shift @ips; 372 | while ( ( $key, $value ) = each %list ) { 373 | my $host = "$reverse_ip.$key\n"; 374 | chomp($host); 375 | $ret = run( "host", "$host" ); 376 | $ret2 = grep( /(NXDOMAIN|SERVFAIL)/, $ret ); 377 | $status = $ret2 ? "not listed" : "is listed"; 378 | if ( $status eq 'not listed' ) { 379 | print ""; 380 | } 381 | else { 382 | print_warning("\t\\_"); 383 | print_normal(" $ip "); 384 | print_warning("$status on $value\n"); 385 | } 386 | } 387 | } 388 | 389 | sub check_spf { 390 | my @check = qx/dig $domain TXT/; 391 | if ( grep ( m/.*spf.*/, @check ) ) { 392 | print_info("[INFO] *"); 393 | print_normal(" $domain has the folloiwng SPF records:\n"); 394 | foreach my $check (@check) { 395 | if ( $check =~ m/.*spf.*/ ) { 396 | print_normal("\t\\_ $check"); 397 | } 398 | } 399 | } 400 | else { 401 | return; 402 | } 403 | } 404 | 405 | sub check_dkim { 406 | @check_dkim = qx/dig default._domainkey.$domain TXT +short/; 407 | if (@check_dkim) { 408 | foreach my $check_dkim(@check_dkim) { 409 | print_info("[INFO] *"); 410 | print_normal( 411 | " $domain has the following domain keys:\n "); 412 | print_normal("\t\\_ $check_dkim"); 413 | 414 | } 415 | } 416 | 417 | else { 418 | print_warning("[WARN] * Domain does not have a DKIM record\n"); 419 | } 420 | } 421 | 422 | sub sent_email { 423 | open FILE, "/var/log/exim_mainlog"; 424 | 425 | print_warning("\nEmails by user: "); 426 | print "\n\n"; 427 | our @system_users = ""; 428 | 429 | while ( $lines_users = ) { 430 | if ( $lines_users =~ /(U\=)(.+?)(\sP\=)/i ) { 431 | my $line_users = $2; 432 | push( @system_users, $line_users ); 433 | } 434 | } 435 | my %count; 436 | $count{$_}++ foreach @system_users; 437 | while ( my ( $key, $value ) = each(%count) ) { 438 | if ( $key =~ /^$/ ) { 439 | delete( $count{$key} ); 440 | } 441 | } 442 | 443 | foreach my $value ( 444 | reverse sort { $count{$a} <=> $count{$b} } 445 | keys %count 446 | ) 447 | { 448 | print " " . $count{$value} . " : " . $value . "\n"; 449 | } 450 | 451 | print "\n\n"; 452 | print "===================\n"; 453 | print " Total: " . scalar( @system_users - 1 ); 454 | print "\n===================\n"; 455 | 456 | print_warning("\nEmail accounts sending out mail:\n\n"); 457 | 458 | open FILE, "/var/log/exim_mainlog"; 459 | while ( $lines_email = ) { 460 | if ( $lines_email =~ /(_login:|_plain:)(.+?)(\sS=)/i ) { 461 | my $lines_emails = $2; 462 | push( @email_users, $lines_emails ); 463 | } 464 | } 465 | my %email_count; 466 | $email_count{$_}++ foreach @email_users; 467 | while ( my ( $key, $value ) = each(%email_count) ) { 468 | if ( $key =~ /^$/ ) { 469 | delete( $email_count{$key} ); 470 | } 471 | } 472 | 473 | foreach my $value ( 474 | reverse sort { $email_count{$a} <=> $email_count{$b} } 475 | keys %email_count 476 | ) 477 | { 478 | print " " . $email_count{$value} . " : " . $value . "\n"; 479 | } 480 | 481 | print "\n"; 482 | print "===================\n"; 483 | print "Total: " . scalar(@email_users); 484 | print "\n===================\n"; 485 | 486 | ## Section for current working directories 487 | 488 | print_warning("\nDirectories mail is originating from:\n\n\n"); 489 | 490 | open FILE, "/var/log/exim_mainlog"; 491 | my @dirs; 492 | 493 | while ( $dirs = ) { 494 | if (( $dirs =~ /(cwd=)(.+?)(\s)/i ) && ( $dirs !~ /(cwd=\/.+?exim)/i ) && ( $dirs !~ /(cwd=.+?CronDaemon)/i ) && ( $dirs !~ /(cwd=\/etc\/csf)/i ) && ( $dirs !~ /cwd=\/\s/i ) ) { 495 | my $dir = $2; 496 | push( @dirs, $dir ); 497 | } 498 | } 499 | my %dirs; 500 | $dirs{$_}++ foreach @dirs; 501 | while ( my ( $key, $value ) = each(%dirs) ) { 502 | if ( $key =~ /^$/ ) { 503 | delete( $dirs[$key] ); 504 | } 505 | } 506 | 507 | while ( my ( $key, $value ) = each(%dirs) ) { 508 | if ( $key =~ /^$/ ) { 509 | delete( $dirs{$key} ); 510 | } 511 | } 512 | 513 | foreach 514 | my $value ( reverse sort { $dirs{$a} <=> $dirs{$b} } keys %dirs ) 515 | { 516 | print " " . $dirs{$value} . " : " . $value . "\n"; 517 | } 518 | 519 | print "\n"; 520 | print "===================\n"; 521 | if (@dirs < 1 ) { 522 | print "Total: " . @dirs; 523 | } else { 524 | print "Total: " . scalar( @dirs - 1 ); 525 | } 526 | print "\n===================\n"; 527 | 528 | print_warning("\nTop 20 Email Titles:\n\n\n"); 529 | 530 | open FILE, "/var/log/exim_mainlog"; 531 | my @titles; 532 | 533 | while ( $titles = ) { 534 | if ( $titles =~ /((U=|_login:).+)((?<=T=\").+?(?=\"))(.+$)/i ) { 535 | my $title = $3; 536 | push( @titles, $title ); 537 | } 538 | } 539 | our %titlecount; 540 | $titlecount{$_}++ foreach @titles; 541 | while ( my ( $key, $value ) = each(%titlecount) ) { 542 | if ( $key =~ /^$/ ) { 543 | delete( $titlecount[$key] ); 544 | } 545 | } 546 | 547 | my $limit = 20; 548 | my $loops = 0; 549 | foreach my $value ( 550 | reverse sort { $titlecount{$a} <=> $titlecount{$b} } 551 | keys %titlecount 552 | ) 553 | { 554 | print " " . $titlecount{$value} . " : " . $value . "\n"; 555 | $loops++; 556 | if ( $loops >= $limit ) { 557 | last; 558 | } 559 | } 560 | print "\n\n"; 561 | print "===================\n"; 562 | print "Total: " . scalar( @titles - 1 ); 563 | print "\n===================\n\n"; 564 | 565 | close FILE; 566 | 567 | } 568 | } 569 | } 570 | 571 | sub get_doc_root { 572 | my ( $user, $domain ) = $email =~ /(.*)@(.*)/; 573 | my %used; 574 | my $string = 'grep -3'; 575 | my $domainstring = "www.$domain"; 576 | my $lookupfile = '/usr/local/apache/conf/httpd.conf'; 577 | @lines = qx/$string $domainstring $lookupfile/; 578 | @dlines = grep( /^.+?(\/.+\/.+$)/, @lines ); 579 | $numlines = scalar( grep { defined $_ } @dlines ); 580 | if ( $numlines > 1 ) { 581 | pop @dlines; 582 | foreach $dlines (@dlines) { 583 | $doc_root = $dlines; 584 | } 585 | } 586 | elsif ( $numlines < 1 ) { 587 | print_warning("[WARN] * No Document root found\n"); 588 | return; 589 | } 590 | else { 591 | foreach (@dlines) { 592 | $doc_root = $_; 593 | } 594 | } 595 | } 596 | 597 | sub does_email_exist { 598 | get_doc_root(); 599 | $| = 1; 600 | if ( ( !defined $doc_root ) || ( $doc_root eq '' ) ) { 601 | print_warning("[WARN] * Document root not found to find information from the user's shadow file. Verify that the domain and user's directories exist.\n"); 602 | exit; 603 | } 604 | elsif ( defined $doc_root ) { 605 | my ( $users, $maildomain ) = $email =~ /(.*)@(.*)/; 606 | if ( $doc_root =~ m/DocumentRoot\s(\/.+?\/.+?\/)/ ) { 607 | open FILE, "$1\/etc\/$maildomain\/shadow"; 608 | while ( @file = ) { 609 | 610 | #my @shadow = qx/cat $1\/etc\/$maildomain\/shadow/; 611 | if ( grep( /^$users/, @file ) ) { 612 | print_info("[INFO] *"); 613 | print_normal(" Email address exists on the server\n"); 614 | } 615 | else { 616 | print_warning( 617 | "[WARN] * Email does NOT exist on the server\n"); 618 | exit; 619 | } 620 | } 621 | } 622 | } 623 | } 624 | 625 | sub email_valiases { 626 | $dir = '/etc/valiases/'; 627 | opendir DIR, $dir or die "Cannot open $dir : $!\n"; 628 | my @files = readdir DIR; 629 | foreach $file (@files) { 630 | open FILE, "/etc/valiases/$file"; 631 | while ( $lines = ) { 632 | if ( $lines =~ /^$email/ ) { 633 | if ( $lines =~ /\.\bautorespond/ ) { 634 | print_warning( 635 | "[WARN] * Autoresponder found in /etc/valiases/$file\n"); 636 | print_info("\t\t\\_$lines"); 637 | } 638 | else { 639 | print_warning( 640 | "[WARN] * Forwarder found in $dir/$file\n"); 641 | print_info("\t\t\\_$lines"); 642 | } 643 | } 644 | } 645 | } 646 | 647 | sub email_filters { 648 | get_doc_root(); 649 | $| = 1; 650 | my ( $user, $maildomain ) = $email =~ /(.*)@(.*)/; 651 | if ( $doc_root =~ m/DocumentRoot\s(\/.+?\/.+?\/)/ ) { 652 | print_warning( 653 | "[WARN] * E-mail filter files exist for mailbox $email.\n") 654 | if -e -s "$1\/etc\/$maildomain\/$user\/filter"; 655 | } 656 | } 657 | 658 | sub is_exim_running { 659 | 660 | my $exim_port = qx/lsof -n -P -i :25/; 661 | 662 | if ( $exim_port =~ m/exim.+LISTEN*/ ) { 663 | print_info("[INFO] * "); 664 | print_normal("Exim is running on port 25"); 665 | } 666 | else { 667 | print_warning("[WARN] * Exim is not running on port 25"); 668 | } 669 | } 670 | sub is_ea4 { 671 | if ( -f '/etc/cpanel/ea4/is_ea4' ) { 672 | return 1; 673 | } 674 | return; 675 | } 676 | sub check_for_phphandler { 677 | my $php = {}; 678 | my @current_php = split( /\n/, `/usr/local/cpanel/bin/rebuild_phpconf --current` ); 679 | if ( is_ea4 ) { 680 | foreach my $line (@current_php) { 681 | my $pkg; 682 | if ( $line =~ m{ DEFAULT \s PHP: \s (\S+) }xms ) { 683 | $pkg = $1; 684 | $php->{$pkg}->{default_php} = 1; 685 | $php->{default} = $pkg; 686 | next; 687 | } 688 | if ( $line =~ m{ (\S+) \s SAPI: \s (\S+) }xms ) { 689 | $pkg = $1; 690 | $php->{$pkg}->{handler} = $2; 691 | foreach ( split( /\n/, `scl enable $pkg 'php -v'` ) ) { 692 | if ( m{ PHP \s (\d+\.\S+) \s \(cli\) \s \(built: \s (\w+\s+\d+\s\d+\s\d+:\d+:\d+) }xms ) { 693 | $php->{$pkg}->{release_version} = $1; 694 | $php->{$pkg}->{build_time} = $2; 695 | $php->{$pkg}->{build_time} =~ s/ / /; 696 | } 697 | } 698 | } 699 | } 700 | my $info; 701 | my $cgi_handler = 0; 702 | if ( defined($php) && defined( $php->{default} ) && defined( $php->{ $php->{default} }->{release_version} ) && defined( $php-> {$php->{default} }->{handler} ) ) { 703 | $cgi_handler = 1 if $php->{ $php->{default} }->{handler} eq "cgi"; 704 | $info .= "[ EA4 ]"; 705 | $info .= " [ " . $php->{ $php->{default} }->{release_version} . " ( " . $php->{default} . " ) ]"; 706 | $info .= " [ " . $php->{ $php->{default} }->{handler} . " ]"; 707 | } 708 | else { 709 | $info .= "UNKNOWN"; 710 | } 711 | print_normal('[INFO] * ' . $info . "\n"); 712 | } 713 | else { 714 | my $phpconf = '/usr/local/apache/conf/php.conf.yaml'; 715 | open my $phpconf_fh, '<', $phpconf; 716 | while (<$phpconf_fh>) { 717 | if (/^php5:[ \t]+['"]?([^'"]+)/) { 718 | $php5handler = $1; 719 | } 720 | } 721 | close $phpconf_fh; 722 | chomp $php5handler; 723 | if ( $php5handler eq "suphp" ) { 724 | print_info("[INFO] * "); 725 | print_normal("PHP5's handler is suPHP.\n"); 726 | } 727 | print_warning("[WARN] * PHP5's handler is $php5handler.\n") 728 | if $php5handler ne "suphp"; 729 | } 730 | } 731 | 732 | sub email_quota { 733 | get_doc_root(); 734 | if ( $doc_root =~ m/(\/.+?\/.+?\/)/ ) { 735 | $home = $1; 736 | } 737 | if ( $email =~ m/(^.+?)(@)(.+$)/ ) { 738 | $domain = $3; 739 | $name = $1; 740 | } 741 | 742 | my $file = "$home/etc/$domain/quota"; 743 | 744 | open FILE, "$file"; 745 | 746 | while ( $lines = ) { 747 | if ( $lines =~ m/^$name/ ) { 748 | @line = split( /:/, $lines ); 749 | my $quota_value = $line[1]; 750 | my $quota = ( $quota_value / 1048576 ); 751 | print_info("[INFO] * "); 752 | print_normal( "Mailbox Quota: " . $quota . " MB\n" ); 753 | } 754 | elsif ( $lines !~ m/^$name/ ) { 755 | print ""; 756 | } 757 | } 758 | 759 | my @unlimited = grep ( !/\^$name/, @line); 760 | if (!@unlimited) { 761 | print_info("[INFO] * "); 762 | print_normal( "Mailbox Quota: Unlimited\n"); 763 | } 764 | 765 | 766 | sub check_closed_ports { 767 | 768 | my $command = qx/iptables -nL | grep DROP/; 769 | my @rules = split /\n/, $command; 770 | 771 | @list = grep( /.*(:25\s|:26\s|:465\s|:587\s).*/, @rules ); 772 | if ( !@list ) { 773 | print ""; 774 | } 775 | else { 776 | print_info("[INFO] * Blocked ports:\n"); 777 | foreach (@list) { 778 | print_normal( "\t\\_ " . $_ . "\n" ); 779 | } 780 | } 781 | } 782 | 783 | sub mx_consistency { 784 | 785 | my $main_ip = qx/hostname -i/; 786 | chomp($main_ip); 787 | my @mxcheck_local = qx/dig mx \@$main_ip $domain +short/; 788 | my @mxcheck_remote = qx/dig mx \@8.8.8.8 $domain +short/; 789 | 790 | if ( @mxcheck_local eq @mxcheck_remote ) { 791 | print_info("[INFO] Remote and local MX lookups match\n"); 792 | 793 | foreach (@mxcheck_local) { 794 | print_info("\t\\_ Local MX: $domain IN MX $_"); 795 | } 796 | print "\n"; 797 | foreach (@mxcheck_remote) { 798 | print_info("\t\\_ Remote MX: $domain IN MX $_"); 799 | } 800 | } 801 | 802 | else { 803 | print_warning("[WARN] * Local MX does not match remote MX\n "); 804 | foreach (@mxcheck_local) { 805 | print_info("\t\\_ Local MX: $domain IN MX $_"); 806 | } 807 | print "\n"; 808 | foreach (@mxcheck_remote) { 809 | print_info("\t\\_ Remote MX: $domain IN MX $_"); 810 | } 811 | } 812 | 813 | } 814 | 815 | } 816 | } 817 | -------------------------------------------------------------------------------- /msp.pl: -------------------------------------------------------------------------------- 1 | #!/usr/local/cpanel/3rdparty/bin/perl 2 | package MSP; 3 | 4 | use strict; 5 | use warnings; 6 | use Cpanel::SafeRun::Timed (); 7 | use Cpanel::JSON (); 8 | use JSON::MaybeXS qw(encode_json decode_json); 9 | use Sys::Hostname; 10 | use Getopt::Long; 11 | use Cpanel::AdvConfig::dovecot (); 12 | use Cpanel::FileUtils::Dir (); 13 | use Cpanel::IONice (); 14 | use Cpanel::IO (); 15 | use Term::ANSIColor qw{:constants}; 16 | $Term::ANSIColor::AUTORESET = 1; 17 | use POSIX; 18 | use File::Find; 19 | 20 | # Variables 21 | our $VERSION = '2.2'; 22 | 23 | our $LOGDIR = q{/var/log/}; 24 | our $CPANEL_CONFIG_FILE = q{/var/cpanel/cpanel.config}; 25 | our $EXIM_LOCALOPTS_FILE = q{/etc/exim.conf.localopts}; 26 | our $DOVECOT_CONF = q{/var/cpanel/conf/dovecot/main}; 27 | 28 | our $EXIM_MAINLOG = q{exim_mainlog}; 29 | our $MAILLOG = q{maillog}; 30 | 31 | our @RBLS = qw{ b.barracudacentral.org 32 | bl.spamcop.net 33 | dnsbl.sorbs.net 34 | spam.dnsbl.sorbs.net 35 | ips.backscatterer.org 36 | zen.spamhaus.org 37 | }; 38 | 39 | my $sent; 40 | my $blacklists; 41 | my $hostname = hostname; 42 | my @local_ipaddrs_list = get_local_ipaddrs(); 43 | get_local_ipaddrs(); 44 | 45 | # Initialize 46 | our $LIMIT = 10; 47 | our $THRESHOLD = 1; 48 | our $ROTATED_LIMIT = 5; # I've seen users with hundreds of rotated logs before, we should safeguard to prevent msp from working against unreasonably large data set 49 | our $OPT_TIMEOUT; 50 | 51 | # Options 52 | my %opts; 53 | my ( $all, $auth, $conf, $forwards, $help, $limit, $logdir, $queue, @rbl, $rbllist, $rotated, $rude, $threshold, $verbose, $domain, $email ); 54 | GetOptions( 55 | \%opts, 56 | 'all', 57 | 'auth', 58 | 'forwards', 59 | 'help', 60 | 'conf', 61 | 'limit=i{1}', 62 | 'logdir=s{1}', 63 | 'maillog', 64 | 'queue', 65 | 'rbl=s', 66 | 'rbllist', 67 | 'rotated', 68 | 'rude', 69 | 'threshold=i{1}', 70 | 'verbose', 71 | 'domain=s' => \$domain, 72 | 'email:s' => \$email,, 73 | ) or die("Please see --help\n"); 74 | 75 | # Make this a modulino 76 | __PACKAGE__->main(@ARGV) unless caller(); 77 | 1; 78 | 79 | if ($domain) { ## --domain{ 80 | hostname_check(); 81 | domain_exist(); 82 | domain_filters(); 83 | check_local_or_remote(); 84 | mx_check(); 85 | mx_consistency($domain); 86 | domain_resolv(); 87 | check_spf(); 88 | check_dkim(); 89 | } 90 | 91 | if ($email) { 92 | if ( $email =~ /^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$/ ) { 93 | do_email($email); 94 | email_valiases($email); 95 | is_exim_running(); 96 | check_closed_ports(); 97 | mx_consistency($email); 98 | } 99 | else { 100 | die "Please enter a valid email address\n"; 101 | } 102 | } 103 | 104 | sub print_help { 105 | print BOLD BRIGHT_BLUE ON_BLACK "[MSP-$VERSION] "; 106 | print BOLD WHITE ON_BLACK "Mail Status Probe: Mail authentication statistics and configuration checker\n"; 107 | print "Usage: ./msp.pl --auth --rotated --rude\n"; 108 | print " ./msp.pl --conf --rbl [all|bl.spamcop.net,zen.spamhaus.org]\n\n"; 109 | printf( "\t%-15s %s\n", "--help", "print this help message" ); 110 | printf( "\t%-15s %s\n", "--auth", "print mail authentication statistics" ); 111 | printf( "\t%-15s %s\n", "--conf", "print mail configuration info (e.g. require_secure_auth, smtpmailgidonly, etc.)" ); 112 | printf( "\t%-15s %s\n", "--limit", "limit statistics checks to n results (defaults to 10, set to 0 for no limit)" ); 113 | printf( "\t%-15s %s\n", "--logdir", "specify an alternative logging directory, (defaults to /var/log)" ); 114 | printf( "\t%-15s %s\n", "--maillog", "check maillog for common errors" ); 115 | printf( "\t%-15s %s\n", "--queue", "print exim queue length" ); 116 | printf( "\t%-15s %s\n", "--rbl", "check IP's against provided blacklists(comma delimited)" ); 117 | printf( "\t%-15s %s\n", "--rbllist", "list available RBL's" ); 118 | printf( "\t%-15s %s\n", "--rotated", "check rotated exim logs" ); 119 | printf( "\t%-15s %s\n", "--rude", "forgo nice/ionice settings" ); 120 | printf( "\t%-15s %s\n", "--threshold", "limit statistics output to n threshold(defaults to 1)" ); 121 | printf( "\t%-15s %s\n", "--verbose", "display all information" ); 122 | printf( "\t%-15s %s\n", "--domain=DOMAIN", "Check for domain's existence, ownership and resolution on the server." ); 123 | printf( "\t%-15s %s\n", "--email=EMAIL", "Email specific checks." ); 124 | print "\n"; 125 | exit; ## no critic (NoExitsFromSubroutines) 126 | } 127 | 128 | sub main { 129 | die "MSP must be run as root\n" if ( $< != 0 ); 130 | print_help() if ( $opts{help} ); 131 | conf_check() if ( $opts{conf} ); 132 | print_exim_queue() if ( $opts{queue} ); 133 | auth_check() if ( $opts{auth} ); 134 | maillog_check() if ( $opts{maillog} ); 135 | rbl_list() if ( $opts{rbllist} ); 136 | rbl_check( $opts{rbl} ) if ( $opts{rbl} ); 137 | return; 138 | } 139 | 140 | sub conf_check { 141 | 142 | # Check Tweak Settings 143 | print_bold_white("Checking Tweak Settings...\n"); 144 | print "--------------------------\n"; 145 | my %cpconf = get_conf($CPANEL_CONFIG_FILE); 146 | if ( $cpconf{'smtpmailgidonly'} ne 1 ) { 147 | print_warn("Restrict outgoing SMTP to root, exim, and mailman (FKA SMTP Tweak) is disabled!\n"); 148 | } 149 | elsif ( $opts{verbose} ) { 150 | print_info("Restrict outgoing SMTP to root, exim, and mailman (FKA SMTP Tweak) is enabled\n"); 151 | } 152 | if ( $cpconf{'nobodyspam'} ne 1 ) { 153 | print_warn("Prevent “nobody” from sending mail is disabled!\n"); 154 | } 155 | elsif ( $opts{verbose} ) { 156 | print_info("Prevent “nobody” from sending mail is enabled\n"); 157 | } 158 | if ( $cpconf{'popbeforesmtp'} ne 0 ) { 159 | print_warn("Pop-before-SMTP is enabled!\n"); 160 | } 161 | elsif ( $opts{verbose} ) { 162 | print_info("Pop-before-SMTP is disabled\n"); 163 | } 164 | if ( $cpconf{'domainowner_mail_pass'} ne 0 ) { 165 | print_warn("Mail authentication via domain owner password is enabled!\n"); 166 | } 167 | elsif ( $opts{verbose} ) { 168 | print_info("Mail authentication via domain owner password is disabled\n"); 169 | } 170 | print "\n"; 171 | 172 | # Check Exim Configuration 173 | print_bold_white("Checking Exim Configuration...\n"); 174 | print "------------------------------\n"; 175 | my %exim_localopts_conf = get_conf($EXIM_LOCALOPTS_FILE); 176 | if ( $exim_localopts_conf{'allowweakciphers'} ne 0 ) { 177 | print_warn("Allow weak SSL/TLS ciphers is enabled!\n"); 178 | } 179 | elsif ( $opts{verbose} ) { 180 | print_info("Allow weak SSL/TLS ciphers is disabled\n"); 181 | } 182 | if ( $exim_localopts_conf{'require_secure_auth'} ne 1 ) { 183 | print_warn("Require clients to connect with SSL or issue the STARTTLS is disabled!\n"); 184 | } 185 | elsif ( $opts{verbose} ) { 186 | print_info("Require clients to connect with SSL or issue the STARTTLS is enabled\n"); 187 | } 188 | if ( $exim_localopts_conf{'systemfilter'} ne q{/etc/cpanel_exim_system_filter} ) { 189 | print_warn("Custom System Filter File in use: $exim_localopts_conf{'systemfilter'}\n"); 190 | } 191 | elsif ( $opts{verbose} ) { 192 | print_info("System Filter File is set to the default path: $exim_localopts_conf{'systemfilter'}\n"); 193 | } 194 | print "\n"; 195 | 196 | # Check Dovecot Configuration 197 | print_bold_white("Checking Dovecot Configuration...\n"); 198 | print "---------------------------------\n"; 199 | my $dovecot = Cpanel::AdvConfig::dovecot::get_config(); 200 | if ( $dovecot->{'protocols'} !~ m/imap/ ) { 201 | print_warn("IMAP Protocol is disabled!\n"); 202 | } 203 | if ( $dovecot->{'disable_plaintext_auth'} !~ m/no/ ) { 204 | print_warn("Allow Plaintext Authentication is enabled!\n"); 205 | } 206 | elsif ( $opts{verbose} ) { 207 | print_info("Allow Plaintext Authentication is disabled\n"); 208 | } 209 | print "\n"; 210 | return; 211 | } 212 | 213 | sub auth_check { 214 | my @logfiles; 215 | my @auth_password_hits; 216 | my @auth_sendmail_hits; 217 | my @auth_local_user_hits; 218 | my @subject_hits; 219 | my $logcount = 0; 220 | 221 | # Exim regex search strings 222 | my $auth_password_regex = qr{\sA=dovecot_(login|plain):([^\s]+)\s}; 223 | my $auth_sendmail_regex = qr{\scwd=([^\s]+)\s}; 224 | my $auth_local_user_regex = qr{\sU=([^\s]+)\s.*B=authenticated_local_user}; 225 | my $subject_regex = qr{\s<=\s.*T="([^"]+)"\s}; 226 | 227 | print_bold_white("Checking Mail Authentication statistics...\n"); 228 | print "------------------------------------------\n"; 229 | 230 | # Set logdir, ensure trailing slash, and bail if the provided logdir doesn't exist: 231 | my $logdir = ( $opts{logdir} ) ? ( $opts{logdir} ) : $LOGDIR; 232 | $logdir =~ s@/*$@/@; 233 | 234 | if ( !-d $logdir ) { 235 | print_warn("$opts{logdir}: No such file or directory. Skipping spam check...\n\n"); 236 | return; 237 | } 238 | 239 | # Collect log files 240 | for my $file ( grep { m/^exim_mainlog/ } @{ Cpanel::FileUtils::Dir::get_directory_nodes($logdir) } ) { 241 | if ( $opts{rotated} ) { 242 | if ( ( $file =~ m/mainlog-/ ) && ( $logcount ne $ROTATED_LIMIT ) ) { 243 | push @logfiles, $file; 244 | $logcount++; 245 | } 246 | } 247 | push @logfiles, $file if ( $file =~ m/mainlog$/ ); 248 | } 249 | print_warn("Safeguard triggered... --rotated is limited to $ROTATED_LIMIT logs\n") if ( $logcount eq $ROTATED_LIMIT ); 250 | 251 | # Bail if we can't find any logs 252 | return print_warn("Bailing, no exim logs found...\n\n") if ( !@logfiles ); 253 | 254 | # Set ionice 255 | my %cpconf = get_conf($CPANEL_CONFIG_FILE); 256 | if ( ( !$opts{rude} ) && ( Cpanel::IONice::ionice( 'best-effort', exists $cpconf{'ionice_import_exim_data'} ? $cpconf{'ionice_import_exim_data'} : 6 ) ) ) { 257 | print( "Setting I/O priority to reduce system load: " . Cpanel::IONice::get_ionice() . "\n\n" ); 258 | setpriority( 0, 0, 19 ); 259 | } 260 | 261 | my $fh; 262 | lOG: for my $log (@logfiles) { 263 | if ( $log =~ /[.]gz$/ ) { 264 | my @cmd = ( qw{ gunzip -c -f }, $logdir . $log ); 265 | if ( !open $fh, '-|', @cmd ) { 266 | print_warn("Skipping $logdir/$log: Cannot open pipe to read stdout from command '@{ [ join ' ', @cmd ] }' : $!\n"); 267 | next LOG; 268 | } 269 | } 270 | else { 271 | if ( !open $fh, '<', $logdir . $log ) { 272 | print_warn("Skipping $logdir/$log: Cannot open for reading $!\n"); 273 | next LOG; 274 | } 275 | } 276 | while ( my $block = Cpanel::IO::read_bytes_to_end_of_line( $fh, 65_535 ) ) { 277 | foreach my $line ( split( m{\n}, $block ) ) { 278 | next if ( $line =~ m{__cpanel__service__auth__icontact__} ); 279 | push @auth_password_hits, $2 if ( $line =~ $auth_password_regex ); 280 | push @auth_sendmail_hits, $1 if ( $line =~ $auth_sendmail_regex ); 281 | push @auth_local_user_hits, $1 if ( $line =~ $auth_local_user_regex ); 282 | push @subject_hits, $1 if ( $line =~ $subject_regex ); 283 | } 284 | } 285 | close($fh); 286 | } 287 | 288 | # Print info 289 | print_bold_white("Emails sent via Password Authentication:\n"); 290 | if (@auth_password_hits) { 291 | sort_uniq(@auth_password_hits); 292 | } 293 | else { 294 | print "None\n"; 295 | } 296 | print "\n"; 297 | print_bold_white("Directories where email was sent via sendmail/script:\n"); 298 | if (@auth_sendmail_hits) { 299 | sort_uniq(@auth_sendmail_hits); 300 | } 301 | else { 302 | print "None\n"; 303 | } 304 | print "\n"; 305 | print_bold_white("Users who sent mail via local SMTP:\n"); 306 | if (@auth_local_user_hits) { 307 | sort_uniq(@auth_local_user_hits); 308 | } 309 | else { 310 | print "None\n"; 311 | } 312 | print "\n"; 313 | print_bold_white("Subjects by commonality:\n"); 314 | sort_uniq(@subject_hits); 315 | print "\n"; 316 | 317 | return; 318 | } 319 | 320 | sub print_exim_queue { 321 | 322 | # Print exim queue length 323 | print_bold_white("Exim Queue: "); 324 | my $queue = get_exim_queue(); 325 | if ( $queue >= 1000 ) { 326 | print_bold_red("$queue\n"); 327 | } 328 | else { 329 | print_bold_green("$queue\n"); 330 | } 331 | return; 332 | } 333 | 334 | sub get_exim_queue { 335 | my $queue = timed_run_trap_stderr( 10, 'exim', '-bpc' ); 336 | return $queue; 337 | } 338 | 339 | sub rbl_check { 340 | my $rbls = shift; 341 | my @rbls = split( /,/, $rbls ); 342 | my @ips; 343 | 344 | # Fetch IP's... should we only check mailips? this is more thorough... 345 | # could ignore local through bogon regex? 346 | return unless my $ips = get_ips(); 347 | 348 | # Uncomment the following for testing positive hits 349 | # push @$ips, qw{ 127.0.0.2 }; 350 | 351 | # In cPanel 11.84, we switched to the libunbound resolver 352 | my ( $cp_numeric_version, $cp_original_version ) = get_cpanel_version(); 353 | my $libunbound = ( version_compare( $cp_numeric_version, qw( < 11.84) ) ) ? 0 : 1; 354 | 355 | # If "all" is found in the --rbl arg, ignore rest, use default rbl list 356 | # maybe we should append so that user can specify all and ones which are not included in the list? 357 | @rbls = @RBLS if ( grep { /\ball\b/i } @rbls ); 358 | print_bold_white("Checking IP's against RBL's...\n"); 359 | print "------------------------------\n"; 360 | 361 | foreach my $ip (@$ips) { 362 | print "$ip:\n"; 363 | my $ip_rev = join( '.', reverse split( '\.', $ip ) ); 364 | foreach my $rbl (@rbls) { 365 | printf( "\t%-25s ", $rbl ); 366 | 367 | my $result; 368 | if ($libunbound) { 369 | $result = dns_query( "$ip_rev.$rbl", 'A' )->[0] || 0; 370 | } 371 | else { 372 | # This uses libunbound, which will return an aref, but we can always expect just one result here 373 | $result = dns_query_pre_84( "$ip_rev.$rbl", 'A' ) || 0; 374 | } 375 | 376 | if ( $result =~ /\A 127\.0\.0\./xms ) { 377 | print_bold_red("LISTED\n"); 378 | } 379 | else { 380 | print_bold_green("GOOD\n"); 381 | } 382 | } 383 | print "\n"; 384 | } 385 | 386 | return; 387 | } 388 | 389 | sub rbl_list { 390 | print_bold_white("Available RBL's:\n"); 391 | print "----------------\n"; 392 | 393 | foreach my $rbl (@RBLS) { 394 | print "$rbl\n"; 395 | } 396 | print "\n"; 397 | return; 398 | } 399 | 400 | sub maillog_check { 401 | my @logfiles; 402 | my $logcount = 0; 403 | 404 | # General 405 | my @out_of_memory; 406 | my $out_of_memory_regex = qr{lmtp\(([\w\.@]+)\): Fatal: \S+: Out of memory}; 407 | 408 | my $time_backwards = 0; 409 | my $time_backwards_regex = qr{Fatal: Time just moved backwards by \d+ \w+\. This might cause a lot of problems, so I'll just kill myself now}; 410 | 411 | # Quota errors 412 | my @quota_failed; 413 | my $quotactl_failed_regex = qr{quota-fs: (quotactl\(Q_X?GETQUOTA, [\w/]+\) failed: .+)}; 414 | my $ioctl_failed_regex = qr{quota-fs: (ioctl\([\w/]+, Q_QUOTACTL\) failed: .+)}; 415 | my $invalid_nfs_regex = qr{quota-fs: (.+ is not a valid NFS device path)}; 416 | my $unrespponsive_rpc_regex = qr{quota-fs: (could not contact RPC service on .+)}; 417 | my $rquota_remote_regex = qr{quota-fs: (remote( ext)? rquota call failed: .+)}; 418 | my $rquota_eacces_regex = qr{quota-fs: (permission denied to( ext)? rquota service)}; 419 | my $rquota_compile_regex = qr{quota-fs: (rquota not compiled with group support)}; 420 | my $dovecot_compile_regex = qr{quota-fs: (Dovecot was compiled with Linux quota .+)}; 421 | my $unrec_code_regex = qr{quota-fs: (unrecognized status code .+)}; 422 | 423 | # Spamd error 424 | my $pyzor_timeout = 0; 425 | my $pyzor_timeout_regex = qr{Timeout: Did not receive a response from the pyzor server public\.pyzor\.org}; 426 | 427 | my $pyzor_unreachable = 0; 428 | my $pyzor_unreachable_regex = qr{pyzor: check failed: Cannot connect to public.pyzor.org:24441: IO::Socket::INET: connect: Network is unreachable}; 429 | 430 | print_bold_white("Checking Maillog for common errors...\n"); 431 | print "-----------------------------------------\n"; 432 | 433 | # Set logdir, ensure trailing slash, and bail if the provided logdir doesn't exist: 434 | my $logdir = ( $opts{logdir} ) ? ( $opts{logdir} ) : $LOGDIR; 435 | $logdir =~ s@/*$@/@; 436 | 437 | if ( !-d $logdir ) { 438 | print_warn("$opts{logdir}: No such file or directory. Skipping spam check...\n\n"); 439 | return; 440 | } 441 | 442 | # Collect log files 443 | for my $file ( grep { m/^maillog/ } @{ Cpanel::FileUtils::Dir::get_directory_nodes($logdir) } ) { 444 | if ( $opts{rotated} ) { 445 | if ( ( $file =~ m/maillog-/ ) && ( $logcount ne $ROTATED_LIMIT ) ) { 446 | push @logfiles, $file; 447 | $logcount++; 448 | } 449 | } 450 | push @logfiles, $file if ( $file =~ m/maillog$/ ); 451 | } 452 | print_warn("Safeguard triggered... --rotated is limited to $ROTATED_LIMIT logs\n") if ( $logcount eq $ROTATED_LIMIT ); 453 | 454 | # Bail if we can't find any logs 455 | return print_warn("Bailing, no maillog found...\n\n") if ( !@logfiles ); 456 | 457 | # Set ionice 458 | my %cpconf = get_conf($CPANEL_CONFIG_FILE); 459 | if ( ( !$opts{rude} ) && ( Cpanel::IONice::ionice( 'best-effort', exists $cpconf{'ionice_import_exim_data'} ? $cpconf{'ionice_import_exim_data'} : 6 ) ) ) { 460 | print( "Setting I/O priority to reduce system load: " . Cpanel::IONice::get_ionice() . "\n\n" ); 461 | setpriority( 0, 0, 19 ); 462 | } 463 | 464 | my $fh; 465 | lOG: for my $log (@logfiles) { 466 | if ( $log =~ /[.]gz$/ ) { 467 | my @cmd = ( qw{ gunzip -c -f }, $logdir . $log ); 468 | if ( !open $fh, '-|', @cmd ) { 469 | print_warn("Skipping $logdir/$log: Cannot open pipe to read stdout from command '@{ [ join ' ', @cmd ] }' : $!\n"); 470 | next LOG; 471 | } 472 | } 473 | else { 474 | if ( !open $fh, '<', $logdir . $log ) { 475 | print_warn("Skipping $logdir/$log: Cannot open for reading $!\n"); 476 | next LOG; 477 | } 478 | } 479 | while ( my $block = Cpanel::IO::read_bytes_to_end_of_line( $fh, 65_535 ) ) { 480 | foreach my $line ( split( m{\n}, $block ) ) { 481 | push @out_of_memory, $1 if ( $line =~ $out_of_memory_regex ); 482 | push @quota_failed, $1 if ( $line =~ $quotactl_failed_regex ); 483 | ++$pyzor_timeout if ( $line =~ $pyzor_timeout_regex ); 484 | } 485 | } 486 | close($fh); 487 | } 488 | 489 | # Print info 490 | print_bold_white("LMTP quota issues:\n"); 491 | if (@quota_failed) { 492 | sort_uniq(@quota_failed); 493 | } 494 | else { 495 | print "None\n"; 496 | } 497 | print "\n"; 498 | print_bold_white("Email accounts triggering LMTP Out of memory:\n"); 499 | if (@out_of_memory) { 500 | sort_uniq(@out_of_memory); 501 | } 502 | else { 503 | print "None\n"; 504 | } 505 | print "\n"; 506 | print_bold_white("Timeouts to public.pyzor.org:24441:\n"); 507 | if ( $pyzor_timeout ne 0 ) { 508 | print "Pyzor timed out $pyzor_timeout times\n"; 509 | } 510 | else { 511 | print "None\n"; 512 | } 513 | print "\n"; 514 | 515 | return; 516 | } 517 | 518 | sub version_compare { 519 | 520 | # example: return if version_compare($ver_string, qw( >= 1.2.3.3 )); 521 | # Must be no more than four version numbers separated by periods and/or underscores. 522 | my ( $ver1, $mode, $ver2 ) = @_; 523 | return if ( !defined($ver1) || ( $ver1 =~ /[^\._0-9]/ ) ); 524 | return if ( !defined($ver2) || ( $ver2 =~ /[^\._0-9]/ ) ); 525 | 526 | # Shamelessly copied the comparison logic out of Cpanel::Version::Compare 527 | my %modes = ( 528 | '>' => sub { 529 | return if $_[0] eq $_[1]; 530 | return _version_cmp(@_) > 0; 531 | }, 532 | '<' => sub { 533 | return if $_[0] eq $_[1]; 534 | return _version_cmp(@_) < 0; 535 | }, 536 | '==' => sub { return $_[0] eq $_[1] || _version_cmp(@_) == 0; }, 537 | '!=' => sub { return $_[0] ne $_[1] && _version_cmp(@_) != 0; }, 538 | '>=' => sub { 539 | return 1 if $_[0] eq $_[1]; 540 | return _version_cmp(@_) >= 0; 541 | }, 542 | '<=' => sub { 543 | return 1 if $_[0] eq $_[1]; 544 | return _version_cmp(@_) <= 0; 545 | } 546 | ); 547 | return if ( !exists $modes{$mode} ); 548 | return $modes{$mode}->( $ver1, $ver2 ); 549 | } 550 | 551 | sub _version_cmp { 552 | my ( $first, $second ) = @_; 553 | my ( $a1, $b1, $c1, $d1 ) = split /[\._]/, $first; 554 | my ( $a2, $b2, $c2, $d2 ) = split /[\._]/, $second; 555 | for my $ref ( \$a1, \$b1, \$c1, \$d1, \$a2, \$b2, \$c2, \$d2, ) { # Fill empties with 0 556 | $$ref = 0 unless defined $$ref; 557 | } 558 | return $a1 <=> $a2 || $b1 <=> $b2 || $c1 <=> $c2 || $d1 <=> $d2; 559 | } 560 | 561 | sub get_cpanel_version { 562 | my $cpanel_version_file = '/usr/local/cpanel/version'; 563 | my $numeric_version; 564 | my $original_version; 565 | 566 | if ( open my $file_fh, '<', $cpanel_version_file ) { 567 | $original_version = readline($file_fh); 568 | close $file_fh; 569 | } 570 | return ( 'UNKNOWN', 'UNKNOWN' ) unless defined $original_version; 571 | chomp $original_version; 572 | 573 | # Parse either 1.2.3.4 or 1.2.3-THING_4 to 1.2.3.4 574 | $numeric_version = join( '.', split( /\.|-[a-zA-Z]+_/, $original_version ) ); 575 | $numeric_version = 'UNKNOWN' unless $numeric_version =~ /^\d+\.\d+\.\d+\.\d+$/; 576 | 577 | return ( $numeric_version, $original_version ); 578 | } 579 | 580 | sub get_ips { 581 | my @ips; 582 | return if !load_module_with_fallbacks( 583 | 'needed_subs' => [qw{get_detailed_ip_cfg}], 584 | 'modules' => [qw{Whostmgr::Ips}], 585 | 'fail_warning' => 'can\'t load Whostmgr::Ips', 586 | ); 587 | 588 | return if !load_module_with_fallbacks( 589 | 'needed_subs' => [qw{get_public_ip}], 590 | 'modules' => [qw{Cpanel::NAT}], 591 | 'fail_warning' => 'can\'t load Cpanel::NAT', 592 | ); 593 | 594 | my $ipref = Whostmgr::Ips::get_detailed_ip_cfg(); 595 | foreach my $iphash ( @{$ipref} ) { 596 | push @ips, Cpanel::NAT::get_public_ip( $iphash->{'ip'} ); 597 | } 598 | 599 | return \@ips; 600 | } 601 | 602 | sub dns_query_pre_84 { 603 | my ( $name, $type ) = @_; 604 | 605 | return if !load_module_with_fallbacks( 606 | 'needed_subs' => [qw{new recursive_query}], 607 | 'modules' => [qw{Cpanel::DnsRoots::Resolver}], 608 | 'fail_warning' => 'can\'t load Cpanel::DnsRoots::Resolver', 609 | ); 610 | 611 | my $dns = Cpanel::DnsRoots::Resolver->new(); 612 | my ($res) = $dns->recursive_query( $name, $type ); 613 | return $res; 614 | } 615 | 616 | sub dns_query { 617 | my ( $name, $type ) = @_; 618 | 619 | return if !load_module_with_fallbacks( 620 | 'needed_subs' => [qw{new recursive_queries}], 621 | 'modules' => [qw{Cpanel::DNS::Unbound}], 622 | 'fail_warning' => 'can\'t load Cpanel::DNS::Unbound', 623 | ); 624 | 625 | my $dns = Cpanel::DNS::Unbound->new(); 626 | my ($res) = $dns->recursive_queries( [ [ $name, $type ] ] )->[0]; 627 | return $res->{'decoded_data'} || $res->{result}{data}; 628 | } 629 | 630 | sub sort_uniq { 631 | my @input = @_; 632 | my %count; 633 | my $line = 1; 634 | $opts{limit} //= $LIMIT; 635 | $opts{threshold} //= $THRESHOLD; 636 | foreach (@input) { $count{$_}++; } 637 | for ( sort { $count{$b} <=> $count{$a} } keys %count ) { 638 | if ( $line ne $opts{limit} ) { 639 | printf( "%7d %s\n", "$count{$_}", "$_" ) if ( $count{$_} >= $opts{threshold} ); 640 | $line++; 641 | } 642 | else { 643 | printf( "%7d %s\n", "$count{$_}", "$_" ) if ( $count{$_} >= $opts{threshold} ); 644 | last; 645 | } 646 | } 647 | return; 648 | } 649 | 650 | # cpanel.confg and exim.conf.localopts 651 | sub get_conf { 652 | my $conf = shift; 653 | my %cpconf; 654 | if ( open( my $cpconf_fh, '<', $conf ) ) { 655 | local $/ = undef; 656 | %cpconf = map { ( split( /=/, $_, 2 ) )[ 0, 1 ] } split( /\n/, readline($cpconf_fh) ); 657 | close $cpconf_fh; 658 | return %cpconf; 659 | } 660 | else { 661 | print_warn("Could not open file: $conf\n"); 662 | } 663 | return; 664 | } 665 | 666 | # exec utilities, taken from SSP 667 | sub timed_run_trap_stderr { 668 | my ( $timer, @PROGA ) = @_; 669 | return _timedsaferun( $timer, 1, @PROGA ); 670 | } 671 | 672 | sub _timedsaferun { # Borrowed from WHM 66 Cpanel::SafeRun::Timed and modified 673 | # We need to be sure to never return undef, return an empty string instead. 674 | my ( $timer, $stderr_to_stdout, @PROGA ) = @_; 675 | return '' if ( substr( $PROGA[0], 0, 1 ) eq '/' && !-x $PROGA[0] ); 676 | $timer = $timer ? $timer : 25; # A timer value of 0 means use the default, currently 25. 677 | $timer = $OPT_TIMEOUT ? $OPT_TIMEOUT : $timer; 678 | 679 | my $output; 680 | my $complete = 0; 681 | my $pid; 682 | my $fh; # FB-63723: must declare $fh before eval block in order to avoid unwanted implicit waitpid on die 683 | eval { 684 | local $SIG{'__DIE__'} = 'DEFAULT'; 685 | local $SIG{'ALRM'} = sub { $output = ''; print RED ON_BLACK 'Timeout while executing: ' . join( ' ', @PROGA ) . "\n"; die; }; 686 | alarm($timer); 687 | if ( $pid = open( $fh, '-|' ) ) { ## no critic (BriefOpen) 688 | local $/; 689 | $output = readline($fh); 690 | close($fh); 691 | } 692 | elsif ( defined $pid ) { 693 | open( STDIN, '<', '/dev/null' ); ## no critic (BriefOpen) 694 | if ($stderr_to_stdout) { 695 | open( STDERR, '>&', 'STDOUT' ); ## no critic (BriefOpen) 696 | } 697 | else { 698 | open( STDERR, '>', '/dev/null' ); ## no critic (BriefOpen) 699 | } 700 | exec(@PROGA) or exit 1; ## no critic (NoExitsFromSubroutines) 701 | } 702 | else { 703 | print RED ON_BLACK 'Error while executing: [ ' . join( ' ', @PROGA ) . ' ]: ' . $! . "\n"; 704 | alarm 0; 705 | die; 706 | } 707 | $complete = 1; 708 | alarm 0; 709 | }; 710 | alarm 0; 711 | if ( !$complete && $pid && $pid > 0 ) { 712 | kill( 15, $pid ); #TERM 713 | sleep(2); # Give the process a chance to die 'nicely' 714 | kill( 9, $pid ); #KILL 715 | } 716 | return defined $output ? $output : ''; 717 | } 718 | 719 | # SUB load_module_with_fallbacks( 720 | # 'modules' => [ 'module1', 'module2', ... ], 721 | # 'needed_subs' => [ 'do_needful', ... ], 722 | # 'fallback' => sub { *do_needful = sub { ... }; return; }, 723 | # 'fail_warning' => "Oops, something went wrong, you may want to do something about this", 724 | # 'fail_fatal' => 1, 725 | # ); 726 | # 727 | # Input is HASH of options: 728 | # 'modules' => ARRAYREF of SCALAR strings corresponding to module names to attempt to import. These are attempted first. 729 | # 'needed_subs' => ARRAYREF of SCALAR strings corresponding to subroutine names you need defined from the module(s). 730 | # 'fallback' => CODEREF which defines the needed subs manually. Only used if all modules passed in above fail to load. Optional. 731 | # 'fail_warning' => SCALAR string that will convey a message to the user if the module(s) fail to load. Optional. 732 | # 'fail_fatal' => BOOL whether you want to die if you fail to load the needed subs/modules via all available methods. Optional. 733 | # 734 | # Returns the module/namespace that loaded correctly, throws if all available attempts at finding the desired needed_subs subs fail and fail_fatal is passed. 735 | sub load_module_with_fallbacks { 736 | my %opts = @_; 737 | my $namespace_loaded; 738 | foreach my $module2try ( @{ $opts{'modules'} } ) { 739 | 740 | # Don't 'require' it if we already have it. 741 | my $inc_entry = join( "/", split( "::", $module2try ) ) . ".pm"; 742 | if ( !$INC{$module2try} ) { 743 | local $@; 744 | next if !eval "require $module2try; 1"; ## no critic (StringyEval) 745 | } 746 | 747 | # Check if the imported modules 'can' do the job 748 | next if ( scalar( grep { $module2try->can($_) } @{ $opts{'needed_subs'} } ) != scalar( @{ $opts{'needed_subs'} } ) ); 749 | 750 | # Ok, we're good to go! 751 | $namespace_loaded = $module2try; 752 | last; 753 | } 754 | 755 | # Fallback to coderef, but don't do sanity checking on this, as it is presumed the caller "knows what they are doing" if passing a coderef. 756 | if ( !$namespace_loaded ) { 757 | if ( !$opts{'fallback'} || ref $opts{'fallback'} != 'CODE' ) { 758 | print_warn( 'Missing Perl Module(s): ' . join( ', ', @{ $opts{'modules'} } ) . ' -- ' . $opts{'fail_warning'} . " -- Try using /usr/local/cpanel/3rdparty/bin/perl?\n" ) if $opts{'fail_warning'}; 759 | die "Stopping here." if $opts{'fail_fatal'}; 760 | } 761 | else { 762 | $opts{'fallback'}->(); 763 | 764 | # call like main::subroutine instead of Name::Space::subroutine 765 | $namespace_loaded = 'main'; 766 | } 767 | } 768 | return $namespace_loaded; 769 | } 770 | 771 | # pretty prints 772 | sub print_warn { 773 | my $text = shift // ''; 774 | return if $text eq ''; 775 | 776 | print BOLD RED ON_BLACK '[WARN] * '; 777 | print WHITE ON_BLACK "$text"; 778 | return; 779 | } 780 | 781 | sub print_info { 782 | my $text = shift // ''; 783 | return if $text eq ''; 784 | 785 | print BOLD GREEN ON_BLACK '[INFO] * '; 786 | print WHITE ON_BLACK "$text"; 787 | return; 788 | } 789 | 790 | sub print_std { 791 | my $text = shift // ''; 792 | return if $text eq ''; 793 | 794 | print BOLD BRIGHT_BLUE ON_BLACK ' * '; 795 | print BOLD WHITE ON_BLACK "$text"; 796 | return; 797 | } 798 | 799 | sub print_bold_white { 800 | my $text = shift // ''; 801 | return if $text eq ''; 802 | 803 | print BOLD WHITE ON_BLACK "$text"; 804 | return; 805 | } 806 | 807 | sub print_bold_red { 808 | my $text = shift // ''; 809 | return if $text eq ''; 810 | 811 | print BOLD RED ON_BLACK "$text"; 812 | return; 813 | } 814 | 815 | sub print_bold_green { 816 | my $text = shift // ''; 817 | return if $text eq ''; 818 | 819 | print BOLD GREEN ON_BLACK "$text"; 820 | return; 821 | } 822 | 823 | sub hostname_check { 824 | if ( $hostname eq $domain ) { 825 | print_warn("[WARN] * Your hostname $hostname appears to be the same as $domain. Was this intentional?\n"); 826 | } 827 | } 828 | 829 | sub domain_exist { 830 | open( USERDOMAINS, "/etc/userdomains" ); 831 | while () { 832 | if (/^$domain: (\S+)/i) { 833 | my $user = $1; 834 | print_info("The domain $domain is owned by $user.\n"); 835 | my $suspchk = "/var/cpanel/suspended/$user"; 836 | if ( -e $suspchk ) { 837 | print_warn("[WARN] * The user $user is SUSPENDED.\n"); 838 | } 839 | return; 840 | } 841 | } 842 | print_warn("[WARN] * The domain $domain DOES NOT exist on this server.\n"); 843 | close(USERDOMAINS); 844 | } 845 | 846 | sub domain_filters { 847 | print_warn("The virtual filter for $domain is NOT empty (/etc/vfilters/$domain).\n") if -s "/etc/vfilters/$domain"; 848 | } 849 | 850 | sub check_local_or_remote { 851 | 852 | open my $loc_domain, '<', '/etc/localdomains'; 853 | while (<$loc_domain>) { 854 | if (/^${domain}$/) { 855 | print_info("$domain is in LOCALDOMAINS.\n"); 856 | } 857 | } 858 | close $loc_domain; 859 | 860 | open my $remote_domain, '<', '/etc/remotedomains'; 861 | while (<$remote_domain>) { 862 | if (/^${domain}$/) { 863 | print_info("[INFO] *"); 864 | print_std(" $domain is in REMOTEDOMAINS.\n"); 865 | last; 866 | } 867 | } 868 | close $remote_domain; 869 | } 870 | 871 | sub mx_check { 872 | my @mx_record = qx/dig mx $domain +short/; 873 | chomp(@mx_record); 874 | my @dig_mx_ip; 875 | 876 | foreach my $mx_record (@mx_record) { 877 | my $dig_mx_ip = qx/dig $mx_record +short/; 878 | push( @dig_mx_ip, $dig_mx_ip ); 879 | chomp(@dig_mx_ip); 880 | 881 | } 882 | 883 | foreach my $mx_record (@mx_record) { 884 | print_info("\t \\_ MX Record: $mx_record\n"); 885 | } 886 | foreach (@mx_record) { 887 | print_info( "\t\t \\_ " . qx/dig $_ +short/ ); 888 | 889 | } 890 | } 891 | 892 | sub domain_resolv { 893 | my $domain_ip; 894 | chomp( $domain_ip = run( 'dig', $domain, '@8.8.4.4', '+short' ) ); 895 | if ( grep { $_ eq $domain_ip } @local_ipaddrs_list ) { 896 | print_info("The domain $domain resolves to IP: \n\t \\_ $domain_ip\n"); 897 | return; 898 | } 899 | elsif ( ( !defined $domain_ip ) || ( $domain_ip eq '' ) ) { 900 | print_warn("Domain did not return an A record. It is likely not registered or not pointed to any IP\n"); 901 | } 902 | else { 903 | print_warn("The domain $domain DOES NOT resolve to this server.\n"); 904 | print_warn("\t\\_ It currently resolves to: $domain_ip \n"); 905 | } 906 | 907 | sub check_blacklists { 908 | 909 | # Way more lists out there, but I'll add them later. 910 | my %list = ( 911 | 'sbl-xbl.spamhaus.org' => 'Spamhaus', 912 | 'pbl.spamhaus.org' => 'Spamhaus', 913 | 'sbl.spamhaus.org' => 'Spamhaus', 914 | 'bl.spamcop.net' => 'SpamCop', 915 | 'dsn.rfc-ignorant.org' => 'Rfc-ignorant.org', 916 | 'postmaster.rfc-ignorant.org' => 'Rfc.ignorant.org', 917 | 'abuse.rfc-ignorant.org' => 'Rfc.ignorant.org', 918 | 'whois.rfc-ignorant.org' => 'Rfc.ignorant.org', 919 | 'ipwhois.rfc-ignorant.org' => 'Rfc.ignorant.org', 920 | 'bogusmx.rfc-ignorant.org' => 'Rfc.ignorant.org', 921 | 'dnsbl.sorbs.net' => 'Sorbs', 922 | 'badconf.rhsbl.sorbs.net' => 'Sorbs', 923 | 'nomail.rhsbl.sorbs.net' => 'Sorbs', 924 | 'cbl.abuseat.org' => 'Abuseat.org', 925 | 'relays.visi.com' => 'Visi.com', 926 | 'zen.spamhaus.org' => 'Spamhaus', 927 | 'bl.spamcannibal.org' => 'Spamcannibal', 928 | 'ubl.unsubscore.com' => 'LashBack', 929 | 'b.barracudacentral.org' => 'Barracuda', 930 | ); 931 | 932 | # Grab the mail addresses 933 | 934 | my @files = qw(/var/cpanel/mainip /etc/mailips); 935 | 936 | my @ips = ''; 937 | my $lines; 938 | my $reverse_lines; 939 | my @reverse_ips = ''; 940 | foreach my $files (@files) { 941 | open FILE, "$files"; 942 | while ( $lines = ) { 943 | if ( $lines =~ m/([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/ ) { 944 | $lines = "$1\.$2\.$3\.$4"; 945 | $reverse_lines = "$4\.$3\.$2\.$1"; 946 | chomp $lines; 947 | chomp $reverse_lines; 948 | push @ips, $lines; 949 | push @reverse_ips, $reverse_lines; 950 | } 951 | } 952 | close FILE; 953 | } 954 | 955 | shift @ips; 956 | 957 | print_info("[INFO] * "); 958 | print_std("Checking Blacklists:\n"); 959 | 960 | foreach my $reverse_ip (@reverse_ips) { 961 | my $ip = shift @ips; 962 | my $key; 963 | my $value; 964 | while ( ( $key, $value ) = each %list ) { 965 | my $host = "$reverse_ip.$key\n"; 966 | chomp($host); 967 | my $ret = run( "host", "$host" ); 968 | my $ret2 = grep( /(NXDOMAIN|SERVFAIL)/, $ret ); 969 | my $status = $ret2 ? "not listed" : "is listed"; 970 | if ( $status eq 'not listed' ) { 971 | print ""; 972 | } 973 | else { 974 | print_warn("\t\\_"); 975 | print_std(" $ip "); 976 | print_warn("$status on $value\n"); 977 | } 978 | } 979 | } 980 | 981 | sub check_spf { 982 | my @check = qx/dig $domain TXT/; 983 | if ( grep ( m/.*spf.*/, @check ) ) { 984 | print_info("$domain has the folloiwng SPF records:\n"); 985 | foreach my $check (@check) { 986 | if ( $check =~ m/.*spf.*/ ) { 987 | print_std("\t\\_ $check"); 988 | } 989 | } 990 | } 991 | else { 992 | return; 993 | } 994 | } 995 | 996 | sub check_dkim { 997 | my @check_dkim = qx/dig default._domainkey.$domain TXT +short/; 998 | if (@check_dkim) { 999 | foreach my $check_dkim (@check_dkim) { 1000 | print_info("$domain has the following domain keys:\n "); 1001 | print_std("\t\\_ $check_dkim"); 1002 | 1003 | } 1004 | } 1005 | 1006 | else { 1007 | print_warn("[WARN] * Domain does not have a DKIM record\n"); 1008 | } 1009 | } 1010 | 1011 | sub sent_email { 1012 | open FILE, "/var/log/exim_mainlog"; 1013 | 1014 | print_warn("\nEmails by user: "); 1015 | print "\n\n"; 1016 | our @system_users = ""; 1017 | 1018 | while ( my $lines_users = ) { 1019 | if ( $lines_users =~ /(U\=)(.+?)(\sP\=)/i ) { 1020 | my $line_users = $2; 1021 | push( @system_users, $line_users ); 1022 | } 1023 | } 1024 | my %count; 1025 | $count{$_}++ foreach @system_users; 1026 | while ( my ( $key, $value ) = each(%count) ) { 1027 | if ( $key =~ /^$/ ) { 1028 | delete( $count{$key} ); 1029 | } 1030 | } 1031 | 1032 | foreach my $value ( 1033 | reverse sort { $count{$a} <=> $count{$b} } 1034 | keys %count 1035 | ) { 1036 | print " " . $count{$value} . " : " . $value . "\n"; 1037 | } 1038 | 1039 | print "\n\n"; 1040 | print "===================\n"; 1041 | print " Total: " . scalar( @system_users - 1 ); 1042 | print "\n===================\n"; 1043 | 1044 | print_warn("\nEmail accounts sending out mail:\n\n"); 1045 | 1046 | open FILE, "/var/log/exim_mainlog"; 1047 | my @email_users = ''; 1048 | while ( my $lines_email = ) { 1049 | if ( $lines_email =~ /(_login:|_plain:)(.+?)(\sS=)/i ) { 1050 | my $lines_emails = $2; 1051 | push( @email_users, $lines_emails ); 1052 | } 1053 | } 1054 | my %email_count; 1055 | $email_count{$_}++ foreach @email_users; 1056 | while ( my ( $key, $value ) = each(%email_count) ) { 1057 | if ( $key =~ /^$/ ) { 1058 | delete( $email_count{$key} ); 1059 | } 1060 | } 1061 | 1062 | foreach my $value ( 1063 | reverse sort { $email_count{$a} <=> $email_count{$b} } 1064 | keys %email_count 1065 | ) { 1066 | print " " . $email_count{$value} . " : " . $value . "\n"; 1067 | } 1068 | 1069 | print "\n"; 1070 | print "===================\n"; 1071 | print "Total: " . scalar(@email_users); 1072 | print "\n===================\n"; 1073 | 1074 | ## Section for current working directories 1075 | 1076 | print_warn("\nDirectories mail is originating from:\n\n\n"); 1077 | 1078 | open FILE, "/var/log/exim_mainlog"; 1079 | my @dirs; 1080 | my $dirs; 1081 | 1082 | while ( $dirs = ) { 1083 | if ( ( $dirs =~ /(cwd=)(.+?)(\s)/i ) && ( $dirs !~ /(cwd=\/.+?exim)/i ) && ( $dirs !~ /(cwd=.+? CronDaemon)/i ) && ( $dirs !~ /(cwd=\/etc\/csf)/i ) && ( $dirs !~ /cwd=\/\s/i ) ) { 1084 | my $dir = $2; 1085 | push( @dirs, $dir ); 1086 | } 1087 | } 1088 | my %dirs; 1089 | $dirs{$_}++ foreach @dirs; 1090 | while ( my ( $key, $value ) = each(%dirs) ) { 1091 | if ( $key =~ /^$/ ) { 1092 | delete( $dirs[$key] ); 1093 | } 1094 | } 1095 | 1096 | while ( my ( $key, $value ) = each(%dirs) ) { 1097 | if ( $key =~ /^$/ ) { 1098 | delete( $dirs{$key} ); 1099 | } 1100 | } 1101 | 1102 | foreach my $value ( reverse sort { $dirs{$a} <=> $dirs{$b} } keys %dirs ) { 1103 | print " " . $dirs{$value} . " : " . $value . "\n"; 1104 | } 1105 | 1106 | print "\n"; 1107 | print "===================\n"; 1108 | if ( @dirs < 1 ) { 1109 | print "Total: " . @dirs; 1110 | } 1111 | else { 1112 | print "Total: " . scalar( @dirs - 1 ); 1113 | } 1114 | print "\n===================\n"; 1115 | 1116 | print_warn("\nTop 20 Email Titles:\n\n\n"); 1117 | 1118 | open FILE, "/var/log/exim_mainlog"; 1119 | my @titles; 1120 | 1121 | while ( my $titles = ) { 1122 | if ( $titles =~ /((U=|_login:).+)((?<=T=\").+?(?=\"))(.+$)/i ) { 1123 | my $title = $3; 1124 | push( @titles, $title ); 1125 | } 1126 | } 1127 | our %titlecount; 1128 | my @titlecount; 1129 | $titlecount{$_}++ foreach @titles; 1130 | while ( my ( $key, $value ) = each(%titlecount) ) { 1131 | if ( $key =~ /^$/ ) { 1132 | delete( $titlecount[$key] ); 1133 | } 1134 | } 1135 | 1136 | my $limit = 20; 1137 | my $loops = 0; 1138 | foreach my $value ( 1139 | reverse sort { $titlecount{$a} <=> $titlecount{$b} } 1140 | keys %titlecount 1141 | ) { 1142 | print " " . $titlecount{$value} . " : " . $value . "\n"; 1143 | $loops++; 1144 | if ( $loops >= $limit ) { 1145 | last; 1146 | } 1147 | } 1148 | print "\n\n"; 1149 | print "===================\n"; 1150 | print "Total: " . scalar( @titles - 1 ); 1151 | print "\n===================\n\n"; 1152 | 1153 | close FILE; 1154 | } 1155 | } 1156 | } 1157 | 1158 | sub get_local_ipaddrs { ## Ripped from SSP as well. Likely less gratuitous, but will likely drop the use of run() in the future cuz IPC. 1159 | my @ifconfig = split /\n/, run( 'ifconfig', '-a' ); 1160 | for my $line (@ifconfig) { 1161 | if ( $line =~ m{ (\d+\.\d+\.\d+\.\d+) }xms ) { 1162 | my $ipaddr = $1; 1163 | unless ( $ipaddr =~ m{ \A 127\. }xms ) { 1164 | push @local_ipaddrs_list, $ipaddr; 1165 | } 1166 | } 1167 | } 1168 | return @local_ipaddrs_list; 1169 | } 1170 | 1171 | sub run { #Directly ripped run() from SSP; likely more gratuitous than what is actually needed. Remember to look into IPC::Run. 1172 | 1173 | my $cmdline = \@_; 1174 | my $output; 1175 | local ($/); 1176 | my ( $pid, $prog_fh ); 1177 | if ( $pid = open( $prog_fh, '-|' ) ) { 1178 | 1179 | } 1180 | else { 1181 | open STDERR, '>', '/dev/null'; 1182 | ( $ENV{'PATH'} ) = $ENV{'PATH'} =~ m/(.*)/; 1183 | exec(@$cmdline); 1184 | exit(127); 1185 | } 1186 | 1187 | if ( !$prog_fh || !$pid ) { 1188 | $? = -1; 1189 | return \$output; 1190 | } 1191 | $output = readline($prog_fh); 1192 | close($prog_fh); 1193 | return $output; 1194 | } 1195 | 1196 | #sub mx_consistency { 1197 | # my $main_ip = qx/hostname -i/; 1198 | # chomp($main_ip); 1199 | # my @mxcheck_local = qx/dig mx \@$main_ip $domain +short/; 1200 | # my @mxcheck_remote = qx/dig mx \@8.8.8.8 $domain +short/; 1201 | # if ( @mxcheck_local eq @mxcheck_remote ) { 1202 | # print_info("[INFO] Remote and local MX lookups match\n"); 1203 | # foreach (@mxcheck_local) { 1204 | # print_bold_white("\t\\_ Local MX: $domain IN MX $_"); 1205 | # } 1206 | # print "\n"; 1207 | # foreach (@mxcheck_remote) { 1208 | # print_info("\t\\_ Remote MX: $domain IN MX $_"); 1209 | # } 1210 | # } 1211 | # else { 1212 | # print_warn("[WARN] * Local MX does not match remote MX\n "); 1213 | # foreach (@mxcheck_local) { 1214 | # print_bold_white("\t\\_ Local MX: $domain IN MX $_"); 1215 | # } 1216 | # print "\n"; 1217 | # foreach (@mxcheck_remote) { 1218 | # print_bold_white("\t\\_ Remote MX: $domain IN MX $_"); 1219 | # } 1220 | # } 1221 | #} 1222 | 1223 | sub does_email_exist { 1224 | my $tcEmail = shift; 1225 | my ( $localpart, $domain ) = $tcEmail =~ /(.*)@(.*)/; 1226 | my $DataJSON = get_whmapi1( 'getdomainowner', "domain=$domain" ); 1227 | my $cpUser = $DataJSON->{data}->{user}; 1228 | my $ListPopsJSON = get_uapi( 'Email', 'list_pops', "--user=$cpUser" ); 1229 | for my $EmailPop ( @{ $ListPopsJSON->{result}->{data} } ) { 1230 | if ( $EmailPop->{email} eq $tcEmail ) { 1231 | print_info("[INFO] Email address $tcEmail exists.\n"); 1232 | return; 1233 | } 1234 | } 1235 | print_warn("[WARN] * $tcEmail doesn't seem to exist on this server.\n "); 1236 | return; 1237 | } 1238 | 1239 | sub get_doc_root { 1240 | 1241 | # REDO THIS!!! 1242 | my ( $user, $domain ) = $email =~ /(.*)@(.*)/; 1243 | my $DomainInfoJSON = get_whmapi1('get_domain_info'); 1244 | my $DomainInfoLines; 1245 | my $maindocroot; 1246 | for $DomainInfoLines ( @{ $DomainInfoJSON->{data}->{domains} } ) { 1247 | if ( $DomainInfoLines->{domain} eq $domain ) { 1248 | if ( $DomainInfoLines->{domain_type} eq "main" ) { 1249 | $maindocroot = $DomainInfoLines->{docroot}; 1250 | last; 1251 | } 1252 | } 1253 | } 1254 | if ( !defined $maindocroot ) { 1255 | print_warn("[WARN] * No Document root found\n"); 1256 | return; 1257 | } 1258 | return $maindocroot; 1259 | } 1260 | 1261 | sub email_valiases { 1262 | my $tcEmail = shift; 1263 | my ( $localpart, $domain ) = $tcEmail =~ /(.*)@(.*)/; 1264 | my $DataJSON = get_whmapi1( 'getdomainowner', "domain=$domain" ); 1265 | my $cpUser = $DataJSON->{data}->{user}; 1266 | my $dir = "/etc/valiases/$domain"; 1267 | return unless ( -s $dir ); 1268 | open FILE, $dir; 1269 | while ( my $lines = ) { 1270 | if ( $lines =~ /^$email/ ) { 1271 | if ( $lines =~ /\.\bautorespond/ ) { 1272 | print_warn("[WARN] * Autoresponder found in $dir\n"); 1273 | print_info("\t\t\\_$lines"); 1274 | } 1275 | else { 1276 | print_warn("[WARN] * Forwarder found in $dir\n"); 1277 | print_info("\t\t\\_$lines"); 1278 | } 1279 | } 1280 | } 1281 | } 1282 | 1283 | sub is_exim_running { 1284 | my $exim_port = qx/lsof -n -P -i :25/; 1285 | 1286 | if ( $exim_port =~ m/exim.+LISTEN*/ ) { 1287 | print_info("Exim is running on port 25\n"); 1288 | } 1289 | else { 1290 | print_warn("[WARN] * Exim is not running on port 25\n"); 1291 | } 1292 | } 1293 | 1294 | sub get_json_from_command { 1295 | my @cmd = @_; 1296 | return Cpanel::JSON::Load( Cpanel::SafeRun::Timed::timedsaferun( 30, @cmd ) ); 1297 | } 1298 | 1299 | sub get_whmapi1 { 1300 | return get_json_from_command( 'whmapi1', '--output=json', @_ ); 1301 | } 1302 | 1303 | sub get_uapi { 1304 | return get_json_from_command( 'uapi', '--output=json', @_ ); 1305 | } 1306 | 1307 | sub do_email { 1308 | my $tcEmail = shift; 1309 | my ( $localpart, $tcdomain ) = $tcEmail =~ /(.*)@(.*)/; 1310 | my $DataJSON = get_whmapi1( 'getdomainowner', "domain=$tcdomain" ); 1311 | my $cpUser = $DataJSON->{data}->{user}; 1312 | my $ListPopsJSON = get_uapi( 'Email', 'list_pops_with_disk', "--user=$cpUser", "domain=$tcdomain" ); 1313 | my $found = 0; 1314 | my $ShowHeader = 0; 1315 | for my $EmailPop ( @{ $ListPopsJSON->{result}->{data} } ) { 1316 | if ( $EmailPop->{email} eq $tcEmail ) { 1317 | $found = 1; 1318 | } 1319 | } 1320 | print_warn("[WARN] * $tcEmail doesn't seem to exist on this server.\n ") unless ($found); 1321 | return unless ($found); 1322 | my @listPops; 1323 | for my $EmailPop ( @{ $ListPopsJSON->{result}->{data} } ) { 1324 | my $emailacctline = $EmailPop->{email}; 1325 | next unless ( $emailacctline eq $tcEmail ); 1326 | my $qused = ( $EmailPop->{humandiskused} eq 'None' ) ? $EmailPop->{diskused} : $EmailPop->{humandiskused}; 1327 | my $qlimit = ( $EmailPop->{humandiskquota} eq 'None' ) ? "Unlimited" : $EmailPop->{humandiskquota}; 1328 | my $qpercent = $EmailPop->{diskusedpercent20}; 1329 | $localpart = $EmailPop->{user}; 1330 | print_info("$emailacctline - Quota: $qused used of $qlimit [ $qpercent % ]\n"); 1331 | print_warn( RED "\t\\_ OVER QUOTA\n" ) unless ( $qpercent < 100 ); 1332 | } 1333 | my $UserFilterJSON = get_uapi( 'Email', 'list_filters', "--user=$cpUser", "account=$localpart%40$tcdomain" ); 1334 | $ShowHeader = 0; 1335 | for my $UserFilter ( @{ $UserFilterJSON->{result}->{data} } ) { 1336 | print YELLOW "\t \\_ has the following user level filters\n" unless ($ShowHeader); 1337 | $ShowHeader = 1; 1338 | print WHITE "\t\t \\_ $UserFilter->{filtername}\n"; 1339 | } 1340 | $ShowHeader = 0; 1341 | my $UserForwarderJSON = get_uapi( 'Email', 'list_forwarders', "--user=$cpUser", "account=$tcdomain" ); 1342 | for my $UserForwarder ( @{ $UserForwarderJSON->{result}->{data} } ) { 1343 | if ( $UserForwarder->{dest} eq $tcEmail ) { 1344 | print YELLOW "\t \\_ has the following user level filters\n" unless ($ShowHeader); 1345 | $ShowHeader = 1; 1346 | print WHITE "\t\\_ $UserForwarder->{dest} => $UserForwarder->{forward}\n"; 1347 | } 1348 | } 1349 | } 1350 | 1351 | sub check_closed_ports { 1352 | my $command = qx/iptables -nL | grep DROP/; 1353 | my @rules = split /\n/, $command; 1354 | my @list = grep( /.*(:25\s|:26\s|:465\s|:587\s).*/, @rules ); 1355 | if ( !@list ) { 1356 | print_info("All mail ports appear to be open\n"); 1357 | } 1358 | else { 1359 | print_info("[INFO] * Blocked ports:\n"); 1360 | foreach (@list) { 1361 | print_warn( "\t\\_ " . $_ . "\n" ); 1362 | } 1363 | } 1364 | } 1365 | 1366 | sub mx_consistency { 1367 | my $tcValue = shift; 1368 | my $isEmail = 0; 1369 | my $localpart; 1370 | my $tcdomain; 1371 | if ( $tcValue =~ /\@/ ) { 1372 | ( $localpart, $tcdomain ) = $tcValue =~ /(.*)@(.*)/; 1373 | $isEmail = 1; 1374 | } 1375 | if ($isEmail) { 1376 | $tcValue = $tcdomain; 1377 | } 1378 | my $main_ip = qx/hostname -i/; 1379 | chomp($main_ip); 1380 | my @mxcheck_local = qx/dig mx \@$main_ip $tcValue +short/; 1381 | my @mxcheck_remote = qx/dig mx \@1.1.1.1 $tcValue +short/; 1382 | if ( @mxcheck_local eq @mxcheck_remote ) { 1383 | print_info("Remote and local MX lookups match\n"); 1384 | foreach (@mxcheck_local) { 1385 | print_info("\t\\_ Local MX: $tcValue IN MX $_"); 1386 | } 1387 | print "\n"; 1388 | foreach (@mxcheck_remote) { 1389 | print_info("\t\\_ Remote MX: $tcValue IN MX $_"); 1390 | } 1391 | } 1392 | else { 1393 | print_warn("Local MX does not match remote MX\n "); 1394 | foreach (@mxcheck_local) { 1395 | chomp($_); 1396 | print "\t\\_ Local MX: $tcValue IN MX $_"; 1397 | } 1398 | print "\n"; 1399 | foreach (@mxcheck_remote) { 1400 | chomp($_); 1401 | print "\t\\_ Remote MX: $tcValue IN MX $_\n"; 1402 | } 1403 | } 1404 | } 1405 | 1406 | --------------------------------------------------------------------------------