osa 0.1.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 22c6dd2dc64261089df031e75fee841e1416276244674dce617792aacbb12fca
4
- data.tar.gz: f0442134bc13835edcbacea2aa594fdc95202e47beed2e71c29f5214bbae2335
3
+ metadata.gz: b3bc9c2b204260bd0cecd1650fdbfca0676c53e6e44c59aef8926caf9a5f0246
4
+ data.tar.gz: f32d6566bc6195847fc12fe680f8c22e6cbfe91f587bc7a35937efb1582eaccc
5
5
  SHA512:
6
- metadata.gz: ca001d8b232f9bfa8bcfde6e75be2a6e1a7b2d025254e22af2feec25625d979e3bd10a88325cee888442b5686b22c7f93fd1866225d7bb6ca38e27e9c62e05af
7
- data.tar.gz: 7d96e933b2c4b09d335f825c933ba01197399aea947e7ca2fca3f66a851c7f469f046fd37abbb59548c54fbb37835eef3684540172cf01bcf30489f2106e6208
6
+ metadata.gz: 76462c72b811ecb6b2224725ec954aa3ff3c6d2fc99a4cc91ae66698d72de709c740683ce6c4690095853199c7ef67c836ddc0bf6c0d3c4eb33751ee0fec3058
7
+ data.tar.gz: f4e79f61caa68f2c1ecd646ffa3d127070addf334f827bb33066bfdefc1e44f8b2986b6983c120c94867fa907e65184fd679ef73ad0eaa6b1b34f49956d5393d
data/Gemfile.lock CHANGED
@@ -1,42 +1,56 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- osa (0.1.1)
4
+ osa (0.2.2)
5
5
  activerecord (~> 6.0)
6
6
  faraday (~> 1.1)
7
+ mail (~> 2.7.1)
7
8
  public_suffix (~> 4.0)
9
+ sinatra (~> 2.1.0)
10
+ sinatra-contrib (~> 2.1.0)
8
11
  sqlite3 (~> 1.4)
9
12
  tty-prompt (~> 0.22)
10
13
 
11
14
  GEM
12
15
  remote: https://rubygems.org/
13
16
  specs:
14
- activemodel (6.0.3.4)
15
- activesupport (= 6.0.3.4)
16
- activerecord (6.0.3.4)
17
- activemodel (= 6.0.3.4)
18
- activesupport (= 6.0.3.4)
19
- activesupport (6.0.3.4)
17
+ activemodel (6.1.3)
18
+ activesupport (= 6.1.3)
19
+ activerecord (6.1.3)
20
+ activemodel (= 6.1.3)
21
+ activesupport (= 6.1.3)
22
+ activesupport (6.1.3)
20
23
  concurrent-ruby (~> 1.0, >= 1.0.2)
21
- i18n (>= 0.7, < 2)
22
- minitest (~> 5.1)
23
- tzinfo (~> 1.1)
24
- zeitwerk (~> 2.2, >= 2.2.2)
24
+ i18n (>= 1.6, < 2)
25
+ minitest (>= 5.1)
26
+ tzinfo (~> 2.0)
27
+ zeitwerk (~> 2.3)
25
28
  ast (2.4.1)
26
- concurrent-ruby (1.1.7)
27
- faraday (1.1.0)
29
+ concurrent-ruby (1.1.8)
30
+ faraday (1.3.0)
31
+ faraday-net_http (~> 1.0)
28
32
  multipart-post (>= 1.2, < 3)
29
33
  ruby2_keywords
30
- i18n (1.8.5)
34
+ faraday-net_http (1.0.1)
35
+ i18n (1.8.9)
31
36
  concurrent-ruby (~> 1.0)
32
- minitest (5.14.2)
37
+ mail (2.7.1)
38
+ mini_mime (>= 0.1.1)
39
+ mini_mime (1.0.2)
40
+ minitest (5.14.4)
41
+ multi_json (1.15.0)
33
42
  multipart-post (2.1.1)
43
+ mustermann (1.1.1)
44
+ ruby2_keywords (~> 0.0.1)
34
45
  parallel (1.20.0)
35
46
  parser (2.7.2.0)
36
47
  ast (~> 2.4.1)
37
48
  pastel (0.8.0)
38
49
  tty-color (~> 0.5)
39
50
  public_suffix (4.0.6)
51
+ rack (2.2.3)
52
+ rack-protection (2.1.0)
53
+ rack
40
54
  rainbow (3.0.0)
41
55
  regexp_parser (1.8.2)
42
56
  rexml (3.2.4)
@@ -55,24 +69,35 @@ GEM
55
69
  rubocop (>= 0.90.0, < 2.0)
56
70
  rubocop-ast (>= 0.4.0)
57
71
  ruby-progressbar (1.10.1)
58
- ruby2_keywords (0.0.2)
72
+ ruby2_keywords (0.0.4)
73
+ sinatra (2.1.0)
74
+ mustermann (~> 1.0)
75
+ rack (~> 2.2)
76
+ rack-protection (= 2.1.0)
77
+ tilt (~> 2.0)
78
+ sinatra-contrib (2.1.0)
79
+ multi_json
80
+ mustermann (~> 1.0)
81
+ rack-protection (= 2.1.0)
82
+ sinatra (= 2.1.0)
83
+ tilt (~> 2.0)
59
84
  sqlite3 (1.4.2)
60
- thread_safe (0.3.6)
85
+ tilt (2.0.10)
61
86
  tty-color (0.6.0)
62
87
  tty-cursor (0.7.1)
63
- tty-prompt (0.22.0)
88
+ tty-prompt (0.23.0)
64
89
  pastel (~> 0.8)
65
90
  tty-reader (~> 0.8)
66
- tty-reader (0.8.0)
91
+ tty-reader (0.9.0)
67
92
  tty-cursor (~> 0.7)
68
93
  tty-screen (~> 0.8)
69
94
  wisper (~> 2.0)
