osa 0.2.0 → 0.2.3
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 +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
|