about summary refs log tree commit diff
path: root/lib/mastodon/email_domain_blocks_cli.rb
blob: 55a637d68281c230f1b391dfd18126fb95ad9b33 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# 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 blocked e-mail domains'
    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...', 'Block e-mail domain(s)'
    long_desc <<-LONG_DESC
      Blocking an e-mail domain prevents users from signing up
      with e-mail addresses from that domain. You can provide one or
      multiple domains to the command.

      When the --with-dns-records option is given, an attempt to resolve the
      given domains' DNS records will be made and the results (A, AAAA and MX) will
      also be blocked. This can be helpful if you are blocking an e-mail server that
      has many different domains pointing to it as it allows you to essentially block
      it at the root.
    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 = 5
          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} could not be unblocked.", :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