70
95
  tty-screen (0.8.1)
71
- tzinfo (1.2.8)
72
- thread_safe (~> 0.1)
96
+ tzinfo (2.0.4)
97
+ concurrent-ruby (~> 1.0)
73
98
  unicode-display_width (1.7.0)
74
99
  wisper (2.0.1)
75
- zeitwerk (2.4.1)
100
+ zeitwerk (2.4.2)
76
101
 
77
102
  PLATFORMS
78
103
  ruby
@@ -83,4 +108,4 @@ DEPENDENCIES
83
108
  rubocop-performance
84
109
 
85
110
  BUNDLED WITH
86
- 2.1.2
111
+ 2.1.4
data/README.md CHANGED
@@ -4,17 +4,18 @@
4
4
 
5
5
  ### Basics
6
6
 
7
- OSA uses asks you to choose two folders one "junk" folder and one "report" folder. Even though you can choose any folder
8
- for both, it's recommend to choose the default junk folder as the junk folder and create a custom folder for as the report
9
- folder.
7
+ OSA asks you to choose your junk folder on first configuration. After the folder is selected, the `scan-junk` command
8
+ allows you to scan the folder to report and delete any unwanted spam. It's important to chose the actual junk folder
9
+ so your Outlook spam filters does not get broken. However, OSA will work with any folder. The processing for each email
10
+ uses the following rules:
10
11
 
