osa 0.2.0 → 0.2.1

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: 1008becc098b639309f218c9f763e57d4d01f201b28fd105c478cd46534de913
4
+ data.tar.gz: 2c285065f454aeaa7fcc1f9dd6f72e92d3423dbe292d06ef36afba6a8db6f9eb
5
5
  SHA512:
6
- metadata.gz: 2a7fd8af344acc7f7839f4b9165cc39c913a9edca0e94155a68b231d773b918da69637a1c79dcd292654adf8248f30991c630f31b03588641ff1a9ca9e9bdc85
7
- data.tar.gz: 9cc8eab12bacc5010f712ed0dbb5a2bbdf056c16ec97b98e1a90bcb93e9eede72af274bb91379ef3552a11022894dd2c4da0b853d843e43a9f2e6658e086e7bb
6
+ metadata.gz: 4b53ba423fc0b0da3bc8f49188d34370cf49d12e968a13a436ffaa6c849dceddedcf10c7e2b833d7316dbc0045fe487c44891b6eb50b98fb63477f63b8542a0e
7
+ data.tar.gz: ba063539fe384116422310df5a0d3a14c324a0560674932dd39998951d433236fb929a86aae981895adff04a29d71d0780c8fc4a2a9d11fa817359ad56c10ff0
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.1)
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,6 +34,9 @@ GEM
33
34
  faraday-net_http (1.0.1)
34
35
  i18n (1.8.9)
35
36
  concurrent-ruby (~> 1.0)
37
+ mail (2.7.1)
38
+ mini_mime (>= 0.1.1)
39
+ mini_mime (1.0.2)
36
40
  minitest (5.14.3)
37
41
  multi_json (1.15.0)
