osa 0.1.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: 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