osa 0.1.0 → 0.2.1

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: d4ad6f3864c9196aff9026ad350f71c7904c27817dbd0217bf6008885830ac15
4
- data.tar.gz: fe44a6e9d98590eb231c04c794327ca1132feaf9d13d556563fe900ec5bce190
3
+ metadata.gz: 1008becc098b639309f218c9f763e57d4d01f201b28fd105c478cd46534de913
4
+ data.tar.gz: 2c285065f454aeaa7fcc1f9dd6f72e92d3423dbe292d06ef36afba6a8db6f9eb
5
5
  SHA512:
6
- metadata.gz: c9dfa0f4cf44acf3ca08654f670cdca47a7a9ae7c681dfea0b9b9ddb6acf064a9b88c98dd325e449e9efa23647fc663cb407a4ec19a1fdd4e86df1a556fe467e
7
- data.tar.gz: 1f32dbf2f0cbf9373788100ca8f99579be14732cf86c91ce45c443d9ae67476d762eae81b7b9d3b5243b97011dd066df713f72dd8002ab53d32d9e27f145999f
6
+ metadata.gz: 4b53ba423fc0b0da3bc8f49188d34370cf49d12e968a13a436ffaa6c849dceddedcf10c7e2b833d7316dbc0045fe487c44891b6eb50b98fb63477f63b8542a0e
7
+ data.tar.gz: ba063539fe384116422310df5a0d3a14c324a0560674932dd39998951d433236fb929a86aae981895adff04a29d71d0780c8fc4a2a9d11fa817359ad56c10ff0
data/.rubocop.yml CHANGED
@@ -6,6 +6,7 @@ AllCops:
6
6
  DisabledByDefault: true
7
7
  Exclude:
8
8
  - '**/vendor/**/*'
9
+ - 'bin/**'
9
10
 
10
11
  Bundler/DuplicatedGem:
11
12
  Enabled: true
data/Gemfile CHANGED
@@ -1,4 +1,5 @@
1
- source "https://rubygems.org"
1
+ # frozen_string_literal: true
2
+ source 'https://rubygems.org'
2
3
 
3
4
  gemspec
4
5
 
data/Gemfile.lock CHANGED
@@ -1,42 +1,56 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- osa (0.1.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)
9
+ sinatra (~> 2.1.0)
10
+ sinatra-contrib (~> 2.1.0)
8
11
  sqlite3 (~> 1.4)
9
12
  tty-prompt (~> 0.22)
10
13
 
11
14
  GEM
12
15
  remote: https://rubygems.org/
13
16
  specs:
14
- activemodel (6.0.3.4)
15
- activesupport (= 6.0.3.4)
16
- activerecord (6.0.3.4)
17
- activemodel (= 6.0.3.4)
18
- activesupport (= 6.0.3.4)
19
- activesupport (6.0.3.4)
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)
20
23
  concurrent-ruby (~> 1.0, >= 1.0.2)
21
- i18n (>= 0.7, < 2)
22
- minitest (~> 5.1)
23
- tzinfo (~> 1.1)
24
- zeitwerk (~> 2.2, >= 2.2.2)
24
+ i18n (>= 1.6, < 2)
25
+ minitest (>= 5.1)
26
+ tzinfo (~> 2.0)
27
+ zeitwerk (~> 2.3)
25
28
  ast (2.4.1)
26
- concurrent-ruby (1.1.7)
27
- faraday (1.1.0)
29
+ concurrent-ruby (1.1.8)
30
+ faraday (1.3.0)
31
+ faraday-net_http (~> 1.0)
28
32
  multipart-post (>= 1.2, < 3)
29
33
  ruby2_keywords
30
- i18n (1.8.5)
34
+ faraday-net_http (1.0.1)
35
+ i18n (1.8.9)
31
36
  concurrent-ruby (~> 1.0)
32
- minitest (5.14.2)
37
+ mail (2.7.1)
38
+ mini_mime (>= 0.1.1)
39
+ mini_mime (1.0.2)
40
+ minitest (5.14.3)
41
+ multi_json (1.15.0)
33
42
  multipart-post (2.1.1)
