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