38
42
  multipart-post (2.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
@@ -11,6 +11,10 @@ class DashboardServer < Sinatra::Base
11
11
  erb :index
12
12
  end
13
13
 
14
+ get '/spammers/:spammer' do
15
+ erb :spammer
16
+ end
17
+
14
18
  get '/api/stats/summary' do
15
19
  blacklist_count = OSA::Blacklist.count
16
20
  total_reported = OSA::Report.count
@@ -31,13 +35,34 @@ class DashboardServer < Sinatra::Base
31
35
 
32
36
  end
33
37
 
38
+ get '/api/stats/spammers/:spammer/summary' do |spammer|
39
+ reports = OSA::Report.where(sender: spammer).or(OSA::Report.where(sender_domain: spammer))
40
+ total_reported = reports.count
41
+ today = DateTime.now.to_date
42
+ today_reported = reports.where('reported_at > ?', today).count
43
+ week = DateTime.now - 1.week
44
+ week_reported = reports.where('reported_at > ?', week).count
45
+ month = DateTime.now - 30.days
46
+ month_reported = reports.where('reported_at > ?', month).count
47
+ domains = reports.select(:sender).distinct.map { |e| e.sender.split('@', 2)[1] }.uniq
48
+
49
+ json total_reported: total_reported,
50
+ today_reported: today_reported,
51
+ week_reported: week_reported,
52
+ month_reported: month_reported,
53
+ domains: domains
54
+ end
55
+
34
56
  get '/api/stats/spammers' do
35
57
  spammers = OSA::Report.select('sender_domain as domain', 'COUNT(*) as count').limit(50).order(count: :desc).group(:sender_domain)
36
58
  json spammers
37
59
  end
38
60
 
39
61
  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
62
+ historical_data = OSA::Report.select("strftime('%Y-%m-%d', reported_at) as date", 'count(*) as count')
63
+ unless params[:spammer].blank?
64
+ historical_data = historical_data.where(sender: params[:spammer]).or(OSA::Report.where(sender_domain: params[:spammer]))
65
+ end
66
+ json historical_data.group(:date)
42
67
  end
43
68
  end
@@ -9,6 +9,18 @@ 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
+
12
24
  while continue
13
25
  mails = context.graph_client.mails(context.config.junk_folder_id)
14
26
  continue = false
@@ -20,13 +32,12 @@ while continue
20
32
  domain = PublicSuffix.domain(email_address.split('@', 2)[1])
21
33
 
22
34
  flagged = mail['flag']['flagStatus'] == 'flagged'
23
- blacklisted = nil
24
- blacklisted = OSA::Blacklist.where(value: email_address).or(OSA::Blacklist.where(value: domain)).exists?
35
+ blacklist = (resolve_blacklist(mail['id'], email_address, domain, context, dns_blacklists) unless flagged)
25
36
 
26
37
  if flagged
27
38
  puts "Email from #{email_address} is flagged, reporting and deleting"
28
- elsif blacklisted
29
- puts "#{email_address} is blacklisted, reporting and deleting"
39
+ elsif !blacklist.nil?
40
+ puts "#{email_address} is blacklisted by #{blacklist}, reporting and deleting"
30
41
  else
31
42
  puts "Skipping mail from #{email_address}, its not blacklisted"
32
43
  next
@@ -39,8 +50,16 @@ while continue
39
50
  puts "deleting spam from #{email_address}"
40
51
  context.graph_client.delete_mail(mail['id'])
41
52
 
42
- domain = PublicSuffix.domain(email_address.split('@', 2)[1])
53
+ OSA::Report.create!(sender: email_address,
54
+ sender_domain: domain,
55
+ subject: mail['subject'],
56
+ flagged: flagged,
57
+ blacklist: blacklist,
58
+ received_at: Time.iso8601(mail['receivedDateTime']),
59
+ reported_at: Time.now)
43
60
 
61
+ # Do not add to the blacklist if the it's blacklisted by the db (it's already present)
62
+ # or blacklisted by DNSBLs (these blacklists are only supposed to be temporary).
44
63
  if flagged
45
64
  is_free_provider = OSA::EmailProvider.where(value: domain).exists?
46
65
  if is_free_provider
@@ -51,15 +70,8 @@ while continue
51
70
  OSA::Blacklist.find_or_create_by(value: domain).save!
52
71
  end
53
72
  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
73
  end
74
+
63
75
  mails = mails.next
64
76
  end
65
77
  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.1'
4
4
  end
@@ -138,16 +138,13 @@
138
138
  let totalReportCount = 0;
139
139
 
140
140
  function sec2time(timeInSeconds) {
141
- const pad = function (num, size) {
142
- return ('000' + num).slice(size * -1);
143
- }
144
141
  const time = parseFloat(timeInSeconds).toFixed(3);
145
142
  const hours = Math.floor(time / 60 / 60);
146
143
  const minutes = Math.floor(time / 60) % 60;
147
144
 
148
145
  const units = [{value: hours, unit: 'h'}, {value: minutes, unit: 'm'}].filter((value) => value.value > 0 );
149
146
 
150
- return units.map((value) => `${pad(value.value, 2)}${value.unit}`).join(' ');
147
+ return units.map((value) => `${value.value}${value.unit}`).join(' ');
151
148
  }
152
149
 
153
150
  async function updateStats() {
@@ -194,7 +191,10 @@
194
191
  tr.appendChild(indexTd);
195
192
 
196
193
  const domainTd = document.createElement("td");
197
- domainTd.innerText = spammer['domain'];
194
+ const domainRef = document.createElement("a");
195
+ domainRef.setAttribute("href", `/spammers/${encodeURI(spammer['domain'])}`);
196
+ domainRef.innerText = spammer['domain']
197
+ domainTd.appendChild(domainRef);
198
198
  tr.appendChild(domainTd);
199
199
 
200
200
  const reportCountTd = document.createElement("td");
@@ -208,7 +208,7 @@
208
208
  tr.appendChild(spamRatioProgressTd);
209
209
 
210
210
  const spamRatioTd = document.createElement("td");
211
- spamRatioTd.innerText = `${spamRatio}%`;
211
+ spamRatioTd.innerText = `${spamRatio.toFixed(1)}%`;
212
212
  tr.appendChild(spamRatioTd);
213
213
 
214
214
  return tr;
@@ -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.1
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: 2021-02-20 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