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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c74cb930be462e745a7682afed8e2715e10bfb391f188d5f0f885c3b8150d435
4
- data.tar.gz: e41a5966566a34b0f48f5eb40c9df7742fe56a8aa75299ce5db1cd0e64aeaa76
3
+ metadata.gz: aa725966ad372fd1be8d324225544fe3d41075433fc2c6f5832f915600774b0d
4
+ data.tar.gz: 1ecdbe1fc443c62db5d611cbcceacb5c88e9c0777df2e0ae17a1b720f07bce18
5
5
  SHA512:
6
- metadata.gz: 2a7fd8af344acc7f7839f4b9165cc39c913a9edca0e94155a68b231d773b918da69637a1c79dcd292654adf8248f30991c630f31b03588641ff1a9ca9e9bdc85
7
- data.tar.gz: 9cc8eab12bacc5010f712ed0dbb5a2bbdf056c16ec97b98e1a90bcb93e9eede72af274bb91379ef3552a11022894dd2c4da0b853d843e43a9f2e6658e086e7bb
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.0)
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.2.1)
17
- activesupport (= 6.1.2.1)
18
- activerecord (6.1.2.1)
19
- activemodel (= 6.1.2.1)
20
- activesupport (= 6.1.2.1)
21
- activesupport (6.1.2.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
- minitest (5.14.3)
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').limit(50).order(count: :desc).group(:sender_domain)
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').group(:date)
41
- json historical_data
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['sender']['emailAddress']['address']
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
- flagged = mail['flag']['flagStatus'] == 'flagged'
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 blacklisted
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
- puts "forwarding spam from #{email_address}"
38
- context.graph_client.forward_mail_as_attachment(mail['id'], context.config.spamcop_report_email)
39
- puts "deleting spam from #{email_address}"
40
- context.graph_client.delete_mail(mail['id'])
41
-
42
- domain = PublicSuffix.domain(email_address.split('@', 2)[1])
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
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module OSA
3
- VERSION = '0.2.0'
3
+ VERSION = '0.2.3'
4
4
  end
@@ -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) => `${pad(value.value, 2)}${value.unit}`).join(' ');
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('/api/stats/spammers');
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
- domainTd.innerText = spammer['domain'];
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("/api/stats/reports/historical");
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
- .then(updateSpammers)
289
- .then(updateHistoricalChart)
290
- .then(() => {
291
- setTimeout(update, 10 * 60 * 1000);
292
- })
312
+ .then(updateSpammers)
313
+ .then(updateHistoricalChart)
314
+ .then(() => {
315
+ setTimeout(update, 10 * 60 * 1000);
316
+ })
293
317
  }
294
318
 
295
319
  update();
@@ -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="index.html" class="brand-link">
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="#" class="nav-link">
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
- <!-- Main Footer -->
42
- <footer class="main-footer">
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
@@ -28,4 +28,5 @@ Gem::Specification.new do |spec|
28
28
  spec.add_dependency 'tty-prompt', '~> 0.22'
29
29
  spec.add_dependency 'sinatra', '~> 2.1.0'
30
30
  spec.add_dependency 'sinatra-contrib', '~> 2.1.0'
31
+ spec.add_dependency 'mail', '~> 2.7.1'
31
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.2.0
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: 2021-02-14 00:00:00.000000000 Z
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.2.3
190
+ rubygems_version: 3.1.4
175
191
  signing_key:
176
192
  specification_version: 4
177
193
  summary: Outlook Spam Automator