osa 0.1.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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