11
- These two folders will be used the following way:
12
- - When the report folder is scanned, all emails are reported to [Spamcop](https://spamcop.net), deleted and the senders blacklisted.
13
- - When the junk folder is scanned, all emails from the blacklist are reported to [Spamcop](https://spamcop.net) and deleted.
12
+ 1. If the email is flagged, the email is reported to Spamcop, then deleted and the sender is blacklisted.
13
+ 2. If the email's sender is blacklisted, the email is reported to Spamcop, then deleted.
14
+ 3. Otherwise, the email is left untouched.
14
15
 
15
- *OSA will not touch any folder beside these two.*
16
+ *OSA will not touch any folder beside the folder you've chosen.*
16
17
 
17
- *It's the user's responsibility to move junk mails to the report folder to build up the blacklist.*
18
+ *It's the user's responsibility to move junk mails to the junk folder and flag them to build up the blacklist.*
18
19
 
19
20
  ### The blacklist
20
21
 
@@ -24,6 +25,13 @@ be blacklisted. However, to prevent millions of users to go blacklisted because
24
25
  list of free email providers (which includes domains like gmail.com, outlook.com among others). If the sender uses a free
25
26
  email provider, the full address is blacklisted.
26
27
 
28
+ OSA also supports Domain Name System Blacklist. In fact it comes bundled with 3 DNSBL:
29
+ 1. [Spamcop Blocking List](https://www.spamcop.net/fom-serve/cache/297.html)
30
+ 2. [Spamhaus Block List](https://www.spamhaus.org/sbl)
31
+ 3. [Passive Spam Block List](https://psbl.org)
32
+
33
+ You can remove these or add more blacklists, from the database, after you configure OSA.
34
+
27
35
  ## Installation
28
36
 
29
37
  You can install OSA from RubyGems:
@@ -58,18 +66,21 @@ osa setup
58
66
 
59
67
  Each time you run this command, your previous configuration will be erased, except for your blacklist.
60
68
 
61
- Process your report folder:
69
+ Process your junk folder:
62
70
 
63
71
  ```sh
64
- osa scan-report
72
+ osa scan-junk
65
73
  ```
66
74
 
67
- Process your junk folder:
75
+ OSA also provides you a nice administration dashboard you. You can access the dashboard by running
68
76
 
69
77
  ```sh
70
- osa scan-junk
78
+ osa dashboard
71
79
  ```
72
80
 
81
+ You are now able to access the dashboard on `http://localhost:8080`. You can also change the port of the server by
82
+ providing the `SERVER_PORT` environment variable.
83
+
73
84
  ## Contributing
74
85
 
75
86
  Bug reports and pull requests are welcome on GitHub at https://github.com/moray95/osa.
data/exe/osa CHANGED
@@ -12,9 +12,10 @@ when 'login'
12
12
  OSA::AuthService.login(OSA::Config.first || OSA::Config.new)
13
13
  when 'scan-junk'
14
14
  require 'osa/scripts/scan_junk_folder'
15
- when 'scan-report'
16
- require 'osa/scripts/scan_report_folder'
15
+ when 'dashboard'
16
+ require 'osa/scripts/dashboard_server'
17
+ DashboardServer.start!
17
18
  else
18
- $stderr.puts "Usage: #{File.basename($0)} [setup|login|scan-junk|scan-report]"
19
+ $stderr.puts "Usage: #{File.basename($0)} [setup|login|scan-junk|dashboard]"
19
20
  exit 1
20
21
  end
@@ -6,6 +6,7 @@ require 'osa/util/paginated'
6
6
  require 'osa/clients/http_client'
7
7
  require 'active_support/core_ext/numeric/bytes'
8
8
  require 'active_support'
9
+ require 'mail'
9
10
 
10
11
  module OSA
11
12
  class MSGraphClient
@@ -33,21 +34,30 @@ module OSA
33
34
  end
34
35
 
35
36
  def folders
36
- Paginated.new(@authenticated.get('/v1.0/me/mailFolders'), self)
37
+ Paginated.new(@authenticated.get('/v1.0/me/mailFolders'), @authenticated)
37
38
  end
38
39
 
39
40
  def mails(folder_id)
40
- Paginated.new(@authenticated.get("/v1.0/me/mailFolders/#{folder_id}/messages"), self)
41
+ Paginated.new(@authenticated.get("/v1.0/me/mailFolders/#{folder_id}/messages"), @authenticated)
41
42
  end
42
43
 
43
44
  def raw_mail(mail_id)
44
45
  @authenticated.get("/v1.0/me/messages/#{mail_id}/$value")
45
46
  end
46
47
 
48
+ def sender_ip(mail_id)
49
+ content = raw_mail(mail_id)
50
+ mail = Mail.new(content)
51
+ mail.header['x-sender-ip']
52
+ end
53
+
47
54
  def forward_mail_as_attachment(mail_id, to)
48
55
  raw_mail = self.raw_mail(mail_id)
49
56
  forward_message = create_forward_message(mail_id)
50
57
  update = {
58
+ body: {
59
+ content: ''
60
+ },
51
61
  toRecipients: [
52
62
  {
53
63
  emailAddress: {
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ require 'active_record/migration'
3
+
4
+ class CreateReports < ActiveRecord::Migration[5.0]
5
+ def change
6
+ create_table :reports do |t|
7
+ t.text :sender, null: false
8
+ t.text :sender_domain, null: false
9
+ t.text :subject, null: false
10
+ t.boolean :flagged, null: false
11
+ t.boolean :blacklisted, null: false
12
+ t.datetime :received_at, null: false
13
+ t.datetime :reported_at, null: false
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+ require 'active_record/migration'
3
+
4
+ class CreateDnsBlacklists < ActiveRecord::Migration[5.0]
5
+ def change
6
+ create_table :dns_blacklists do |t|
7
+ t.text :name, null: false
8
+ t.text :server, null: false
9
+ end
10
+
11
+ reversible do |dir|
12
+ dir.up do
13
+ execute <<~SQL
14
+ insert into dns_blacklists (name, server) values
15
+ ('spamcop', 'bl.spamcop.net'),
16
+ ('sbl', 'sbl.spamhaus.org'),
17
+ ('psbl', 'psbl.surriel.org');
18
+ SQL
19
+ add_column :reports, :blacklist, :string, null: true
20
+ execute <<~SQL
21
+ update reports set blacklist = 'db' where blacklisted = true;
22
+ SQL
23
+ remove_column :reports, :blacklisted
24
+ end
25
+
26
+ dir.down do
27
+ add_column :reports, :blacklisted, :boolean, default: false
28
+ execute <<~SQL
29
+ update reports set blacklisted = true where blacklist = 'db';
30
+ SQL
31
+ remove_column :reports, :blacklist
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+ require 'sinatra'
3
+ require 'sinatra/json'
4
+ require 'osa/util/db'
5
+
6
+ class DashboardServer < Sinatra::Base
7
+ set :views, File.absolute_path(File.dirname(__FILE__) + '/../views')
8
+ set :port, ENV['SERVER_PORT'] || 8080
9
+ after { ActiveRecord::Base.connection.close }
10
+
11
+ get '/' do
12
+ erb :index
13
+ end
14
+
15
+ get '/spammers/:spammer' do
16
+ erb :spammer
17
+ end
18
+
19
+ get '/api/stats/summary' do
20
+ blacklist_count = OSA::Blacklist.count
21
+ total_reported = OSA::Report.count
22
+ today = DateTime.now.to_date
23
+ today_reported = OSA::Report.where('reported_at > ?', today).count
24
+ week = DateTime.now - 1.week
25
+ week_reported = OSA::Report.where('reported_at > ?', week).count
26
+ month = DateTime.now - 30.days
27
+ month_reported = OSA::Report.where('reported_at > ?', month).count
28
+ mean_report_time = OSA::Report.select('avg(julianday(reported_at) - julianday(received_at)) * 86400.0 as avg').first['avg']
29
+
30
+ json blacklist_count: blacklist_count,
31
+ total_reported: total_reported,
32
+ today_reported: today_reported,
33
+ week_reported: week_reported,
34
+ month_reported: month_reported,
35
+ mean_report_time: mean_report_time
36
+
37
+ end
38
+
39
+ get '/api/stats/spammers/:spammer/summary' do |spammer|
40
+ reports = OSA::Report.where(sender: spammer).or(OSA::Report.where(sender_domain: spammer))
41
+ total_reported = reports.count
42
+ today = DateTime.now.to_date
43
+ today_reported = reports.where('reported_at > ?', today).count
44
+ week = DateTime.now - 1.week
45
+ week_reported = reports.where('reported_at > ?', week).count
46
+ month = DateTime.now - 30.days
47
+ month_reported = reports.where('reported_at > ?', month).count
48
+ domains = reports.select(:sender).distinct.map { |e| e.sender.split('@', 2)[1] }.uniq
49
+
50
+ json total_reported: total_reported,
51
+ today_reported: today_reported,
52
+ week_reported: week_reported,
53
+ month_reported: month_reported,
54
+ domains: domains
55
+ end
56
+
57
+ get '/api/stats/spammers' do
58
+ spammers = OSA::Report.select('sender_domain as domain', 'COUNT(*) as count')
59
+ unless params[:interval].blank?
60
+ spammers = spammers.where('received_at > ?', Time.now - params[:interval].to_i.days)
61
+ end
62
+ spammers = spammers.limit(50).order(count: :desc).group(:sender_domain)
63
+ json spammers
64
+ end
65
+
66
+ get '/api/stats/reports/historical' do
67
+ historical_data = OSA::Report.select("strftime('%Y-%m-%d', reported_at) as date", 'count(*) as count')
68
+ unless params[:spammer].blank?
69
+ historical_data = historical_data.where(sender: params[:spammer]).or(OSA::Report.where(sender_domain: params[:spammer]))
70
+ end
71
+ unless params[:interval].blank?
72
+ historical_data = historical_data.where('received_at > ?', Time.now - params[:interval].to_i.days)
73
+ end
74
+ json historical_data.group(:date)
75
+ end
76
+ end
@@ -3,24 +3,54 @@ require 'osa/util/constants'
3
3
  require 'public_suffix'
4
4
  require 'osa/util/db'
5
5
  require 'osa/util/context'
6
+ require 'public_suffix'
6
7
 
7
8
  context = OSA::Context.new
8
9
 
9
10
  continue = true
10
11
 
12
+ dns_blacklists = OSA::DnsBlacklist.all
13
+
14
+ def resolve_blacklist(mail_id, email_address, domain, context, dns_blacklists)
15
+ return 'db' if OSA::Blacklist.where(value: email_address).or(OSA::Blacklist.where(value: domain)).exists?
16
+ ip = context.graph_client.sender_ip(mail_id)
17
+ if ip.nil?
18
+ puts "Couldn't detect ip for email from #{email_address}"
19
+ return nil
20
+ end
21
+ dns_blacklists.find { |bl| bl.blacklisted?(ip) }&.server
22
+ end
23
+
24
+ def extract_email_address(mail)
25
+ email_address = mail['sender']['emailAddress']['address']
26
+ return email_address unless email_address.nil?
27
+
28
+ # Sometimes the SMTP From header is misformatted and the email address is
29
+ # parsed as part of the name by Outlook. Try to extract the email address
30
+ # from the name.
31
+ sender_name = mail['sender']['emailAddress']['name']
32
+ return nil if sender_name.nil?
33
+
34
+ sender_name.scan(/<(.+@.+)>/).first&.first
35
+ end
36
+
11
37
  while continue
12
38
  mails = context.graph_client.mails(context.config.junk_folder_id)
13
39
  continue = false
14
40
  loop do
15
41
  break if mails.nil?
16
42
  mails['value'].each do |mail|
17
- email_address = mail['sender']['emailAddress']['address']
43
+ email_address = extract_email_address(mail)
18
44
  next if email_address.nil?
19
45
  domain = PublicSuffix.domain(email_address.split('@', 2)[1])
20
46
 
21
- blacklisted = OSA::Blacklist.where(value: email_address).or(OSA::Blacklist.where(value: domain)).exists?
22
- if blacklisted
23
- puts "#{email_address} is blacklisted, reporting and deleting"
47
+ flagged = mail['flag']['flagStatus'] == 'flagged'
48
+ blacklist = (resolve_blacklist(mail['id'], email_address, domain, context, dns_blacklists) unless flagged)
49
+
50
+ if flagged
51
+ puts "Email from #{email_address} is flagged, reporting and deleting"
52
+ elsif !blacklist.nil?
53
+ puts "#{email_address} is blacklisted by #{blacklist}, reporting and deleting"
24
54
  else
25
55
  puts "Skipping mail from #{email_address}, its not blacklisted"
26
56
  next
@@ -32,7 +62,29 @@ while continue
32
62
  context.graph_client.forward_mail_as_attachment(mail['id'], context.config.spamcop_report_email)
33
63
  puts "deleting spam from #{email_address}"
34
64
  context.graph_client.delete_mail(mail['id'])
65
+
66
+ OSA::Report.create!(sender: email_address,
67
+ sender_domain: domain,
68
+ subject: mail['subject'],
69
+ flagged: flagged,
70
+ blacklist: blacklist,
71
+ received_at: Time.iso8601(mail['receivedDateTime']),
72
+ reported_at: Time.now)
73
+
74
+ # Do not add to the blacklist if the it's blacklisted by the db (it's already present)
75
+ # or blacklisted by DNSBLs (these blacklists are only supposed to be temporary).
76
+ if flagged
77
+ is_free_provider = OSA::EmailProvider.where(value: domain).exists?
78
+ if is_free_provider
79
+ puts "#{email_address} is using a free provider, blacklisting full address"
80
+ OSA::Blacklist.find_or_create_by(value: email_address).save!
81
+ else
82
+ puts "Adding #{domain} to blacklist"
83
+ OSA::Blacklist.find_or_create_by(value: domain).save!
84
+ end
85
+ end
35
86
  end
87
+
36
88
  mails = mails.next
37
89
  end
38
90
  end
@@ -18,10 +18,9 @@ module OSA
18
18
  }.to_h
19
19
  prompt = TTY::Prompt.new
20
20
  junk_folder_id = prompt.select('Select junk folder:', choices)
21
- report_folder_id = prompt.select('Select report folder:', choices)
22
21
  spamcop_email = prompt.ask('Spamcop report email:') { |q| q.validate :email }
23
22
 
24
- config.update! junk_folder_id: junk_folder_id, report_folder_id: report_folder_id, spamcop_report_email: spamcop_email
23
+ config.update! junk_folder_id: junk_folder_id, spamcop_report_email: spamcop_email
25
24
  end
26
25
  end
27
26
  end
data/lib/osa/util/db.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  require 'active_record'
3
+ require 'resolv'
3
4
 
4
5
  ActiveRecord::Base.establish_connection(
5
6
  adapter: 'sqlite3',
@@ -18,4 +19,15 @@ module OSA
18
19
 
19
20
  class EmailProvider < ActiveRecord::Base
20
21
  end
22
+
23
+ class Report < ActiveRecord::Base
24
+ end
25
+
26
+ class DnsBlacklist < ActiveRecord::Base
27
+ def blacklisted?(ip)
28
+ ::Resolv.getaddress("#{ip}.#{server}") != '0.0.0.0'
29
+ rescue ::Resolv::ResolvError
30
+ return false
31
+ end
32
+ end
21
33
  end
data/lib/osa/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module OSA
3
- VERSION = '0.1.1'
3
+ VERSION = '0.2.2'
4
4
  end
@@ -0,0 +1,320 @@
1
+ <!-- Content Wrapper. Contains page content -->
2
+ <div class="content-wrapper" style="min-height: 1233px;">
3
+ <!-- Content Header (Page header) -->
4
+ <div class="content-header">
5
+ <div class="container-fluid">
6
+ <div class="row mb-2">
7
+ <div class="col-sm-6">
8
+ <h1 class="m-0">Overview</h1>
9
+ </div><!-- /.col -->
10
+ </div><!-- /.row -->
11
+ </div><!-- /.container-fluid -->
12
+ </div>
13
+ <!-- /.content-header -->
14
+
15
+ <!-- Main content -->
16
+ <div class="content">
17
+ <div class="container-fluid">
18
+ <div class="row">
19
+ <div class="col-lg">
20
+ <!-- small card -->
21
+ <div class="small-box bg-info">
22
+ <div class="inner">
23
+ <h3 id="blacklist-count"></h3>
24
+ <p>entries in blacklist</p>
25
+ </div>
26
+ </div>
27
+ </div>
28
+ <div class="col-lg">
29
+ <!-- small card -->
30
+ <div class="small-box bg-info">
31
+ <div class="inner">
32
+ <h3 id="reporting-time"></h3>
33
+ <p>average reporting time</p>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ <!-- ./col -->
38
+ <div class="col-lg">
39
+ <!-- small card -->
40
+ <div class="small-box bg-success">
41
+ <div class="inner">
42
+ <h3 id="today-count"></h3>
43
+ <p>emails reported today</p>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ <!-- ./col -->
48
+ <div class="col-lg">
49
+ <!-- small card -->
50
+ <div class="small-box bg-success">
51
+ <div class="inner">
52
+ <h3 id="week-count"></h3>
53
+ <p>emails reported in the last 7 days</p>
54
+ </div>
55
+ </div>
56
+ </div>
57
+ <!-- ./col -->
58
+ <div class="col-lg">
59
+ <!-- small card -->
60
+ <div class="small-box bg-success">
61
+ <div class="inner">
62
+ <h3 id="month-count"></h3>
63
+ <p>emails reported in the last 30 days</p>
64
+ </div>
65
+ </div>
66
+ </div>
67
+ <!-- ./col -->
68
+ <div class="col-lg">
69
+ <!-- small card -->
70
+ <div class="small-box bg-warning">
71
+ <div class="inner">
72
+ <h3 id="total-count"></h3>
73
+ <p>emails reported in total</p>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ <!-- ./col -->
78
+ </div>
79
+
80
+ <div class="row">
81
+ <div id="interval-btn-group" class="btn-group">
82
+ <button onclick="updateInterval(this, '')" type="button" class="btn btn-info active">All time</button>
83
+ <button onclick="updateInterval(this, '1')" type="button" class="btn btn-info">1 day</button>
84
+ <button onclick="updateInterval(this, '7')" type="button" class="btn btn-info">7 days</button>
85
+ <button onclick="updateInterval(this, '30')" type="button" class="btn btn-info">30 days</button>
86
+ </div>
87
+ </div>
88
+
89
+ <div class="row">
90
+ <div class="card">
91
+ <div class="card-header">
92
+ <h3 class="card-title">Spams reported</h3>
93
+ </div>
94
+ <!-- /.card-header -->
95
+ <div class="card-body">
96
+ <div class="chart">
97
+ <div class="chartjs-size-monitor">
98
+ <div class="chartjs-size-monitor-expand">
99
+ <div class=""></div>
100
+ </div>
101
+ <div class="chartjs-size-monitor-shrink">
102
+ <div class=""></div>
103
+ </div>
104
+ </div>
105
+ <canvas id="report-history-chart" style="min-height: 250px; height: 250px; max-height: 250px; max-width: 100%; display: block; width: 1732px;" width="1732" height="250" class="chartjs-render-monitor"></canvas>
106
+ </div>
107
+ </div>
108
+ <!-- /.card-body -->
109
+ </div>
110
+ </div>
111
+
112
+ <div class="row">
113
+ <div class="card">
114
+ <div class="card-header">
115
+ <h3 class="card-title">Top spammers</h3>
116
+ </div>
117
+ <!-- /.card-header -->
118
+ <div class="card-body p-0">
119
+ <table class="table table-striped">
120
+ <thead>
121
+ <tr>
122
+ <th style="width: 10px">#</th>
123
+ <th>Domain</th>
124
+ <th>Emails reported</th>
125
+ <th>Spam ratio</th>
126
+ <th style="width: 40px"></th>
127
+ </tr>
128
+ </thead>
129
+ <tbody id="top-spammers-table-body">
130
+ </tbody>
131
+ </table>
132
+ </div>
133
+ <!-- /.card-body -->
134
+ </div>
135
+ </div>
136
+
137
+
138
+ </div> <!-- /.container-fluid -->
139
+
140
+
141
+ </div>
142
+ <!-- /.content -->
143
+ </div>
144
+ <!-- /.content-wrapper -->
145
+
146
+ <script>
147
+ let totalReportCount = 0;
148
+ let interval = "";
149
+
150
+ function updateInterval(element, newInterval) {
151
+ interval = newInterval;
152
+ let children = Array.from(document.getElementById("interval-btn-group").children);
153
+ console.log(children);
154
+ children.forEach((child) => {
155
+ if (child === element) {
156
+ child.classList.add("active");
157
+ } else {
158
+ child.classList.remove("active");
159
+ }
160
+ });
161
+ update();
162
+ }
163
+
164
+ function sec2time(timeInSeconds) {
165
+ const time = parseFloat(timeInSeconds).toFixed(3);
166
+ const hours = Math.floor(time / 60 / 60);
167
+ const minutes = Math.floor(time / 60) % 60;
168
+
169
+ const units = [{value: hours, unit: 'h'}, {value: minutes, unit: 'm'}].filter((value) => value.value > 0 );
170
+
171
+ return units.map((value) => `${value.value}${value.unit}`).join(' ');
172
+ }
173
+
174
+ async function updateStats() {
175
+ const response = await fetch('/api/stats/summary');
176
+ if (response.ok) {
177
+ const body = await response.json();
178
+ document.getElementById('blacklist-count').innerText = body['blacklist_count'];
179
+ document.getElementById('today-count').innerText = body['today_reported'];
180
+ document.getElementById('week-count').innerText = body['week_reported'];
181
+ document.getElementById('month-count').innerText = body['month_reported'];
182
+ document.getElementById('total-count').innerText = body['total_reported'];
183
+ const meanReportTime = body['mean_report_time'];
184
+ if (meanReportTime !== null) {
185
+ document.getElementById('reporting-time').innerText = sec2time(meanReportTime);
186
+ } else {
187
+ document.getElementById('reporting-time').innerText = 'N/A';
188
+ }
189
+ totalReportCount = body['total_reported'];
190
+ }
191
+ }
192
+
193
+ function createProgressDiv(fillRatio) {
194
+ const progressDiv = document.createElement("div");
195
+ progressDiv.className = "progress progress-xs";
196
+
197
+ const progressBarDiv = document.createElement("div");
198
+ progressBarDiv.className = "progress-bar progress-bar-danger";
199
+ progressBarDiv.style.width = `${fillRatio}%`;
200
+
201
+ progressDiv.appendChild(progressBarDiv);
202
+ return progressDiv;
203
+ }
204
+
205
+ async function updateSpammers() {
206
+ const response = await fetch(`/api/stats/spammers?interval=${interval}`);
207
+ if (response.ok) {
208
+ const body = await response.json();
209
+
210
+ const table = body.map((spammer, index) => {
211
+ const tr = document.createElement("tr");
212
+
213
+ const indexTd = document.createElement("td");
214
+ indexTd.innerText = index + 1;
215
+ tr.appendChild(indexTd);
216
+
217
+ const domainTd = document.createElement("td");
218
+ const domainRef = document.createElement("a");
219
+ domainRef.setAttribute("href", `/spammers/${encodeURI(spammer['domain'])}`);
220
+ domainRef.innerText = spammer['domain']
221
+ domainTd.appendChild(domainRef);
222
+ tr.appendChild(domainTd);
223
+
224
+ const reportCountTd = document.createElement("td");
225
+ reportCountTd.innerText = spammer['count'];
226
+ tr.appendChild(reportCountTd);
227
+
228
+ const spamRatio = spammer['count'] / totalReportCount * 100;
229
+
230
+ const spamRatioProgressTd = document.createElement("td");
231
+ spamRatioProgressTd.append(createProgressDiv(spamRatio));
232
+ tr.appendChild(spamRatioProgressTd);
233
+
234
+ const spamRatioTd = document.createElement("td");
235
+ spamRatioTd.innerText = `${spamRatio.toFixed(1)}%`;
236
+ tr.appendChild(spamRatioTd);
237
+
238
+ return tr;
239
+ });
240
+ const tableBody = document.getElementById("top-spammers-table-body")
241
+ tableBody.innerHTML = "";
242
+ table.forEach((row) => {
243
+ tableBody.appendChild(row);
244
+ });
245
+
246
+ }
247
+ }
248
+
249
+ function createHistoricalChart() {
250
+ const options = {
251
+ maintainAspectRatio: false,
252
+ responsive: true,
253
+ legend: {
254
+ display: false
255
+ },
256
+ scales: {
257
+ xAxes: [{
258
+ gridLines: {
259
+ display: false,
260
+ }
261
+ }],
262
+ yAxes: [{
263
+ gridLines: {
264
+ display: false,
265
+ }
266
+ }]
267
+ }
268
+ }
269
+
270
+ const chartData = {
271
+ labels: [],
272
+ datasets: [
273
+ {
274
+ label: 'Spams reported',
275
+ backgroundColor: 'rgba(60,141,188,0.9)',
276
+ borderColor: 'rgba(60,141,188,0.8)',
277
+ pointColor: '#3b8bba',
278
+ pointStrokeColor: 'rgba(60,141,188,1)',
279
+ pointHighlightFill: '#fff',
280
+ pointHighlightStroke: 'rgba(60,141,188,1)',
281
+ data: []
282
+ }
283
+ ]
284
+ }
285
+ const ctx = document.getElementById('report-history-chart').getContext('2d');
286
+
287
+ return new Chart(ctx, {
288
+ type: 'line',
289
+ options: options,
290
+ data: chartData
291
+ });
292
+ }
293
+
294
+ const historicalChart = createHistoricalChart();
295
+
296
+ async function updateHistoricalChart() {
297
+ const response = await fetch(`/api/stats/reports/historical?interval=${interval}`);
298
+ if (response.ok) {
299
+ const body = await response.json();
300
+
301
+ const labels = body.map((data) => data['date']);
302
+ const values = body.map((data) => data['count']);
303
+
304
+ historicalChart.data.labels = labels
305
+ historicalChart.data.datasets[0].data = values
306
+ historicalChart.update();
307
+ }
308
+ }
309
+
310
+ function update() {
311
+ updateStats()
312
+ .then(updateSpammers)
313
+ .then(updateHistoricalChart)
314
+ .then(() => {
315
+ setTimeout(update, 10 * 60 * 1000);
316
+ })
317
+ }
318
+
319
+ update();
320
+ </script>
@@ -0,0 +1,46 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Outlook Spam Automator Admin Panel</title>
6
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous">
7
+ <script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
8
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.min.js" integrity="sha512-d9xgZrVZpmmQlfonhQUvTR7lMPtO7NkZMkA0ABN3PHCbKA5nqylQ/yWlFAyY6hYgdF1Qh6nYiuADWwKB4C2WSw==" crossorigin="anonymous"></script>
9
+ <script src="https://cdn.jsdelivr.net/npm/admin-lte@3.0/dist/js/adminlte.min.js" crossorigin="anonymous"></script>
10
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/admin-lte@3.0/dist/css/adminlte.min.css">
11
+ <body class="sidebar-mini" style="height: auto;">
12
+ <div class="wrapper">
13
+ <!-- Main Sidebar Container -->
14
+ <aside class="main-sidebar sidebar-dark-primary elevation-4">
15
+ <!-- Brand Logo -->
16
+ <a href="/" class="brand-link">
17
+ <span class="brand-text font-weight-light">Outlook Spam Automator</span>
18
+ </a>
19
+
20
+ <!-- Sidebar -->
21
+ <div class="sidebar">
22
+
23
+ <!-- Sidebar Menu -->
24
+ <nav class="mt-2">
25
+ <ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
26
+ <li class="nav-item">
27
+ <a href="/" class="nav-link">
28
+ <p>Overview</p>
29
+ </a>
30
+ </li>
31
+ </ul>
32
+ </nav>
33
+ <!-- /.sidebar-menu -->
34
+ </div>
35
+ <!-- /.sidebar -->
36
+ </aside>
37
+
38
+ <%= yield %>
39
+
40
+ <div id="sidebar-overlay"></div>
41
+ </div>
42
+ <!-- ./wrapper -->
43
+
44
+ </body>
45
+
46
+ </html>
@@ -0,0 +1,209 @@
1
+ <!-- Content Wrapper. Contains page content -->
2
+ <div class="content-wrapper" style="min-height: 1233px;">
3
+ <!-- Content Header (Page header) -->
4
+ <div class="content-header">
5
+ <div class="container-fluid">
6
+ <div class="row mb-2">
7
+ <div class="col-sm-6">
8
+ <h1 id="title" class="m-0">Spammer Details</h1>
9
+ </div><!-- /.col -->
10
+ </div><!-- /.row -->
11
+ </div><!-- /.container-fluid -->
12
+ </div>
13
+ <!-- /.content-header -->
14
+
15
+ <!-- Main content -->
16
+ <div class="content">
17
+ <div class="container-fluid">
18
+ <div class="row">
19
+ <!-- ./col -->
20
+ <div class="col-lg">
21
+ <!-- small card -->
22
+ <div class="small-box bg-success">
23
+ <div class="inner">
24
+ <h3 id="today-count"></h3>
25
+ <p>emails reported today</p>
26
+ </div>
27
+ </div>
28
+ </div>
29
+ <!-- ./col -->
30
+ <div class="col-lg">
31
+ <!-- small card -->
32
+ <div class="small-box bg-success">
33
+ <div class="inner">
34
+ <h3 id="week-count"></h3>
35
+ <p>emails reported in the last 7 days</p>
36
+ </div>
37
+ </div>
38
+ </div>
39
+ <!-- ./col -->
40
+ <div class="col-lg">
41
+ <!-- small card -->
42
+ <div class="small-box bg-success">
43
+ <div class="inner">
44
+ <h3 id="month-count"></h3>
45
+ <p>emails reported in the last 30 days</p>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ <!-- ./col -->
50
+ <div class="col-lg">
51
+ <!-- small card -->
52
+ <div class="small-box bg-warning">
53
+ <div class="inner">
54
+ <h3 id="total-count"></h3>
55
+ <p>emails reported in total</p>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ <!-- ./col -->
60
+ </div>
61
+
62
+ <div class="row">
63
+ <div class="card">
64
+ <div class="card-header">
65
+ <h3 class="card-title">Spams reported</h3>
66
+ </div>
67
+ <!-- /.card-header -->
68
+ <div class="card-body">
69
+ <div class="chart">
70
+ <div class="chartjs-size-monitor">
71
+ <div class="chartjs-size-monitor-expand">
72
+ <div class=""></div>
73
+ </div>
74
+ <div class="chartjs-size-monitor-shrink">
75
+ <div class=""></div>
76
+ </div>
77
+ </div>
78
+ <canvas id="report-history-chart" style="min-height: 250px; height: 250px; max-height: 250px; max-width: 100%; display: block; width: 1732px;" width="1732" height="250" class="chartjs-render-monitor"></canvas>
79
+ </div>
80
+ </div>
81
+ <!-- /.card-body -->
82
+ </div>
83
+ </div>
84
+
85
+ <div class="row">
86
+ <div class="card">
87
+ <div class="card-header">
88
+ <h3 class="card-title">Domains</h3>
89
+ </div>
90
+ <!-- /.card-header -->
91
+ <div class="card-body p-0">
92
+ <table class="table table-striped">
93
+ <thead>
94
+ </thead>
95
+ <tbody id="domains-table-body">
96
+ </tbody>
97
+ </table>
98
+ </div>
99
+ <!-- /.card-body -->
100
+ </div>
101
+ </div>
102
+
103
+
104
+ </div> <!-- /.container-fluid -->
105
+ </div>
106
+ </div>
107
+ <!-- /.content -->
108
+ </div>
109
+ <!-- /.content-wrapper -->
110
+
111
+ <script>
112
+ const pathComponents = window.location.pathname.split('/');
113
+ const spammer = pathComponents[pathComponents.length - 1];
114
+ document.getElementById("title").innerText = `${spammer} Details`;
115
+
116
+ async function updateStats() {
117
+ const response = await fetch(`/api/stats/spammers/${spammer}/summary`);
118
+ if (response.ok) {
119
+ const body = await response.json();
120
+ document.getElementById('today-count').innerText = body['today_reported'];
121
+ document.getElementById('week-count').innerText = body['week_reported'];
122
+ document.getElementById('month-count').innerText = body['month_reported'];
123
+ document.getElementById('total-count').innerText = body['total_reported'];
124
+ totalReportCount = body['total_reported'];
125
+
126
+ const tableBody = document.getElementById("domains-table-body");
127
+ tableBody.innerHTML = "";
128
+
129
+ body['domains'].forEach((domain) => {
130
+ const tr = document.createElement("tr");
131
+ const td = document.createElement("td");
132
+ td.innerText = domain;
133
+ tr.appendChild(td);
134
+ tableBody.appendChild(tr);
135
+ });
136
+ }
137
+ }
138
+
139
+ function createHistoricalChart() {
140
+ const options = {
141
+ maintainAspectRatio: false,
142
+ responsive: true,
143
+ legend: {
144
+ display: false
145
+ },
146
+ scales: {
147
+ xAxes: [{
148
+ gridLines: {
149
+ display: false,
150
+ }
151
+ }],
152
+ yAxes: [{
153
+ gridLines: {
154
+ display: false,
155
+ }
156
+ }]
157
+ }
158
+ }
159
+
160
+ const chartData = {
161
+ labels: [],
162
+ datasets: [
163
+ {
164
+ label: 'Spams reported',
165
+ backgroundColor: 'rgba(60,141,188,0.9)',
166
+ borderColor: 'rgba(60,141,188,0.8)',
167
+ pointColor: '#3b8bba',
168
+ pointStrokeColor: 'rgba(60,141,188,1)',
169
+ pointHighlightFill: '#fff',
170
+ pointHighlightStroke: 'rgba(60,141,188,1)',
171
+ data: []
172
+ }
173
+ ]
174
+ }
175
+ const ctx = document.getElementById('report-history-chart').getContext('2d');
176
+
177
+ return new Chart(ctx, {
178
+ type: 'line',
179
+ options: options,
180
+ data: chartData
181
+ });
182
+ }
183
+
184
+ const historicalChart = createHistoricalChart();
185
+
186
+ async function updateHistoricalChart() {
187
+ const response = await fetch(`/api/stats/reports/historical?spammer=${spammer}`);
188
+ if (response.ok) {
189
+ const body = await response.json();
190
+
191
+ const labels = body.map((data) => data['date']);
192
+ const values = body.map((data) => data['count']);
193
+
194
+ historicalChart.data.labels = labels
195
+ historicalChart.data.datasets[0].data = values
196
+ historicalChart.update();
197
+ }
198
+ }
199
+
200
+ function update() {
201
+ updateStats()
202
+ .then(updateHistoricalChart)
203
+ .then(() => {
204
+ setTimeout(update, 10 * 60 * 1000);
205
+ })
206
+ }
207
+
208
+ update();
209
+ </script>
data/osa.gemspec CHANGED
@@ -26,4 +26,7 @@ Gem::Specification.new do |spec|
26
26
  spec.add_dependency 'public_suffix', '~> 4.0'
27
27
  spec.add_dependency 'sqlite3', '~> 1.4'
28
28
  spec.add_dependency 'tty-prompt', '~> 0.22'
29
+ spec.add_dependency 'sinatra', '~> 2.1.0'
30
+ spec.add_dependency 'sinatra-contrib', '~> 2.1.0'
31
+ spec.add_dependency 'mail', '~> 2.7.1'
29
32
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: osa
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Moray Baruh
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-11-28 00:00:00.000000000 Z
11
+ date: 2021-03-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -80,6 +80,48 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0.22'
83
+ - !ruby/object:Gem::Dependency
84
+ name: sinatra
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 2.1.0
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 2.1.0
97
+ - !ruby/object:Gem::Dependency
98
+ name: sinatra-contrib
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 2.1.0
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 2.1.0
111
+ - !ruby/object:Gem::Dependency
112
+ name: mail
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 2.7.1
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 2.7.1
83
125
  description: Get rid of spam on your Outlook account
84
126
  email:
85
127
  - contact@moraybaruh.com
@@ -108,9 +150,11 @@ files:
108
150
  - lib/osa/migrations/00001_create_blacklists.rb
109
151
  - lib/osa/migrations/00002_create_email_providers.rb
110
152
  - lib/osa/migrations/00003_create_config.rb
153
+ - lib/osa/migrations/00004_create_reports.rb
154
+ - lib/osa/migrations/00005_create_dns_blacklists.rb
111
155
  - lib/osa/migrations/free-email-providers.txt
156
+ - lib/osa/scripts/dashboard_server.rb
112
157
  - lib/osa/scripts/scan_junk_folder.rb
113
- - lib/osa/scripts/scan_report_folder.rb
114
158
  - lib/osa/services/auth_service.rb
115
159
  - lib/osa/services/setup_service.rb
116
160
  - lib/osa/util/constants.rb
@@ -118,6 +162,9 @@ files:
118
162
  - lib/osa/util/db.rb
119
163
  - lib/osa/util/paginated.rb
120
164
  - lib/osa/version.rb
165
+ - lib/osa/views/index.erb
166
+ - lib/osa/views/layout.erb
167
+ - lib/osa/views/spammer.erb
121
168
  - osa.gemspec
122
169
  - release.sh
123
170
  - web/login.html
@@ -140,7 +187,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
140
187
  - !ruby/object:Gem::Version
141
188
  version: '0'
142
189
  requirements: []
143
- rubygems_version: 3.1.2
190
+ rubygems_version: 3.1.4
144
191
  signing_key:
145
192
  specification_version: 4
146
193
  summary: Outlook Spam Automator
@@ -1,32 +0,0 @@
1
- # frozen_string_literal: true
2
- require 'uri'
3
- require 'public_suffix'
4
- require 'osa/util/db'
5
- require 'osa/util/context'
6
-
7
- context = OSA::Context.new
8
-
9
- loop do
10
- mails = context.graph_client.mails(context.config.report_folder_id)
11
- break if mails['value'].empty?
12
- mails['value'].each do |mail|
13
- email_address = mail['sender']['emailAddress']['address']
14
-
15
- puts "forwarding spam from #{email_address}"
16
- context.graph_client.forward_mail_as_attachment(mail['id'], context.config.spamcop_report_email)
17
- puts "deleting spam from #{email_address}"
18
- context.graph_client.delete_mail(mail['id'])
19
-
20
- next if email_address.nil?
21
- domain = PublicSuffix.domain(email_address.split('@', 2)[1])
22
-
23
- is_free_provider = OSA::EmailProvider.where(value: domain).exists?
24
- if is_free_provider
25
- puts "#{email_address} is using a free provider, blacklisting full address"
26
- OSA::Blacklist.find_or_create_by(value: email_address).save!
27
- else
28
- puts "Adding #{domain} to blacklist"
29
- OSA::Blacklist.find_or_create_by(value: domain).save!
30
- end
31
- end
32
- end