43
+ mustermann (1.1.1)
44
+ ruby2_keywords (~> 0.0.1)
34
45
  parallel (1.20.0)
35
46
  parser (2.7.2.0)
36
47
  ast (~> 2.4.1)
37
48
  pastel (0.8.0)
38
49
  tty-color (~> 0.5)
39
50
  public_suffix (4.0.6)
51
+ rack (2.2.3)
52
+ rack-protection (2.1.0)
53
+ rack
40
54
  rainbow (3.0.0)
41
55
  regexp_parser (1.8.2)
42
56
  rexml (3.2.4)
@@ -55,24 +69,35 @@ GEM
55
69
  rubocop (>= 0.90.0, < 2.0)
56
70
  rubocop-ast (>= 0.4.0)
57
71
  ruby-progressbar (1.10.1)
58
- ruby2_keywords (0.0.2)
72
+ ruby2_keywords (0.0.4)
73
+ sinatra (2.1.0)
74
+ mustermann (~> 1.0)
75
+ rack (~> 2.2)
76
+ rack-protection (= 2.1.0)
77
+ tilt (~> 2.0)
78
+ sinatra-contrib (2.1.0)
79
+ multi_json
80
+ mustermann (~> 1.0)
81
+ rack-protection (= 2.1.0)
82
+ sinatra (= 2.1.0)
83
+ tilt (~> 2.0)
59
84
  sqlite3 (1.4.2)
60
- thread_safe (0.3.6)
85
+ tilt (2.0.10)
61
86
  tty-color (0.6.0)
62
87
  tty-cursor (0.7.1)
63
- tty-prompt (0.22.0)
88
+ tty-prompt (0.23.0)
64
89
  pastel (~> 0.8)
65
90
  tty-reader (~> 0.8)
66
- tty-reader (0.8.0)
91
+ tty-reader (0.9.0)
67
92
  tty-cursor (~> 0.7)
68
93
  tty-screen (~> 0.8)
69
94
  wisper (~> 2.0)
70
95
  tty-screen (0.8.1)
71
- tzinfo (1.2.8)
72
- thread_safe (~> 0.1)
96
+ tzinfo (2.0.4)
97
+ concurrent-ruby (~> 1.0)
73
98
  unicode-display_width (1.7.0)
74
99
  wisper (2.0.1)
75
- zeitwerk (2.4.1)
100
+ zeitwerk (2.4.2)
76
101
 
77
102
  PLATFORMS
78
103
  ruby
@@ -83,4 +108,4 @@ DEPENDENCIES
83
108
  rubocop-performance
84
109
 
85
110
  BUNDLED WITH
86
- 2.1.2
111
+ 2.1.4
data/README.md CHANGED
@@ -4,17 +4,18 @@
4
4
 
5
5
  ### Basics
6
6
 
7
- OSA uses asks you to choose two folders one "junk" folder and one "report" folder. Even though you can choose any folder
8
- for both, it's recommend to choose the default junk folder as the junk folder and create a custom folder for as the report
9
- folder.
7
+ OSA asks you to choose your junk folder on first configuration. After the folder is selected, the `scan-junk` command
8
+ allows you to scan the folder to report and delete any unwanted spam. It's important to chose the actual junk folder
9
+ so your Outlook spam filters does not get broken. However, OSA will work with any folder. The processing for each email
10
+ uses the following rules:
10
11
 
