osa 0.2.0 → 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +12 -8
- data/README.md +7 -0
- data/lib/osa/clients/ms_graph_client.rb +7 -0
- data/lib/osa/migrations/00005_create_dns_blacklists.rb +35 -0
- data/lib/osa/scripts/dashboard_server.rb +37 -3
- data/lib/osa/scripts/scan_junk_folder.rb +59 -20
- data/lib/osa/util/db.rb +9 -0
- data/lib/osa/version.rb +1 -1
- data/lib/osa/views/index.erb +37 -13
- data/lib/osa/views/layout.erb +4 -13
- data/lib/osa/views/spammer.erb +209 -0
- data/osa.gemspec +1 -0
- metadata +19 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: aa725966ad372fd1be8d324225544fe3d41075433fc2c6f5832f915600774b0d
|
4
|
+
data.tar.gz: 1ecdbe1fc443c62db5d611cbcceacb5c88e9c0777df2e0ae17a1b720f07bce18
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 38eef44a8c56008bc86611994979f717b6dd029f03df021f9720ae6769a3d8be48bd6f2330d1f273867c8c295aa30fcd534949f1f0424a09082ec662ea942ebf
|
7
|
+
data.tar.gz: 0ea2067bb74bdea9220adc2fe8c0e7c53399b2e3340a5342b7ee463a6a3eaabb4c5f87a5397ca4da95182c47fa76e5bd450ea613a688321d64f0b3f5cb784009
|
data/Gemfile.lock
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
osa (0.2.
|
4
|
+
osa (0.2.3)
|
5
5
|
activerecord (~> 6.0)
|
6
6
|
faraday (~> 1.1)
|
7
|
+
mail (~> 2.7.1)
|
7
8
|
public_suffix (~> 4.0)
|
8
9
|
sinatra (~> 2.1.0)
|
9
10
|
sinatra-contrib (~> 2.1.0)
|
@@ -13,12 +14,12 @@ PATH
|
|
13
14
|
GEM
|
14
15
|
remote: https://rubygems.org/
|
15
16
|
specs:
|
16
|
-
activemodel (6.1.
|
17
|
-
activesupport (= 6.1.
|
18
|
-
activerecord (6.1.
|
19
|
-
activemodel (= 6.1.
|
20
|
-
activesupport (= 6.1.
|
21
|
-
activesupport (6.1.
|
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)
|
22
23
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
23
24
|
i18n (>= 1.6, < 2)
|
24
25
|
minitest (>= 5.1)
|
@@ -33,7 +34,10 @@ GEM
|
|
33
34
|
faraday-net_http (1.0.1)
|
34
35
|
i18n (1.8.9)
|
35
36
|
concurrent-ruby (~> 1.0)
|
36
|
-
|
37
|
+
mail (2.7.1)
|
38
|
+
mini_mime (>= 0.1.1)
|
39
|
+
mini_mime (1.0.2)
|
40
|
+
minitest (5.14.4)
|
37
41
|
multi_json (1.15.0)
|
38
42
|
multipart-post (2.1.1)
|
39
43
|
mustermann (1.1.1)
|
data/README.md
CHANGED
@@ -25,6 +25,13 @@ be blacklisted. However, to prevent millions of users to go blacklisted because
|
|
25
25
|
list of free email providers (which includes domains like gmail.com, outlook.com among others). If the sender uses a free
|
26
26
|
email provider, the full address is blacklisted.
|
27
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
|
+
|
28
35
|
## Installation
|
29
36
|
|
30
37
|
You can install OSA from RubyGems:
|
@@ -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
|
@@ -44,6 +45,12 @@ module OSA
|
|
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)
|
@@ -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
|
@@ -6,11 +6,16 @@ require 'osa/util/db'
|
|
6
6
|
class DashboardServer < Sinatra::Base
|
7
7
|
set :views, File.absolute_path(File.dirname(__FILE__) + '/../views')
|
8
8
|
set :port, ENV['SERVER_PORT'] || 8080
|
9
|
+
after { ActiveRecord::Base.connection.close }
|
9
10
|
|
10
11
|
get '/' do
|
11
12
|
erb :index
|
12
13
|
end
|
13
14
|
|
15
|
+
get '/spammers/:spammer' do
|
16
|
+
erb :spammer
|
17
|
+
end
|
18
|
+
|
14
19
|
get '/api/stats/summary' do
|
15
20
|
blacklist_count = OSA::Blacklist.count
|
16
21
|
total_reported = OSA::Report.count
|
@@ -31,13 +36,42 @@ class DashboardServer < Sinatra::Base
|
|
31
36
|
|
32
37
|
end
|
33
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] }
|
49
|
+
domains.uniq!
|
50
|
+
|
51
|
+
json total_reported: total_reported,
|
52
|
+
today_reported: today_reported,
|
53
|
+
week_reported: week_reported,
|
54
|
+
month_reported: month_reported,
|
55
|
+
domains: domains
|
56
|
+
end
|
57
|
+
|
34
58
|
get '/api/stats/spammers' do
|
35
|
-
spammers = OSA::Report.select('sender_domain as domain', 'COUNT(*) as count')
|
59
|
+
spammers = OSA::Report.select('sender_domain as domain', 'COUNT(*) as count')
|
60
|
+
unless params[:interval].blank?
|
61
|
+
spammers = spammers.where('received_at > ?', Time.now - params[:interval].to_i.days)
|
62
|
+
end
|
63
|
+
spammers = spammers.limit(50).order(count: :desc).group(:sender_domain)
|
36
64
|
json spammers
|
37
65
|
end
|
38
66
|
|
39
67
|
get '/api/stats/reports/historical' do
|
40
|
-
historical_data = OSA::Report.select("strftime('%Y-%m-%d', reported_at) as date", 'count(*) as count')
|
41
|
-
|
68
|
+
historical_data = OSA::Report.select("strftime('%Y-%m-%d', reported_at) as date", 'count(*) as count')
|
69
|
+
unless params[:spammer].blank?
|
70
|
+
historical_data = historical_data.where(sender: params[:spammer]).or(OSA::Report.where(sender_domain: params[:spammer]))
|
71
|
+
end
|
72
|
+
unless params[:interval].blank?
|
73
|
+
historical_data = historical_data.where('received_at > ?', Time.now - params[:interval].to_i.days)
|
74
|
+
end
|
75
|
+
json historical_data.group(:date)
|
42
76
|
end
|
43
77
|
end
|
@@ -9,38 +9,84 @@ context = OSA::Context.new
|
|
9
9
|
|
10
10
|
continue = true
|
11
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
|
+
# The SMTP From header might be 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
|
+
|
37
|
+
def report(mail, context, email_address)
|
38
|
+
puts "forwarding spam from #{email_address}"
|
39
|
+
context.graph_client.forward_mail_as_attachment(mail['id'], context.config.spamcop_report_email)
|
40
|
+
puts "deleting spam from #{email_address}"
|
41
|
+
context.graph_client.delete_mail(mail['id'])
|
42
|
+
end
|
43
|
+
|
12
44
|
while continue
|
13
45
|
mails = context.graph_client.mails(context.config.junk_folder_id)
|
14
46
|
continue = false
|
15
47
|
loop do
|
16
48
|
break if mails.nil?
|
17
49
|
mails['value'].each do |mail|
|
18
|
-
email_address = mail
|
50
|
+
email_address = extract_email_address(mail)
|
51
|
+
flagged = mail['flag']['flagStatus'] == 'flagged'
|
52
|
+
|
53
|
+
# Emails without a proper email address are not properly
|
54
|
+
# handled down below. Just report the mail and skip other
|
55
|
+
# procedures.
|
56
|
+
if flagged && email_address.nil?
|
57
|
+
report(mail, context, email_address)
|
58
|
+
continue = true
|
59
|
+
next
|
60
|
+
end
|
61
|
+
|
19
62
|
next if email_address.nil?
|
63
|
+
|
20
64
|
domain = PublicSuffix.domain(email_address.split('@', 2)[1])
|
21
65
|
|
22
|
-
|
23
|
-
blacklisted = nil
|
24
|
-
blacklisted = OSA::Blacklist.where(value: email_address).or(OSA::Blacklist.where(value: domain)).exists?
|
66
|
+
blacklist = (resolve_blacklist(mail['id'], email_address, domain, context, dns_blacklists) unless flagged)
|
25
67
|
|
26
68
|
if flagged
|
27
69
|
puts "Email from #{email_address} is flagged, reporting and deleting"
|
28
|
-
elsif
|
29
|
-
puts "#{email_address} is blacklisted, reporting and deleting"
|
70
|
+
elsif !blacklist.nil?
|
71
|
+
puts "#{email_address} is blacklisted by #{blacklist}, reporting and deleting"
|
30
72
|
else
|
31
73
|
puts "Skipping mail from #{email_address}, its not blacklisted"
|
32
74
|
next
|
33
75
|
end
|
34
76
|
|
77
|
+
report(mail, context, email_address)
|
35
78
|
continue = true
|
36
79
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
80
|
+
OSA::Report.create!(sender: email_address,
|
81
|
+
sender_domain: domain,
|
82
|
+
subject: mail['subject'],
|
83
|
+
flagged: flagged,
|
84
|
+
blacklist: blacklist,
|
85
|
+
received_at: Time.iso8601(mail['receivedDateTime']),
|
86
|
+
reported_at: Time.now)
|
43
87
|
|
88
|
+
# Do not add to the blacklist if it's blacklisted by the db (it's already present)
|
89
|
+
# or blacklisted by DNSBLs (these blacklists are only supposed to be temporary).
|
44
90
|
if flagged
|
45
91
|
is_free_provider = OSA::EmailProvider.where(value: domain).exists?
|
46
92
|
if is_free_provider
|
@@ -51,15 +97,8 @@ while continue
|
|
51
97
|
OSA::Blacklist.find_or_create_by(value: domain).save!
|
52
98
|
end
|
53
99
|
end
|
54
|
-
|
55
|
-
OSA::Report.create!(sender: email_address,
|
56
|
-
sender_domain: domain,
|
57
|
-
subject: mail['subject'],
|
58
|
-
flagged: flagged,
|
59
|
-
blacklisted: blacklisted,
|
60
|
-
received_at: Time.iso8601(mail['receivedDateTime']),
|
61
|
-
reported_at: Time.now)
|
62
100
|
end
|
101
|
+
|
63
102
|
mails = mails.next
|
64
103
|
end
|
65
104
|
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',
|
@@ -21,4 +22,12 @@ module OSA
|
|
21
22
|
|
22
23
|
class Report < ActiveRecord::Base
|
23
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
|
24
33
|
end
|
data/lib/osa/version.rb
CHANGED
data/lib/osa/views/index.erb
CHANGED
@@ -77,6 +77,15 @@
|
|
77
77
|
<!-- ./col -->
|
78
78
|
</div>
|
79
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
|
+
|
80
89
|
<div class="row">
|
81
90
|
<div class="card">
|
82
91
|
<div class="card-header">
|
@@ -136,18 +145,30 @@
|
|
136
145
|
|
137
146
|
<script>
|
138
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
|
+
}
|
139
163
|
|
140
164
|
function sec2time(timeInSeconds) {
|
141
|
-
const pad = function (num, size) {
|
142
|
-
return ('000' + num).slice(size * -1);
|
143
|
-
}
|
144
165
|
const time = parseFloat(timeInSeconds).toFixed(3);
|
145
166
|
const hours = Math.floor(time / 60 / 60);
|
146
167
|
const minutes = Math.floor(time / 60) % 60;
|
147
168
|
|
148
169
|
const units = [{value: hours, unit: 'h'}, {value: minutes, unit: 'm'}].filter((value) => value.value > 0 );
|
149
170
|
|
150
|
-
return units.map((value) => `${
|
171
|
+
return units.map((value) => `${value.value}${value.unit}`).join(' ');
|
151
172
|
}
|
152
173
|
|
153
174
|
async function updateStats() {
|
@@ -182,7 +203,7 @@
|
|
182
203
|
}
|
183
204
|
|
184
205
|
async function updateSpammers() {
|
185
|
-
const response = await fetch(
|
206
|
+
const response = await fetch(`/api/stats/spammers?interval=${interval}`);
|
186
207
|
if (response.ok) {
|
187
208
|
const body = await response.json();
|
188
209
|
|
@@ -194,7 +215,10 @@
|
|
194
215
|
tr.appendChild(indexTd);
|
195
216
|
|
196
217
|
const domainTd = document.createElement("td");
|
197
|
-
|
218
|
+
const domainRef = document.createElement("a");
|
219
|
+
domainRef.setAttribute("href", `/spammers/${encodeURI(spammer['domain'])}`);
|
220
|
+
domainRef.innerText = spammer['domain']
|
221
|
+
domainTd.appendChild(domainRef);
|
198
222
|
tr.appendChild(domainTd);
|
199
223
|
|
200
224
|
const reportCountTd = document.createElement("td");
|
@@ -208,7 +232,7 @@
|
|
208
232
|
tr.appendChild(spamRatioProgressTd);
|
209
233
|
|
210
234
|
const spamRatioTd = document.createElement("td");
|
211
|
-
spamRatioTd.innerText = `${spamRatio}%`;
|
235
|
+
spamRatioTd.innerText = `${spamRatio.toFixed(1)}%`;
|
212
236
|
tr.appendChild(spamRatioTd);
|
213
237
|
|
214
238
|
return tr;
|
@@ -270,7 +294,7 @@
|
|
270
294
|
const historicalChart = createHistoricalChart();
|
271
295
|
|
272
296
|
async function updateHistoricalChart() {
|
273
|
-
const response = await fetch(
|
297
|
+
const response = await fetch(`/api/stats/reports/historical?interval=${interval}`);
|
274
298
|
if (response.ok) {
|
275
299
|
const body = await response.json();
|
276
300
|
|
@@ -285,11 +309,11 @@
|
|
285
309
|
|
286
310
|
function update() {
|
287
311
|
updateStats()
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
312
|
+
.then(updateSpammers)
|
313
|
+
.then(updateHistoricalChart)
|
314
|
+
.then(() => {
|
315
|
+
setTimeout(update, 10 * 60 * 1000);
|
316
|
+
})
|
293
317
|
}
|
294
318
|
|
295
319
|
update();
|
data/lib/osa/views/layout.erb
CHANGED
@@ -13,7 +13,7 @@
|
|
13
13
|
<!-- Main Sidebar Container -->
|
14
14
|
<aside class="main-sidebar sidebar-dark-primary elevation-4">
|
15
15
|
<!-- Brand Logo -->
|
16
|
-
<a href="
|
16
|
+
<a href="/" class="brand-link">
|
17
17
|
<span class="brand-text font-weight-light">Outlook Spam Automator</span>
|
18
18
|
</a>
|
19
19
|
|
@@ -24,8 +24,7 @@
|
|
24
24
|
<nav class="mt-2">
|
25
25
|
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
|
26
26
|
<li class="nav-item">
|
27
|
-
<a href="
|
28
|
-
<i class="nav-icon fas fa-th"></i>
|
27
|
+
<a href="/" class="nav-link">
|
29
28
|
<p>Overview</p>
|
30
29
|
</a>
|
31
30
|
</li>
|
@@ -38,16 +37,8 @@
|
|
38
37
|
|
39
38
|
<%= yield %>
|
40
39
|
|
41
|
-
|
42
|
-
|
43
|
-
<!-- To the right -->
|
44
|
-
<div class="float-right d-none d-sm-inline">
|
45
|
-
Anything you want
|
46
|
-
</div>
|
47
|
-
<!-- Default to the left -->
|
48
|
-
<strong>Copyright © 2014-2020 <a href="https://adminlte.io">AdminLTE.io</a>.</strong> All rights reserved.
|
49
|
-
</footer>
|
50
|
-
<div id="sidebar-overlay"></div></div>
|
40
|
+
<div id="sidebar-overlay"></div>
|
41
|
+
</div>
|
51
42
|
<!-- ./wrapper -->
|
52
43
|
|
53
44
|
</body>
|
@@ -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
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.2.
|
4
|
+
version: 0.2.3
|
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: 2022-02-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -108,6 +108,20 @@ dependencies:
|
|
108
108
|
- - "~>"
|
109
109
|
- !ruby/object:Gem::Version
|
110
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
|
111
125
|
description: Get rid of spam on your Outlook account
|
112
126
|
email:
|
113
127
|
- contact@moraybaruh.com
|
@@ -137,6 +151,7 @@ files:
|
|
137
151
|
- lib/osa/migrations/00002_create_email_providers.rb
|
138
152
|
- lib/osa/migrations/00003_create_config.rb
|
139
153
|
- lib/osa/migrations/00004_create_reports.rb
|
154
|
+
- lib/osa/migrations/00005_create_dns_blacklists.rb
|
140
155
|
- lib/osa/migrations/free-email-providers.txt
|
141
156
|
- lib/osa/scripts/dashboard_server.rb
|
142
157
|
- lib/osa/scripts/scan_junk_folder.rb
|
@@ -149,6 +164,7 @@ files:
|
|
149
164
|
- lib/osa/version.rb
|
150
165
|
- lib/osa/views/index.erb
|
151
166
|
- lib/osa/views/layout.erb
|
167
|
+
- lib/osa/views/spammer.erb
|
152
168
|
- osa.gemspec
|
153
169
|
- release.sh
|
154
170
|
- web/login.html
|
@@ -171,7 +187,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
171
187
|
- !ruby/object:Gem::Version
|
172
188
|
version: '0'
|
173
189
|
requirements: []
|
174
|
-
rubygems_version: 3.
|
190
|
+
rubygems_version: 3.1.4
|
175
191
|
signing_key:
|
176
192
|
specification_version: 4
|
177
193
|
summary: Outlook Spam Automator
|