diff --git a/lib/cli.rb b/lib/cli.rb index 313a36a3d..7cab0d5a1 100644 --- a/lib/cli.rb +++ b/lib/cli.rb @@ -12,6 +12,7 @@ require_relative 'mastodon/domains_cli' require_relative 'mastodon/preview_cards_cli' require_relative 'mastodon/cache_cli' require_relative 'mastodon/upgrade_cli' +require_relative 'mastodon/email_domain_blocks_cli' require_relative 'mastodon/version' module Mastodon @@ -53,6 +54,9 @@ module Mastodon desc 'upgrade SUBCOMMAND ...ARGS', 'Various version upgrade utilities' subcommand 'upgrade', Mastodon::UpgradeCLI + desc 'email-domain-blocks SUBCOMMAND ...ARGS', 'Manage E-mail domain blocks' + subcommand 'email_domain_blocks', Mastodon::EmailDomainBlocksCLI + option :dry_run, type: :boolean desc 'self-destruct', 'Erase the server from the federation' long_desc <<~LONG_DESC diff --git a/lib/mastodon/email_domain_blocks_cli.rb b/lib/mastodon/email_domain_blocks_cli.rb new file mode 100644 index 000000000..8b468ed15 --- /dev/null +++ b/lib/mastodon/email_domain_blocks_cli.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +require 'concurrent' +require_relative '../../config/boot' +require_relative '../../config/environment' +require_relative 'cli_helper' + +module Mastodon + class EmailDomainBlocksCLI < Thor + include CLIHelper + + def self.exit_on_failure? + true + end + + desc 'list', 'list E-mail domain blocks' + long_desc <<-LONG_DESC + list up all E-mail domain blocks. + LONG_DESC + def list + EmailDomainBlock.where(parent_id: nil).order(id: 'DESC').find_each do |entry| + say(entry.domain.to_s, :white) + EmailDomainBlock.where(parent_id: entry.id).order(id: 'DESC').find_each do |child| + say(" #{child.domain}", :cyan) + end + end + end + + option :with_dns_records, type: :boolean + desc 'add [DOMAIN...]', 'add E-mail domain blocks' + long_desc <<-LONG_DESC + add E-mail domain blocks from a given DOMAIN. + + When the --with-dns-records option is given, An attempt to resolve the + given domain's DNS records will be made and the results will also be + blacklisted. + LONG_DESC + def add(*domains) + if domains.empty? + say('No domain(s) given', :red) + exit(1) + end + + skipped = 0 + processed = 0 + + domains.each do |domain| + if EmailDomainBlock.where(domain: domain).exists? + say("#{domain} is already blocked.", :yellow) + skipped += 1 + next + end + + email_domain_block = EmailDomainBlock.new(domain: domain, with_dns_records: options[:with_dns_records] || false) + email_domain_block.save! + processed += 1 + + next unless email_domain_block.with_dns_records? + + hostnames = [] + ips = [] + + Resolv::DNS.open do |dns| + dns.timeouts = 1 + hostnames = dns.getresources(email_domain_block.domain, Resolv::DNS::Resource::IN::MX).to_a.map { |e| e.exchange.to_s } + + ([email_domain_block.domain] + hostnames).uniq.each do |hostname| + ips.concat(dns.getresources(hostname, Resolv::DNS::Resource::IN::A).to_a.map { |e| e.address.to_s }) + ips.concat(dns.getresources(hostname, Resolv::DNS::Resource::IN::AAAA).to_a.map { |e| e.address.to_s }) + end + end + + (hostnames + ips).uniq.each do |hostname| + another_email_domain_block = EmailDomainBlock.new(domain: hostname, parent: email_domain_block) + if EmailDomainBlock.where(domain: hostname).exists? + say("#{hostname} is already blocked.", :yellow) + skipped += 1 + next + end + another_email_domain_block.save! + processed += 1 + end + end + + say("Added #{processed}, skipped #{skipped}", color(processed, 0)) + end + + desc 'remove [DOMAIN...]', 'remove E-mail domain blocks' + def remove(*domains) + if domains.empty? + say('No domain(s) given', :red) + exit(1) + end + + skipped = 0 + processed = 0 + failed = 0 + + domains.each do |domain| + entry = EmailDomainBlock.find_by(domain: domain) + if entry.nil? + say("#{domain} is not yet blocked.", :yellow) + skipped += 1 + next + end + + children_count = EmailDomainBlock.where(parent_id: entry.id).count + + result = entry.destroy + if result + processed += 1 + children_count + else + say("#{domain} was not unblocked. 'destroy' returns false.", :red) + failed += 1 + end + end + + say("Removed #{processed}, skipped #{skipped}, failed #{failed}", color(processed, failed)) + end + + private + + def color(processed, failed) + if !processed.zero? && failed.zero? + :green + elsif failed.zero? + :yellow + else + :red + end + end + end +end