11
- These two folders will be used the following way:
12
- - When the report folder is scanned, all emails are reported to [Spamcop](https://spamcop.net), deleted and the senders blacklisted.
13
- - When the junk folder is scanned, all emails from the blacklist are reported to [Spamcop](https://spamcop.net) and deleted.
12
+ 1. If the email is flagged, the email is reported to Spamcop, then deleted and the sender is blacklisted.
13
+ 2. If the email's sender is blacklisted, the email is reported to Spamcop, then deleted.
14
+ 3. Otherwise, the email is left untouched.
14
15
 
15
- *OSA will not touch any folder beside these two.*
16
+ *OSA will not touch any folder beside the folder you've chosen.*
16
17
 
17
- *It's the user's responsibility to move junk mails to the report folder to build up the blacklist.*
18
+ *It's the user's responsibility to move junk mails to the junk folder and flag them to build up the blacklist.*
18
19
 
19
20
  ### The blacklist
20
21
 
@@ -24,6 +25,13 @@ be blacklisted. However, to prevent millions of users to go blacklisted because
24
25
  list of free email providers (which includes domains like gmail.com, outlook.com among others). If the sender uses a free
25
26
  email provider, the full address is blacklisted.
26
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
+
27
35
  ## Installation
28
36
 
29
37
  You can install OSA from RubyGems:
@@ -58,18 +66,21 @@ osa setup
58
66
 
59
67
  Each time you run this command, your previous configuration will be erased, except for your blacklist.
60
68
 
61
- Process your report folder:
69
+ Process your junk folder:
62
70
 
63
71
  ```sh
64
- osa scan-report
72
+ osa scan-junk
65
73
  ```
66
74
 
67
- Process your junk folder:
75
+ OSA also provides you a nice administration dashboard you. You can access the dashboard by running
68
76
 
69
77
  ```sh
70
- osa scan-junk
78
+ osa dashboard
71
79
  ```
72
80
 
81
+ You are now able to access the dashboard on `http://localhost:8080`. You can also change the port of the server by
82
+ providing the `SERVER_PORT` environment variable.
83
+
73
84
  ## Contributing
74
85
 
75
86
  Bug reports and pull requests are welcome on GitHub at https://github.com/moray95/osa.
data/Rakefile CHANGED
@@ -1,10 +1,11 @@
1
- require "bundler/gem_tasks"
2
- require "rake/testtask"
1
+ # frozen_string_literal: true
2
+ require 'bundler/gem_tasks'
3
+ require 'rake/testtask'
3
4
 
4
5
  Rake::TestTask.new(:test) do |t|
5
- t.libs << "test"
6
- t.libs << "lib"
7
- t.test_files = FileList["test/**/*_test.rb"]
6
+ t.libs << 'test'
7
+ t.libs << 'lib'
8
+ t.test_files = FileList['test/**/*_test.rb']
8
9
  end
9
10
 
10
- task :default => :test
11
+ task default: :test
data/bin/console CHANGED
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
-
3
- require "bundler/setup"
4
- require "osa"
2
+ require 'bundler/setup'
3
+ require 'osa'
5
4
 
6
5
  # You can add fixtures and/or initialization code here to make experimenting
7
6
  # with your gem easier. You can also use a different console, if you like.
@@ -10,5 +9,5 @@ require "osa"
10
9
  # require "pry"
11
10
  # Pry.start
12
11
 
13
- require "irb"
12
+ require 'irb'
14
13
  IRB.start(__FILE__)
data/bin/osa ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ #
4
+ # This file was generated by Bundler.
5
+ #
6
+ # The application 'osa' is installed as part of a gem, and
7
+ # this file is here to facilitate running it.
8
+ #
9
+
10
+ require 'pathname'
11
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
12
+ Pathname.new(__FILE__).realpath)
13
+
14
+ bundle_binstub = File.expand_path('../bundle', __FILE__)
15
+
16
+ if File.file?(bundle_binstub)
17
+ if /This file was generated by Bundler/.match?(File.read(bundle_binstub, 300))
18
+ load(bundle_binstub)
19
+ else
20
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
21
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
22
+ end
23
+ end
24
+
25
+ require 'rubygems'
26
+ require 'bundler/setup'
27
+
28
+ load Gem.bin_path('osa', 'osa')
data/exe/osa CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
  require 'osa/services/setup_service'
3
4
  require 'osa/services/auth_service'
4
5
 
@@ -8,12 +9,13 @@ case cmd
8
9
  when 'setup'
9
10
  OSA::SetupService.new.setup!
10
11
  when 'login'
11
- OSA::AuthService.login(Config.first || Config.new)
12
+ OSA::AuthService.login(OSA::Config.first || OSA::Config.new)
12
13
  when 'scan-junk'
13
14
  require 'osa/scripts/scan_junk_folder'
14
- when 'scan-report'
15
- require 'osa/scripts/scan_report_folder'
15
+ when 'dashboard'
16
+ require 'osa/scripts/dashboard_server'
17
+ DashboardServer.start!
16
18
  else
17
- $stderr.puts "Usage: #{File.basename($0)} [setup|login|scan-junk|scan-report]"
19
+ $stderr.puts "Usage: #{File.basename($0)} [setup|login|scan-junk|dashboard]"
18
20
  exit 1
19
21
  end
data/lib/osa.rb CHANGED
@@ -1 +1,2 @@
1
- require "osa/version"
1
+ # frozen_string_literal: true
2
+ require 'osa/version'
@@ -15,6 +15,11 @@ module OSA
15
15
  handle_response(response)
16
16
  end
17
17
 
18
+ def put(*args, **kwargs)
19
+ response = @connection.put(*args, **kwargs)
20
+ handle_response(response)
21
+ end
22
+
18
23
  def delete(*args, **kwargs)
19
24
  response = @connection.delete(*args, **kwargs)
20
25
  handle_response(response)
@@ -31,7 +36,7 @@ module OSA
31
36
  if response.status > 299
32
37
  raise StandardError, "Request failed with status code: #{response.status}, body: #{response.body}"
33
38
  end
34
- if response.headers['content-type'].include?('application/json')
39
+ if response.headers['content-type']&.include?('application/json')
35
40
  JSON.parse(response.body)
36
41
  else
37
42
  response.body
@@ -4,44 +4,60 @@ require 'json'
4
4
  require 'base64'
5
5
  require 'osa/util/paginated'
6
6
  require 'osa/clients/http_client'
7
+ require 'active_support/core_ext/numeric/bytes'
8
+ require 'active_support'
9
+ require 'mail'
7
10
 
8
11
  module OSA
9
- class MSGraphClient < HttpClient
12
+ class MSGraphClient
13
+ URL = 'https://graph.microsoft.com'
14
+
10
15
  def initialize(token)
11
- connection = Faraday.new(
16
+ @authenticated = HttpClient.new(Faraday.new(
12
17
  url: 'https://graph.microsoft.com',
13
18
  headers: {
14
19
  'authorization' => "Bearer #{token}"
15
20
  }
16
- )
17
- super connection
21
+ ))
22
+
23
+ @unauthenticated = HttpClient.new(Faraday.new(
24
+ url: 'https://graph.microsoft.com'
25
+ ))
18
26
  end
19
27
 
20
28
  def rules
21
- get('/v1.0/me/mailFolders/inbox/messageRules')
29
+ @authenticated.get('/v1.0/me/mailFolders/inbox/messageRules')
22
30
  end
23
31
 
24
32
  def rule(id)
25
- get("/v1.0/me/mailFolders/inbox/messageRules/#{id}")
33
+ @authenticated.get("/v1.0/me/mailFolders/inbox/messageRules/#{id}")
26
34
  end
27
35
 
28
36
  def folders
29
- Paginated.new(get('/v1.0/me/mailFolders'), self)
37
+ Paginated.new(@authenticated.get('/v1.0/me/mailFolders'), @authenticated)
30
38
  end
31
39
 
32
40
  def mails(folder_id)
33
- Paginated.new(get("/v1.0/me/mailFolders/#{folder_id}/messages"), self)
41
+ Paginated.new(@authenticated.get("/v1.0/me/mailFolders/#{folder_id}/messages"), @authenticated)
34
42
  end
35
43
 
36
44
  def raw_mail(mail_id)
37
- get("/v1.0/me/messages/#{mail_id}/$value")
45
+ @authenticated.get("/v1.0/me/messages/#{mail_id}/$value")
46
+ end
47
+
48
+ def sender_ip(mail_id)
49
+ content = raw_mail(mail_id)
50
+ mail = Mail.new(content)
51
+ mail.header['x-sender-ip']
38
52
  end
39
53
 
40
54
  def forward_mail_as_attachment(mail_id, to)
41
55
  raw_mail = self.raw_mail(mail_id)
42
56
  forward_message = create_forward_message(mail_id)
43
- add_email_attachment(forward_message['id'], raw_mail)
44
57
  update = {
58
+ body: {
59
+ content: ''
60
+ },
45
61
  toRecipients: [
46
62
  {
47
63
  emailAddress: {
@@ -51,36 +67,68 @@ module OSA
51
67
  ]
52
68
  }
53
69
  update_message(forward_message['id'], update)
70
+ add_email_attachment(forward_message['id'], 'email.eml', raw_mail)
54
71
  send_message(forward_message['id'])
55
72
  end
56
73
 
57
74
  def create_forward_message(mail_id)
58
- post("/v1.0/me/messages/#{mail_id}/createForward")
75
+ @authenticated.post("/v1.0/me/messages/#{mail_id}/createForward")
59
76
  end
60
77
 
61
- def add_email_attachment(mail_id, content)
62
- body = {
63
- "@odata.type": '#microsoft.graph.fileAttachment',
64
- "contentBytes": Base64.encode64(content),
65
- "name": 'email.eml'
66
- }
67
- post("/v1.0/me/messages/#{mail_id}/attachments", body.to_json, 'content-type': 'application/json')
78
+ def add_email_attachment(mail_id, name, content)
79
+ if content.length < 3.megabytes
80
+ add_small_email_attachment(mail_id, name, content)
81
+ else
82
+ add_large_email_attachment(mail_id, name, content)
83
+ end
68
84
  end
69
85
 
70
86
  def delete_mail(mail_id)
71
- delete("/v1.0/me/messages/#{mail_id}")
87
+ @authenticated.delete("/v1.0/me/messages/#{mail_id}")
72
88
  end
73
89
 
74
90
  def update_rule(id, update)
75
- patch("/v1.0/me/mailFolders/inbox/messageRules/#{id}", update.to_json, 'content-type' => 'application/json')
91
+ @authenticated.patch("/v1.0/me/mailFolders/inbox/messageRules/#{id}", update.to_json, 'content-type' => 'application/json')
76
92
  end
77
93
 
78
94
  def update_message(id, update)
79
- patch("/v1.0/me/messages/#{id}", update.to_json, 'content-type': 'application/json')
95
+ @authenticated.patch("/v1.0/me/messages/#{id}", update.to_json, 'content-type': 'application/json')
80
96
  end
81
97
 
82
98
  def send_message(id)
83
- post("/v1.0/me/messages/#{id}/send")
99
+ @authenticated.post("/v1.0/me/messages/#{id}/send")
84
100
  end
101
+
102
+ private
103
+ def add_small_email_attachment(mail_id, name, content)
104
+ body = {
105
+ "@odata.type": '#microsoft.graph.fileAttachment',
106
+ contentBytes: Base64.encode64(content),
107
+ name: name
108
+ }
109
+ @authenticated.post("/v1.0/me/messages/#{mail_id}/attachments", body.to_json, 'content-type': 'application/json')
110
+ end
111
+
112
+ def add_large_email_attachment(mail_id, name, content)
113
+ upload_session = create_upload_session(mail_id, name, content.length)
114
+ ranges = upload_session['nextExpectedRanges'].map do |range|
115
+ range.split('-').then { |start, finish| (start.to_i..finish&.to_i) }
116
+ end
117
+ ranges.each do |range|
118
+ current_content = content[range]
119
+ @unauthenticated.put(upload_session['uploadUrl'], current_content[range], 'Content-Range': "bytes #{range.begin}-#{(range.end || content.length) - 1}/#{content.length}")
120
+ end
121
+ end
122
+
123
+ def create_upload_session(mail_id, name, size)
124
+ body = {
125
+ AttachmentItem: {
126
+ attachmentType: :file,
127
+ name: name,
128
+ size: size
129
+ }
130
+ }
131
+ @authenticated.post("/v1.0/me/messages/#{mail_id}/attachments/createUploadSession", body.to_json, 'content-type': 'application/json')
132
+ end
85
133
  end
86
134
  end