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 +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
|