about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--lib/cli.rb4
-rw-r--r--lib/mastodon/email_domain_blocks_cli.rb133
2 files changed, 137 insertions, 0 deletions
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