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 +4 -4
- data/Gemfile.lock +48 -23
- data/README.md +23 -12
- data/exe/osa +4 -3
- data/lib/osa/clients/ms_graph_client.rb +12 -2
- data/lib/osa/migrations/00004_create_reports.rb +16 -0
- data/lib/osa/migrations/00005_create_dns_blacklists.rb +35 -0
- data/lib/osa/scripts/dashboard_server.rb +76 -0
- data/lib/osa/scripts/scan_junk_folder.rb +56 -4
- data/lib/osa/services/setup_service.rb +1 -2
- data/lib/osa/util/db.rb +12 -0
- data/lib/osa/version.rb +1 -1
- data/lib/osa/views/index.erb +320 -0
- data/lib/osa/views/layout.erb +46 -0
- data/lib/osa/views/spammer.erb +209 -0
- data/osa.gemspec +3 -0
- metadata +51 -4
- data/lib/osa/scripts/scan_report_folder.rb +0 -32
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b3bc9c2b204260bd0cecd1650fdbfca0676c53e6e44c59aef8926caf9a5f0246
|
4
|
+
data.tar.gz: f32d6566bc6195847fc12fe680f8c22e6cbfe91f587bc7a35937efb1582eaccc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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.
|
15
|
-
activesupport (= 6.
|
16
|
-
activerecord (6.
|
17
|
-
activemodel (= 6.
|
18
|
-
activesupport (= 6.
|
19
|
-
activesupport (6.
|
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 (>=
|
22
|
-
minitest (
|
23
|
-
tzinfo (~>
|
24
|
-
zeitwerk (~> 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.
|
27
|
-
faraday (1.
|
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
|
-
|
34
|
+
faraday-net_http (1.0.1)
|
35
|
+
i18n (1.8.9)
|
31
36
|
concurrent-ruby (~> 1.0)
|
32
|
-
|
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.
|
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
|
-
|
85
|
+
tilt (2.0.10)
|
61
86
|
tty-color (0.6.0)
|
62
87
|
tty-cursor (0.7.1)
|
63
|
-
tty-prompt (0.
|
88
|
+
tty-prompt (0.23.0)
|
64
89
|
pastel (~> 0.8)
|
65
90
|
tty-reader (~> 0.8)
|
66
|
-
tty-reader (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 (
|
72
|
-
|
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.
|
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.
|
111
|
+
2.1.4
|
data/README.md
CHANGED
@@ -4,17 +4,18 @@
|
|
4
4
|
|
5
5
|
### Basics
|
6
6
|
|
7
|
-
OSA
|
8
|
-
|
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
|
-
|
12
|
-
|
13
|
-
|
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
|
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
|
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
|
69
|
+
Process your junk folder:
|
62
70
|
|
63
71
|
```sh
|
64
|
-
osa scan-
|
72
|
+
osa scan-junk
|
65
73
|
```
|
66
74
|
|
67
|
-
|
75
|
+
OSA also provides you a nice administration dashboard you. You can access the dashboard by running
|
68
76
|
|
69
77
|
```sh
|
70
|
-
osa
|
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 '
|
16
|
-
require 'osa/scripts/
|
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|
|
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'),
|
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"),
|
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
|
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
|
-
|
22
|
-
|
23
|
-
|
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,
|
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
@@ -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.
|
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:
|
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.
|
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
|