osa 0.1.0
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 +7 -0
- data/.dockerignore +195 -0
- data/.gitignore +196 -0
- data/.rubocop.yml +197 -0
- data/Dockerfile +5 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +86 -0
- data/LICENSE.txt +21 -0
- data/README.md +79 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/osa +19 -0
- data/lib/osa.rb +1 -0
- data/lib/osa/clients/http_client.rb +41 -0
- data/lib/osa/clients/ms_auth_client.rb +36 -0
- data/lib/osa/clients/ms_graph_client.rb +86 -0
- data/lib/osa/migrations/00001_create_blacklists.rb +10 -0
- data/lib/osa/migrations/00002_create_email_providers.rb +21 -0
- data/lib/osa/migrations/00003_create_config.rb +13 -0
- data/lib/osa/migrations/free-email-providers.txt +3782 -0
- data/lib/osa/scripts/scan_junk_folder.rb +38 -0
- data/lib/osa/scripts/scan_report_folder.rb +33 -0
- data/lib/osa/services/auth_service.rb +56 -0
- data/lib/osa/services/setup_service.rb +27 -0
- data/lib/osa/util/constants.rb +7 -0
- data/lib/osa/util/context.rb +28 -0
- data/lib/osa/util/db.rb +22 -0
- data/lib/osa/util/paginated.rb +31 -0
- data/lib/osa/version.rb +3 -0
- data/osa.gemspec +28 -0
- data/release.sh +23 -0
- data/web/login.html +14 -0
- metadata +146 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'osa/util/constants'
|
3
|
+
require 'public_suffix'
|
4
|
+
require 'osa/util/db'
|
5
|
+
require 'osa/util/context'
|
6
|
+
|
7
|
+
context = OSA::Context.new
|
8
|
+
|
9
|
+
continue = true
|
10
|
+
|
11
|
+
while continue
|
12
|
+
mails = context.graph_client.mails(context.config.junk_folder_id)
|
13
|
+
continue = false
|
14
|
+
loop do
|
15
|
+
break if mails.nil?
|
16
|
+
mails['value'].each do |mail|
|
17
|
+
email_address = mail['sender']['emailAddress']['address']
|
18
|
+
next if email_address.nil?
|
19
|
+
domain = PublicSuffix.domain(email_address.split('@', 2)[1])
|
20
|
+
|
21
|
+
blacklisted = OSA::Blacklist.where(value: email_address).or(OSA::Blacklist.where(value: domain)).exists?
|
22
|
+
if blacklisted
|
23
|
+
puts "#{email_address} is blacklisted, reporting and deleting"
|
24
|
+
else
|
25
|
+
puts "Skipping mail from #{email_address}, its not blacklisted"
|
26
|
+
next
|
27
|
+
end
|
28
|
+
|
29
|
+
continue = true
|
30
|
+
|
31
|
+
puts "forwarding spam from #{email_address}"
|
32
|
+
context.graph_client.forward_mail_as_attachment(mail['id'], context.config.spamcop_report_email)
|
33
|
+
puts "deleting spam from #{email_address}"
|
34
|
+
context.graph_client.delete_mail(mail['id'])
|
35
|
+
end
|
36
|
+
mails = mails.next
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'uri'
|
3
|
+
require 'public_suffix'
|
4
|
+
require 'osa/util/db'
|
5
|
+
require 'osa/util/context'
|
6
|
+
|
7
|
+
context = OSA::Context.new
|
8
|
+
|
9
|
+
loop do
|
10
|
+
mails = context.graph_client.mails(context.config.report_folder_id)
|
11
|
+
break if mails['value'].empty?
|
12
|
+
mails['value'].each do |mail|
|
13
|
+
email_address = mail['sender']['emailAddress']['address']
|
14
|
+
|
15
|
+
puts "forwarding spam from #{email_address}"
|
16
|
+
context.graph_client.forward_mail_as_attachment(mail['id'], context.config.spamcop_report_email)
|
17
|
+
puts "deleting spam from #{email_address}"
|
18
|
+
context.graph_client.delete_mail(mail['id'])
|
19
|
+
|
20
|
+
|
21
|
+
next if email_address.nil?
|
22
|
+
domain = PublicSuffix.domain(email_address.split('@', 2)[1])
|
23
|
+
|
24
|
+
is_free_provider = OSA::EmailProvider.where(value: domain).exists?
|
25
|
+
if is_free_provider
|
26
|
+
puts "#{email_address} is using a free provider, blacklisting full address"
|
27
|
+
OSA::Blacklist.find_or_create_by(value: email_address).save!
|
28
|
+
else
|
29
|
+
puts "Adding #{domain} to blacklist"
|
30
|
+
OSA::Blacklist.find_or_create_by(value: domain).save!
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'osa/util/db'
|
3
|
+
require 'uri'
|
4
|
+
require 'tty-prompt'
|
5
|
+
|
6
|
+
module OSA
|
7
|
+
class AuthService
|
8
|
+
def initialize(client)
|
9
|
+
@client = client
|
10
|
+
end
|
11
|
+
|
12
|
+
def refresh_token(config)
|
13
|
+
token = @client.refresh_token(config.refresh_token)
|
14
|
+
config.update!(refresh_token: token['refresh_token'])
|
15
|
+
token
|
16
|
+
end
|
17
|
+
|
18
|
+
def code_token(code, config)
|
19
|
+
token = @client.code_token(code)
|
20
|
+
config.update!(refresh_token: token['refresh_token'])
|
21
|
+
token
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.login(config)
|
25
|
+
query = {
|
26
|
+
client_id: CLIENT_ID,
|
27
|
+
response_type: 'code',
|
28
|
+
redirect_uri: REDIRECT_URL,
|
29
|
+
response_mode: 'query',
|
30
|
+
scope: SCOPE,
|
31
|
+
state: ''
|
32
|
+
}
|
33
|
+
|
34
|
+
query_str = query.reduce('?') do |acc, val|
|
35
|
+
key, value = val
|
36
|
+
acc + "#{key}=#{URI.encode_www_form_component(value)}&"
|
37
|
+
end
|
38
|
+
|
39
|
+
base_uri = 'https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize'
|
40
|
+
|
41
|
+
uri = "#{base_uri}#{query_str}"
|
42
|
+
puts "Open the following url in your browser and enter the authentication code displayed."
|
43
|
+
puts uri
|
44
|
+
|
45
|
+
prompt = TTY::Prompt.new
|
46
|
+
code = prompt.ask('Code:')
|
47
|
+
|
48
|
+
auth_client = MSAuthClient.new
|
49
|
+
auth_service = AuthService.new(auth_client)
|
50
|
+
|
51
|
+
auth_service.code_token(code, config)
|
52
|
+
|
53
|
+
puts 'login completed'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'osa/util/db'
|
3
|
+
require 'osa/services/auth_service'
|
4
|
+
require 'tty-prompt'
|
5
|
+
require 'osa/util/context'
|
6
|
+
|
7
|
+
module OSA
|
8
|
+
class SetupService
|
9
|
+
def setup!
|
10
|
+
Config.delete_all
|
11
|
+
config = Config.new
|
12
|
+
AuthService.login(config)
|
13
|
+
|
14
|
+
context = Context.new(config)
|
15
|
+
folders = context.graph_client.folders.all
|
16
|
+
choices = folders.map { |folder|
|
17
|
+
[folder['displayName'], folder['id']]
|
18
|
+
}.to_h
|
19
|
+
prompt = TTY::Prompt.new
|
20
|
+
junk_folder_id = prompt.select('Select junk folder:', choices)
|
21
|
+
report_folder_id = prompt.select('Select report folder:', choices)
|
22
|
+
spamcop_email = prompt.ask('Spamcop report email:') { |q| q.validate :email }
|
23
|
+
|
24
|
+
config.update! junk_folder_id: junk_folder_id, report_folder_id: report_folder_id, spamcop_report_email: spamcop_email
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OSA
|
4
|
+
CLIENT_ID = 'befa4a9e-5d16-4a48-9792-4bd1d125abe8'
|
5
|
+
REDIRECT_URL = 'https://storage.googleapis.com/outlook-spam-automator/login.html'
|
6
|
+
SCOPE = 'https://graph.microsoft.com/MailboxSettings.ReadWrite https://graph.microsoft.com/MailboxSettings.ReadWrite offline_access'
|
7
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'osa/clients/ms_auth_client'
|
4
|
+
require 'osa/services/auth_service'
|
5
|
+
require 'osa/clients/ms_graph_client'
|
6
|
+
|
7
|
+
module OSA
|
8
|
+
class Context
|
9
|
+
attr_reader :graph_client
|
10
|
+
attr_reader :auth_service
|
11
|
+
attr_reader :config
|
12
|
+
|
13
|
+
def initialize(config=nil)
|
14
|
+
@config = config || Config.first
|
15
|
+
|
16
|
+
if @config.nil?
|
17
|
+
$stderr.puts 'OSA not configured'
|
18
|
+
exit 1
|
19
|
+
end
|
20
|
+
|
21
|
+
auth_client = MSAuthClient.new
|
22
|
+
@auth_service = AuthService.new(auth_client)
|
23
|
+
token = auth_service.refresh_token(@config)
|
24
|
+
|
25
|
+
@graph_client = MSGraphClient.new(token['access_token'])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/osa/util/db.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'active_record'
|
3
|
+
|
4
|
+
ActiveRecord::Base.establish_connection(
|
5
|
+
adapter: 'sqlite3',
|
6
|
+
database: ENV['DATABASE'] || "#{Dir.pwd}/osa.db"
|
7
|
+
)
|
8
|
+
root = "#{File.dirname(__FILE__ )}/../"
|
9
|
+
ActiveRecord::MigrationContext.new("#{root}/migrations", ActiveRecord::SchemaMigration).migrate
|
10
|
+
|
11
|
+
|
12
|
+
module OSA
|
13
|
+
class Blacklist < ActiveRecord::Base
|
14
|
+
end
|
15
|
+
|
16
|
+
class Config < ActiveRecord::Base
|
17
|
+
self.table_name = 'config'
|
18
|
+
end
|
19
|
+
|
20
|
+
class EmailProvider < ActiveRecord::Base
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OSA
|
4
|
+
class Paginated
|
5
|
+
def initialize(value, client)
|
6
|
+
@value = value
|
7
|
+
@client = client
|
8
|
+
end
|
9
|
+
|
10
|
+
def next
|
11
|
+
next_page = @value['@odata.nextLink']
|
12
|
+
return nil if next_page.nil?
|
13
|
+
Paginated.new(@client.get(next_page), @client)
|
14
|
+
end
|
15
|
+
|
16
|
+
def all
|
17
|
+
value = @value['value'].dup
|
18
|
+
next_page = self.next
|
19
|
+
until next_page.nil?
|
20
|
+
value += next_page['value']
|
21
|
+
next_page = next_page.next
|
22
|
+
end
|
23
|
+
value
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def method_missing(symbol, *args)
|
28
|
+
@value.send(symbol, *args)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/osa/version.rb
ADDED
data/osa.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative 'lib/osa/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = 'osa'
|
5
|
+
spec.version = OSA::VERSION
|
6
|
+
spec.authors = ['Moray Baruh']
|
7
|
+
spec.email = ['contact@moraybaruh.com']
|
8
|
+
|
9
|
+
spec.summary = 'Outlook Spam Automator'
|
10
|
+
spec.description = 'Get rid of spam on your Outlook account'
|
11
|
+
spec.license = 'MIT'
|
12
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
|
13
|
+
|
14
|
+
# Specify which files should be added to the gem when it is released.
|
15
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
16
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
17
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
end
|
19
|
+
spec.bindir = 'exe'
|
20
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
|
+
spec.require_paths = ['lib']
|
22
|
+
|
23
|
+
spec.add_dependency 'activerecord', '~> 6.0'
|
24
|
+
spec.add_dependency 'faraday', '~> 1.1'
|
25
|
+
spec.add_dependency 'public_suffix', '~> 4.0'
|
26
|
+
spec.add_dependency 'sqlite3', '~> 1.4'
|
27
|
+
spec.add_dependency 'tty-prompt', '~> 0.22'
|
28
|
+
end
|
data/release.sh
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
set -eu
|
4
|
+
|
5
|
+
VERSION=$(bundle exec ruby -e 'puts OSA::VERSION')
|
6
|
+
echo -n "Releasing ${VERSION}. Continue? (Y/n) "
|
7
|
+
read -r CONTINUE
|
8
|
+
|
9
|
+
if [ "$CONTINUE" != "y" ] && [ "$CONTINUE" != "Y" ]
|
10
|
+
then
|
11
|
+
echo "Aborting release." >&2
|
12
|
+
exit 1
|
13
|
+
fi
|
14
|
+
|
15
|
+
rake release
|
16
|
+
echo "Waiting for gem to be available..."
|
17
|
+
while ! (gem info -r osa --all | grep "${VERSION}")
|
18
|
+
do
|
19
|
+
sleep 5
|
20
|
+
done
|
21
|
+
|
22
|
+
docker build . --build-arg "OSA_VERSION=${VERSION}" -t "moray95/osa:${VERSION}"
|
23
|
+
docker push "moray95/osa:${VERSION}"
|
data/web/login.html
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8">
|
5
|
+
<title>Outlook Spam Automator Login</title>
|
6
|
+
</head>
|
7
|
+
<body>
|
8
|
+
<div id="code"></div>
|
9
|
+
</body>
|
10
|
+
</html>
|
11
|
+
<script>
|
12
|
+
const urlParams = new URLSearchParams(location.search);
|
13
|
+
document.getElementById("code").innerText = "code: " + urlParams.get('code');
|
14
|
+
</script>
|
metadata
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: osa
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Moray Baruh
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-11-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '6.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '6.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: faraday
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.1'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.1'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: public_suffix
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '4.0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '4.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: sqlite3
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.4'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.4'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: tty-prompt
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.22'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.22'
|
83
|
+
description: Get rid of spam on your Outlook account
|
84
|
+
email:
|
85
|
+
- contact@moraybaruh.com
|
86
|
+
executables:
|
87
|
+
- osa
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- ".dockerignore"
|
92
|
+
- ".gitignore"
|
93
|
+
- ".rubocop.yml"
|
94
|
+
- Dockerfile
|
95
|
+
- Gemfile
|
96
|
+
- Gemfile.lock
|
97
|
+
- LICENSE.txt
|
98
|
+
- README.md
|
99
|
+
- Rakefile
|
100
|
+
- bin/console
|
101
|
+
- bin/setup
|
102
|
+
- exe/osa
|
103
|
+
- lib/osa.rb
|
104
|
+
- lib/osa/clients/http_client.rb
|
105
|
+
- lib/osa/clients/ms_auth_client.rb
|
106
|
+
- lib/osa/clients/ms_graph_client.rb
|
107
|
+
- lib/osa/migrations/00001_create_blacklists.rb
|
108
|
+
- lib/osa/migrations/00002_create_email_providers.rb
|
109
|
+
- lib/osa/migrations/00003_create_config.rb
|
110
|
+
- lib/osa/migrations/free-email-providers.txt
|
111
|
+
- lib/osa/scripts/scan_junk_folder.rb
|
112
|
+
- lib/osa/scripts/scan_report_folder.rb
|
113
|
+
- lib/osa/services/auth_service.rb
|
114
|
+
- lib/osa/services/setup_service.rb
|
115
|
+
- lib/osa/util/constants.rb
|
116
|
+
- lib/osa/util/context.rb
|
117
|
+
- lib/osa/util/db.rb
|
118
|
+
- lib/osa/util/paginated.rb
|
119
|
+
- lib/osa/version.rb
|
120
|
+
- osa.gemspec
|
121
|
+
- release.sh
|
122
|
+
- web/login.html
|
123
|
+
homepage:
|
124
|
+
licenses:
|
125
|
+
- MIT
|
126
|
+
metadata: {}
|
127
|
+
post_install_message:
|
128
|
+
rdoc_options: []
|
129
|
+
require_paths:
|
130
|
+
- lib
|
131
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
132
|
+
requirements:
|
133
|
+
- - ">="
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: 2.7.0
|
136
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - ">="
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: '0'
|
141
|
+
requirements: []
|
142
|
+
rubygems_version: 3.1.2
|
143
|
+
signing_key:
|
144
|
+
specification_version: 4
|
145
|
+
summary: Outlook Spam Automator
|
146
|
+
test_files: []
|