minbox 0.1.4 → 0.1.5
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 +4 -4
- data/.rubocop.yml +91 -0
- data/Dockerfile +2 -0
- data/Gemfile +3 -1
- data/Gemfile.lock +40 -2
- data/README.md +46 -4
- data/Rakefile +10 -3
- data/bin/console +4 -3
- data/bin/docker-build +1 -0
- data/bin/lint +11 -0
- data/exe/minbox +2 -1
- data/lib/minbox.rb +11 -5
- data/lib/minbox/cli.rb +25 -14
- data/lib/minbox/client.rb +108 -93
- data/lib/minbox/core.rb +2 -0
- data/lib/minbox/inbox.rb +76 -0
- data/lib/minbox/publisher.rb +38 -36
- data/lib/minbox/server.rb +24 -20
- data/lib/minbox/version.rb +3 -1
- data/minbox.gemspec +31 -24
- metadata +89 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f483bc9bcd047c68ad7203630dc57f223c3cce4c182c783cf099ef551b02c937
|
4
|
+
data.tar.gz: 2b313d59d6d84b3216250f15295a83800ad1eff6b54b5714a6492f7396293aae
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d45fef638a89f7eda7fb3f7a49d9fda346d0c4086d6264ea95759adfb9aef7ef06e85cac594685627e5c64d941590bd9ae8760102b26270b5bcc756e375cfe8
|
7
|
+
data.tar.gz: 78b921911e7e2a99fa8a584c1fb992dd3374db56cbefa0fcb2ac14aa33cdd80d5afb0c2e2324d55bae1dc1b629f5930b4abeed695a61b818a474c54eaa7536c4
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
require:
|
2
|
+
- rubocop/cop/internal_affairs
|
3
|
+
- rubocop-rspec
|
4
|
+
|
5
|
+
AllCops:
|
6
|
+
Exclude:
|
7
|
+
- 'coverage/**/*'
|
8
|
+
- 'pkg/**/*'
|
9
|
+
- 'tmp/**/*'
|
10
|
+
- 'vendor/**/*'
|
11
|
+
TargetRubyVersion: 2.6
|
12
|
+
|
13
|
+
Layout/AlignParameters:
|
14
|
+
Enabled: true
|
15
|
+
EnforcedStyle: with_fixed_indentation
|
16
|
+
IndentationWidth: 2
|
17
|
+
|
18
|
+
Layout/ClassStructure:
|
19
|
+
Enabled: true
|
20
|
+
Categories:
|
21
|
+
module_inclusion:
|
22
|
+
- include
|
23
|
+
- prepend
|
24
|
+
- extend
|
25
|
+
ExpectedOrder:
|
26
|
+
- module_inclusion
|
27
|
+
- constants
|
28
|
+
- public_class_methods
|
29
|
+
- initializer
|
30
|
+
- instance_methods
|
31
|
+
- protected_methods
|
32
|
+
- private_methods
|
33
|
+
|
34
|
+
Layout/EndOfLine:
|
35
|
+
EnforcedStyle: lf
|
36
|
+
|
37
|
+
Layout/IndentArray:
|
38
|
+
EnforcedStyle: consistent
|
39
|
+
|
40
|
+
Layout/IndentHeredoc:
|
41
|
+
EnforcedStyle: active_support
|
42
|
+
|
43
|
+
Layout/MultilineMethodCallIndentation:
|
44
|
+
Enabled: true
|
45
|
+
EnforcedStyle: indented
|
46
|
+
|
47
|
+
Lint/AmbiguousBlockAssociation:
|
48
|
+
Exclude:
|
49
|
+
- 'spec/**/*.rb'
|
50
|
+
|
51
|
+
Lint/InterpolationCheck:
|
52
|
+
Exclude:
|
53
|
+
- 'spec/**/*.rb'
|
54
|
+
|
55
|
+
Metrics/BlockLength:
|
56
|
+
Exclude:
|
57
|
+
- '**/*.rake'
|
58
|
+
- '*.gemspec'
|
59
|
+
- 'Rakefile'
|
60
|
+
- 'spec/**/*.rb'
|
61
|
+
|
62
|
+
Metrics/ModuleLength:
|
63
|
+
Exclude:
|
64
|
+
- 'spec/**/*.rb'
|
65
|
+
|
66
|
+
Metrics/LineLength:
|
67
|
+
Exclude:
|
68
|
+
- 'spec/**/*.rb'
|
69
|
+
IgnoredPatterns:
|
70
|
+
- '^#*'
|
71
|
+
|
72
|
+
Style/Documentation:
|
73
|
+
Enabled: false
|
74
|
+
|
75
|
+
Style/EachWithObject:
|
76
|
+
Enabled: false
|
77
|
+
|
78
|
+
Style/StringLiterals:
|
79
|
+
EnforcedStyle: 'single_quotes'
|
80
|
+
|
81
|
+
Style/TrailingCommaInArrayLiteral:
|
82
|
+
Enabled: false
|
83
|
+
|
84
|
+
Style/TrailingCommaInHashLiteral:
|
85
|
+
Enabled: false
|
86
|
+
|
87
|
+
RSpec/NamedSubject:
|
88
|
+
Enabled: false
|
89
|
+
|
90
|
+
RSpec/NestedGroups:
|
91
|
+
Max: 4
|
data/Dockerfile
CHANGED
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,10 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
minbox (0.1.
|
4
|
+
minbox (0.1.5)
|
5
|
+
concurrent-ruby (~> 1.1)
|
6
|
+
hashie (~> 3.6)
|
7
|
+
listen (~> 3.1)
|
5
8
|
mail (~> 2.7)
|
6
9
|
redis (~> 4.1)
|
7
10
|
thor (~> 0.20)
|
@@ -9,16 +12,35 @@ PATH
|
|
9
12
|
GEM
|
10
13
|
remote: https://rubygems.org/
|
11
14
|
specs:
|
12
|
-
|
15
|
+
ast (2.4.0)
|
16
|
+
bundler-audit (0.6.1)
|
17
|
+
bundler (>= 1.2.0, < 3)
|
18
|
+
thor (~> 0.18)
|
19
|
+
concurrent-ruby (1.1.5)
|
13
20
|
diff-lcs (1.3)
|
14
21
|
faker (1.9.3)
|
15
22
|
i18n (>= 0.7)
|
23
|
+
ffi (1.10.0)
|
24
|
+
hashie (3.6.0)
|
16
25
|
i18n (1.6.0)
|
17
26
|
concurrent-ruby (~> 1.0)
|
27
|
+
jaro_winkler (1.5.2)
|
28
|
+
listen (3.1.5)
|
29
|
+
rb-fsevent (~> 0.9, >= 0.9.4)
|
30
|
+
rb-inotify (~> 0.9, >= 0.9.7)
|
31
|
+
ruby_dep (~> 1.2)
|
18
32
|
mail (2.7.1)
|
19
33
|
mini_mime (>= 0.1.1)
|
20
34
|
mini_mime (1.0.1)
|
35
|
+
parallel (1.17.0)
|
36
|
+
parser (2.6.2.1)
|
37
|
+
ast (~> 2.4.0)
|
38
|
+
psych (3.1.0)
|
39
|
+
rainbow (3.0.0)
|
21
40
|
rake (10.5.0)
|
41
|
+
rb-fsevent (0.10.3)
|
42
|
+
rb-inotify (0.10.0)
|
43
|
+
ffi (~> 1.0)
|
22
44
|
redis (4.1.0)
|
23
45
|
rspec (3.8.0)
|
24
46
|
rspec-core (~> 3.8.0)
|
@@ -33,17 +55,33 @@ GEM
|
|
33
55
|
diff-lcs (>= 1.2.0, < 2.0)
|
34
56
|
rspec-support (~> 3.8.0)
|
35
57
|
rspec-support (3.8.0)
|
58
|
+
rubocop (0.67.2)
|
59
|
+
jaro_winkler (~> 1.5.1)
|
60
|
+
parallel (~> 1.10)
|
61
|
+
parser (>= 2.5, != 2.5.1.1)
|
62
|
+
psych (>= 3.1.0)
|
63
|
+
rainbow (>= 2.2.2, < 4.0)
|
64
|
+
ruby-progressbar (~> 1.7)
|
65
|
+
unicode-display_width (>= 1.4.0, < 1.6)
|
66
|
+
rubocop-rspec (1.32.0)
|
67
|
+
rubocop (>= 0.60.0)
|
68
|
+
ruby-progressbar (1.10.0)
|
69
|
+
ruby_dep (1.5.0)
|
36
70
|
thor (0.20.3)
|
71
|
+
unicode-display_width (1.5.0)
|
37
72
|
|
38
73
|
PLATFORMS
|
39
74
|
ruby
|
40
75
|
|
41
76
|
DEPENDENCIES
|
42
77
|
bundler (~> 2.0)
|
78
|
+
bundler-audit (~> 0.6)
|
43
79
|
faker (~> 1.9)
|
44
80
|
minbox!
|
45
81
|
rake (~> 10.0)
|
46
82
|
rspec (~> 3.0)
|
83
|
+
rubocop (~> 0.52)
|
84
|
+
rubocop-rspec (~> 1.22)
|
47
85
|
|
48
86
|
BUNDLED WITH
|
49
87
|
2.0.1
|
data/README.md
CHANGED
@@ -1,8 +1,19 @@
|
|
1
1
|
# Minbox
|
2
2
|
|
3
|
-
|
3
|
+
A minimal SMTP server written in ruby. `Minbox` offers a command line
|
4
|
+
interface and is useful for end-to-end test suites or as a standalone SMTP server
|
5
|
+
for development.
|
4
6
|
|
5
|
-
|
7
|
+
`Minbox` is capable of publishing email messages to `stdout`, the `file`
|
8
|
+
system or to `redis`.
|
9
|
+
|
10
|
+
The `file` system publisher will write all emails to `./tmp` of the
|
11
|
+
directory where you run minbox from. Each file is named with the format
|
12
|
+
of `<timestamp>.eml`.
|
13
|
+
|
14
|
+
The `redis` publisher will publish all emails to a channel named
|
15
|
+
`minbox`. Use the `REDIS_URL` environment variable to control the redis
|
16
|
+
client configuration. See [this](https://github.com/redis/redis-rb/blob/df07a4c90413ed5dda7bc8fe928b00aaad5462fa/lib/redis/client.rb#L9) for more information.
|
6
17
|
|
7
18
|
## Installation
|
8
19
|
|
@@ -22,7 +33,38 @@ Or install it yourself as:
|
|
22
33
|
|
23
34
|
## Usage
|
24
35
|
|
25
|
-
|
36
|
+
```bash
|
37
|
+
モ minbox
|
38
|
+
minbox commands:
|
39
|
+
minbox client <HOST> <PORT> # SMTP client
|
40
|
+
minbox help [COMMAND] # Describe available commands or one specific command
|
41
|
+
minbox server <HOST> <PORT> # SMTP server
|
42
|
+
minbox version # Display the current version
|
43
|
+
```
|
44
|
+
|
45
|
+
To start an SMTP server run:
|
46
|
+
|
47
|
+
```bash
|
48
|
+
モ minbox server localhost 8080
|
49
|
+
D, [2019-03-12T17:08:19.671765 #36618] DEBUG -- : Starting server on port 8080...
|
50
|
+
D, [2019-03-12T17:08:19.679380 #36618] DEBUG -- : Server started!
|
51
|
+
```
|
52
|
+
|
53
|
+
You can use the `--output` option to configure the different types of
|
54
|
+
publishers to publish to. The following example will publish emails to
|
55
|
+
`stdout`, `file` system, and `redis`.
|
56
|
+
|
57
|
+
```bash
|
58
|
+
モ minbox server localhost 8080 --output=stdout file redis
|
59
|
+
D, [2019-03-12T17:16:03.564426 #36907] DEBUG -- : Starting server on port 8080...
|
60
|
+
D, [2019-03-12T17:16:03.565964 #36907] DEBUG -- : Server started!
|
61
|
+
```
|
62
|
+
|
63
|
+
To send an example email:
|
64
|
+
|
65
|
+
```bash
|
66
|
+
モ minbox client localhost 8080
|
67
|
+
```
|
26
68
|
|
27
69
|
## Development
|
28
70
|
|
@@ -32,7 +74,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
32
74
|
|
33
75
|
## Contributing
|
34
76
|
|
35
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
77
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/mokhan/minbox.
|
36
78
|
|
37
79
|
## License
|
38
80
|
|
data/Rakefile
CHANGED
@@ -1,6 +1,13 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/audit/task'
|
4
|
+
require 'bundler/gem_tasks'
|
5
|
+
require 'rspec/core/rake_task'
|
6
|
+
require 'rubocop/rake_task'
|
3
7
|
|
4
8
|
RSpec::Core::RakeTask.new(:spec)
|
9
|
+
RuboCop::RakeTask.new(:rubocop)
|
10
|
+
Bundler::Audit::Task.new
|
5
11
|
|
6
|
-
task :
|
12
|
+
task lint: [:rubocop, 'bundle:audit']
|
13
|
+
task default: :spec
|
data/bin/console
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
|
-
require
|
4
|
-
require
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'minbox'
|
5
6
|
|
6
7
|
# You can add fixtures and/or initialization code here to make experimenting
|
7
8
|
# with your gem easier. You can also use a different console, if you like.
|
@@ -10,5 +11,5 @@ require "minbox"
|
|
10
11
|
# require "pry"
|
11
12
|
# Pry.start
|
12
13
|
|
13
|
-
require
|
14
|
+
require 'irb'
|
14
15
|
IRB.start(__FILE__)
|
data/bin/docker-build
CHANGED
data/bin/lint
ADDED
data/exe/minbox
CHANGED
data/lib/minbox.rb
CHANGED
@@ -1,12 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'base64'
|
4
|
+
require 'concurrent'
|
5
|
+
require 'hashie'
|
6
|
+
require 'listen'
|
2
7
|
require 'logger'
|
3
8
|
require 'socket'
|
4
9
|
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
10
|
+
require 'minbox/client'
|
11
|
+
require 'minbox/core'
|
12
|
+
require 'minbox/inbox'
|
13
|
+
require 'minbox/publisher'
|
14
|
+
require 'minbox/server'
|
15
|
+
require 'minbox/version'
|
10
16
|
|
11
17
|
module Minbox
|
12
18
|
class Error < StandardError; end
|
data/lib/minbox/cli.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'mail'
|
2
4
|
require 'net/smtp'
|
3
5
|
require 'openssl'
|
@@ -8,20 +10,17 @@ require 'minbox'
|
|
8
10
|
module Minbox
|
9
11
|
module Cli
|
10
12
|
class Application < Thor
|
11
|
-
package_name
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
body "#{Time.now} This is a test message."
|
20
|
-
end
|
13
|
+
package_name 'minbox'
|
14
|
+
|
15
|
+
method_option :from, type: :string, default: 'me@example.org'
|
16
|
+
method_option :to, type: :string, default: ['them@example.org']
|
17
|
+
method_option :subject, type: :string, default: "#{Time.now} This is a test message."
|
18
|
+
method_option :body, type: :string, default: "#{Time.now} This is a test message."
|
19
|
+
desc 'send <HOST> <PORT>', 'Send mail to SMTP server'
|
20
|
+
def send(host = 'localhost', port = 25)
|
21
21
|
Net::SMTP.start(host, port) do |smtp|
|
22
|
-
smtp.debug_output= Minbox.logger
|
23
|
-
smtp.send_message(
|
24
|
-
smtp.send_message(mail.to_s, 'me+2@example.org', 'them+2@example.com')
|
22
|
+
smtp.debug_output = Minbox.logger
|
23
|
+
smtp.send_message(create_mail(options).to_s, options[:from], options[:to])
|
25
24
|
end
|
26
25
|
end
|
27
26
|
|
@@ -30,7 +29,8 @@ module Minbox
|
|
30
29
|
desc 'server <HOST> <PORT>', 'SMTP server'
|
31
30
|
def server(host = 'localhost', port = '25')
|
32
31
|
publisher = Publisher.from(options[:output])
|
33
|
-
Server.new(host, port, options[:tls])
|
32
|
+
server = Server.new(host: host, port: port, tls: options[:tls])
|
33
|
+
server.listen! do |mail|
|
34
34
|
publisher.publish(mail)
|
35
35
|
end
|
36
36
|
end
|
@@ -39,6 +39,17 @@ module Minbox
|
|
39
39
|
def version
|
40
40
|
say Minbox::VERSION
|
41
41
|
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def create_mail(options)
|
46
|
+
Mail.new do |x|
|
47
|
+
x.to = options[:to]
|
48
|
+
x.from = options[:from]
|
49
|
+
x.subject = options[:subject]
|
50
|
+
x.body = STDIN.tty? ? options[:body] : $stdin.read
|
51
|
+
end
|
52
|
+
end
|
42
53
|
end
|
43
54
|
end
|
44
55
|
end
|
data/lib/minbox/client.rb
CHANGED
@@ -1,124 +1,132 @@
|
|
1
|
-
|
2
|
-
class Client
|
3
|
-
attr_reader :server, :socket, :logger
|
1
|
+
# frozen_string_literal: true
|
4
2
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
module Minbox
|
4
|
+
class Ehlo
|
5
|
+
def run(client, line)
|
6
|
+
_ehlo, _client_domain = line.split(' ')
|
7
|
+
client.write "250-#{client.server.host} offers a warm hug of welcome"
|
8
|
+
client.write '250-8BITMIME'
|
9
|
+
client.write '250-ENHANCEDSTATUSCODES'
|
10
|
+
# client.write "250 STARTTLS"
|
11
|
+
client.write '250-AUTH PLAIN LOGIN'
|
12
|
+
client.write '250 OK'
|
9
13
|
end
|
14
|
+
end
|
10
15
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
when /^EHLO/i then ehlo(line)
|
16
|
-
when /^HELO/i then helo(line)
|
17
|
-
when /^MAIL FROM/i then mail_from(line)
|
18
|
-
when /^RCPT TO/i then rcpt_to(line)
|
19
|
-
when /^DATA/i then data(line, &block)
|
20
|
-
when /^QUIT/i then quit
|
21
|
-
when /^STARTTLS/i then start_tls
|
22
|
-
when /^RSET/i then reset
|
23
|
-
when /^NOOP/i then noop
|
24
|
-
when /^AUTH PLAIN/i then auth_plain(line)
|
25
|
-
when /^AUTH LOGIN/i then auth_login(line)
|
26
|
-
else
|
27
|
-
logger.error(line)
|
28
|
-
write '502 Invalid/unsupported command'
|
29
|
-
end
|
30
|
-
end
|
31
|
-
close
|
32
|
-
rescue Errno::ECONNRESET, Errno::EPIPE => error
|
33
|
-
logger.error(error)
|
34
|
-
close
|
16
|
+
class Helo
|
17
|
+
def run(client, line)
|
18
|
+
_ehlo, _client_domain = line.split(' ')
|
19
|
+
client.write "250 #{client.server.host}"
|
35
20
|
end
|
21
|
+
end
|
36
22
|
|
37
|
-
|
23
|
+
class Noop
|
24
|
+
def run(client, _line)
|
25
|
+
client.write '250 OK'
|
26
|
+
end
|
27
|
+
end
|
38
28
|
|
39
|
-
|
40
|
-
|
41
|
-
|
29
|
+
class Quit
|
30
|
+
def run(client, _line)
|
31
|
+
client.write '221 Bye'
|
32
|
+
client.close
|
42
33
|
end
|
34
|
+
end
|
43
35
|
|
44
|
-
|
45
|
-
|
36
|
+
class Data
|
37
|
+
def run(client, _line)
|
38
|
+
client.write '354 End data with <CR><LF>.<CR><LF>'
|
46
39
|
body = []
|
47
|
-
line = read
|
40
|
+
line = client.read
|
48
41
|
until line.nil? || line.match(/^\.\r\n$/)
|
49
42
|
body << line
|
50
|
-
line = read
|
43
|
+
line = client.read
|
51
44
|
end
|
52
|
-
write
|
53
|
-
|
45
|
+
client.write '250 OK'
|
46
|
+
yield(Mail.new(body.join)) unless body.empty?
|
54
47
|
end
|
48
|
+
end
|
55
49
|
|
56
|
-
|
57
|
-
|
50
|
+
class StartTls
|
51
|
+
def run(client, _line)
|
52
|
+
client.write '220 Ready to start TLS'
|
53
|
+
client.secure_socket!
|
58
54
|
end
|
55
|
+
end
|
59
56
|
|
60
|
-
|
61
|
-
|
57
|
+
class AuthPlain
|
58
|
+
def run(client, line)
|
59
|
+
data = line.gsub(/AUTH PLAIN ?/i, '')
|
60
|
+
if data.strip == ''
|
61
|
+
client.write '334'
|
62
|
+
data = client.read
|
63
|
+
end
|
64
|
+
parts = Base64.decode64(data).split("\0")
|
65
|
+
username = parts[-2]
|
66
|
+
password = parts[-1]
|
67
|
+
client.authenticate(username, password)
|
62
68
|
end
|
69
|
+
end
|
63
70
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
write
|
71
|
+
class AuthLogin
|
72
|
+
def run(client, line)
|
73
|
+
username = line.gsub!(/AUTH LOGIN ?/i, '')
|
74
|
+
if username.strip == ''
|
75
|
+
client.write '334 VXNlcm5hbWU6'
|
76
|
+
username = client.read
|
77
|
+
end
|
78
|
+
client.write '334 UGFzc3dvcmQ6'
|
79
|
+
password = Base64.decode64(client.read)
|
80
|
+
client.authenticate(username, password)
|
72
81
|
end
|
82
|
+
end
|
73
83
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
def start_tls
|
80
|
-
write "220 Ready to start TLS"
|
81
|
-
|
82
|
-
socket = OpenSSL::SSL::SSLSocket.new(@socket, server.ssl_context)
|
83
|
-
socket.sync_close = true
|
84
|
-
@socket = socket.accept
|
84
|
+
class Unsupported
|
85
|
+
def run(client, line)
|
86
|
+
client.logger.error(line)
|
87
|
+
client.write '502 Invalid/unsupported command'
|
85
88
|
end
|
89
|
+
end
|
86
90
|
|
87
|
-
|
88
|
-
|
89
|
-
|
91
|
+
class Client
|
92
|
+
COMMANDS = Hashie::Rash.new(
|
93
|
+
/^AUTH LOGIN/i => AuthLogin.new,
|
94
|
+
/^AUTH PLAIN/i => AuthPlain.new,
|
95
|
+
/^DATA/i => Data.new,
|
96
|
+
/^EHLO/i => Ehlo.new,
|
97
|
+
/^HELO/i => Helo.new,
|
98
|
+
/^MAIL FROM/i => Noop.new,
|
99
|
+
/^NOOP/i => Noop.new,
|
100
|
+
/^QUIT/i => Quit.new,
|
101
|
+
/^RCPT TO/i => Noop.new,
|
102
|
+
/^RSET/i => Noop.new,
|
103
|
+
/^STARTTLS/i => StartTls.new
|
104
|
+
)
|
105
|
+
UNSUPPORTED = Unsupported.new
|
106
|
+
attr_reader :server, :socket, :logger
|
90
107
|
|
91
|
-
def
|
92
|
-
|
108
|
+
def initialize(server, socket, logger)
|
109
|
+
@server = server
|
110
|
+
@logger = logger
|
111
|
+
@socket = socket
|
93
112
|
end
|
94
113
|
|
95
|
-
def
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
114
|
+
def handle(&block)
|
115
|
+
write "220 #{server.host} ESMTP"
|
116
|
+
while connected? && (line = read)
|
117
|
+
command = COMMANDS.fetch(line, UNSUPPORTED)
|
118
|
+
command.run(self, line, &block)
|
100
119
|
end
|
101
|
-
|
102
|
-
|
103
|
-
logger.
|
104
|
-
|
105
|
-
write "235 2.7.0 Authentication successful"
|
120
|
+
close
|
121
|
+
rescue Errno::ECONNRESET, Errno::EPIPE => error
|
122
|
+
logger.error(error)
|
123
|
+
close
|
106
124
|
end
|
107
125
|
|
108
|
-
def
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
username = read
|
113
|
-
write '334 UGFzc3dvcmQ6'
|
114
|
-
else
|
115
|
-
write '334 UGFzc3dvcmQ6'
|
116
|
-
end
|
117
|
-
password = Base64.decode64(read)
|
118
|
-
logger.debug("#{username}:#{password}")
|
119
|
-
|
120
|
-
return write '535 Authenticated failed - protocol error' unless username && password
|
121
|
-
write "235 2.7.0 Authentication successful"
|
126
|
+
def secure_socket!
|
127
|
+
socket = OpenSSL::SSL::SSLSocket.new(@socket, server.ssl_context)
|
128
|
+
socket.sync_close = true
|
129
|
+
@socket = socket.accept
|
122
130
|
end
|
123
131
|
|
124
132
|
def write(message)
|
@@ -141,5 +149,12 @@ module Minbox
|
|
141
149
|
def connected?
|
142
150
|
@socket
|
143
151
|
end
|
152
|
+
|
153
|
+
def authenticate(username, password)
|
154
|
+
logger.debug("#{username}:#{password}")
|
155
|
+
return write '535 Authenticated failed - protocol error' unless username && password
|
156
|
+
|
157
|
+
write '235 2.7.0 Authentication successful'
|
158
|
+
end
|
144
159
|
end
|
145
160
|
end
|
data/lib/minbox/core.rb
CHANGED
data/lib/minbox/inbox.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Minbox
|
4
|
+
class Inbox
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
def self.instance(root_dir:)
|
8
|
+
@instances ||= {}
|
9
|
+
@instances[root_dir] ||= new(root_dir: root_dir)
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(root_dir:)
|
13
|
+
start_listening(root_dir)
|
14
|
+
empty!
|
15
|
+
end
|
16
|
+
|
17
|
+
def emails(count: 0)
|
18
|
+
wait_until { |x| x.count >= count } if count > 0
|
19
|
+
@emails.values
|
20
|
+
end
|
21
|
+
|
22
|
+
def wait_until(seconds: 10, wait: 0.1)
|
23
|
+
iterations = (seconds / wait).to_i
|
24
|
+
iterations.times do
|
25
|
+
result = yield(self)
|
26
|
+
return result if result
|
27
|
+
|
28
|
+
sleep wait
|
29
|
+
end
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def wait_until!(*args, &block)
|
34
|
+
raise "timeout: expired. #{args}" unless wait_until(*args, &block)
|
35
|
+
end
|
36
|
+
|
37
|
+
def open(subject:)
|
38
|
+
wait_until do
|
39
|
+
emails.find do |email|
|
40
|
+
subject.is_a?(String) ? email.subject == subject : email.subject.match?(subject)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def empty!
|
46
|
+
@emails = {}
|
47
|
+
end
|
48
|
+
|
49
|
+
def each
|
50
|
+
@emails.each do |id, email|
|
51
|
+
yield email
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def changed(modified, added, removed)
|
58
|
+
added.each do |file|
|
59
|
+
mail = Mail.read(file)
|
60
|
+
Minbox.logger.debug("Received: #{mail.subject}")
|
61
|
+
@emails[File.basename(file)] = mail
|
62
|
+
end
|
63
|
+
removed.each do |file|
|
64
|
+
@emails.delete(File.basename(file))
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def listener_for(dir)
|
69
|
+
::Listen.to(File.expand_path(dir), only: /\.eml$/, &method(:changed))
|
70
|
+
end
|
71
|
+
|
72
|
+
def start_listening(root_dir)
|
73
|
+
listener_for(root_dir).start
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
data/lib/minbox/publisher.rb
CHANGED
@@ -1,40 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'redis'
|
2
4
|
|
3
5
|
module Minbox
|
4
|
-
class Publisher
|
5
|
-
attr_reader :publishers
|
6
|
-
|
7
|
-
def initialize(*publishers)
|
8
|
-
@publishers = Array(publishers)
|
9
|
-
end
|
10
|
-
|
11
|
-
def add(publisher)
|
12
|
-
publishers.push(publisher)
|
13
|
-
end
|
14
|
-
|
15
|
-
def publish(mail)
|
16
|
-
Thread.new do
|
17
|
-
Minbox.logger.debug("Publishing: #{mail.message_id}")
|
18
|
-
publishers.each { |x| x.publish(mail) }
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
def self.from(outputs)
|
23
|
-
publisher = Publisher.new
|
24
|
-
outputs.each do |x|
|
25
|
-
case x
|
26
|
-
when 'stdout'
|
27
|
-
publisher.add(LogPublisher.new)
|
28
|
-
when 'redis'
|
29
|
-
publisher.add(RedisPublisher.new)
|
30
|
-
when 'file'
|
31
|
-
publisher.add(FilePublisher.new)
|
32
|
-
end
|
33
|
-
end
|
34
|
-
publisher
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
6
|
class LogPublisher
|
39
7
|
def initialize(logger = Minbox.logger)
|
40
8
|
@logger = logger
|
@@ -51,7 +19,7 @@ module Minbox
|
|
51
19
|
end
|
52
20
|
|
53
21
|
def publish(mail)
|
54
|
-
@redis.publish(
|
22
|
+
@redis.publish('minbox', mail.to_s)
|
55
23
|
end
|
56
24
|
end
|
57
25
|
|
@@ -59,7 +27,7 @@ module Minbox
|
|
59
27
|
attr_reader :dir
|
60
28
|
|
61
29
|
def initialize(dir = Dir.pwd)
|
62
|
-
@dir = File.join(dir,
|
30
|
+
@dir = File.join(dir, 'tmp')
|
63
31
|
FileUtils.mkdir_p(@dir)
|
64
32
|
end
|
65
33
|
|
@@ -67,4 +35,38 @@ module Minbox
|
|
67
35
|
IO.write(File.join(dir, "#{Time.now.to_i}.eml"), mail.to_s)
|
68
36
|
end
|
69
37
|
end
|
38
|
+
|
39
|
+
class Publisher
|
40
|
+
REGISTERED_PUBLISHERS = {
|
41
|
+
stdout: LogPublisher,
|
42
|
+
redis: RedisPublisher,
|
43
|
+
file: FilePublisher,
|
44
|
+
}.freeze
|
45
|
+
|
46
|
+
attr_reader :publishers
|
47
|
+
|
48
|
+
def initialize(*publishers)
|
49
|
+
@publishers = Array(publishers)
|
50
|
+
end
|
51
|
+
|
52
|
+
def add(publisher)
|
53
|
+
publishers << publisher
|
54
|
+
end
|
55
|
+
|
56
|
+
def publish(mail)
|
57
|
+
Thread.new do
|
58
|
+
Minbox.logger.debug("Publishing: #{mail.message_id}")
|
59
|
+
publishers.each { |x| x.publish(mail) }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.from(outputs)
|
64
|
+
publisher = Publisher.new
|
65
|
+
outputs.each do |x|
|
66
|
+
clazz = REGISTERED_PUBLISHERS[x.to_sym]
|
67
|
+
publisher.add(clazz.new) if clazz
|
68
|
+
end
|
69
|
+
publisher
|
70
|
+
end
|
71
|
+
end
|
70
72
|
end
|
data/lib/minbox/server.rb
CHANGED
@@ -1,13 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Minbox
|
2
4
|
class Server
|
3
|
-
|
5
|
+
SUBJECT = '/C=CA/ST=AB/L=Calgary/O=minbox/OU=development/CN=minbox'
|
6
|
+
attr_reader :host, :logger, :key, :server
|
4
7
|
|
5
|
-
def initialize(host
|
8
|
+
def initialize(host: 'localhost', port: 25, tls: false, logger: Minbox.logger, thread_pool: Concurrent::CachedThreadPool.new)
|
6
9
|
@host = host
|
7
|
-
@port = port
|
8
10
|
@logger = logger
|
9
11
|
@tls = tls
|
10
12
|
@key = OpenSSL::PKey::RSA.new(2048)
|
13
|
+
logger.debug("Starting server on port #{port}...")
|
14
|
+
@server = TCPServer.new(port.to_i)
|
15
|
+
@thread_pool = thread_pool
|
11
16
|
end
|
12
17
|
|
13
18
|
def tls?
|
@@ -15,25 +20,25 @@ module Minbox
|
|
15
20
|
end
|
16
21
|
|
17
22
|
def listen!(&block)
|
18
|
-
logger.debug("Starting server on port #{port}...")
|
19
|
-
@server = TCPServer.new(port.to_i)
|
20
23
|
@server = upgrade(@server) if tls?
|
21
|
-
logger.debug(
|
24
|
+
logger.debug('Server started!')
|
22
25
|
|
23
26
|
loop do
|
24
|
-
handle(
|
27
|
+
handle(server.accept, &block)
|
25
28
|
rescue StandardError => error
|
26
29
|
logger.error(error)
|
27
30
|
end
|
28
31
|
end
|
29
32
|
|
30
33
|
def handle(socket, &block)
|
31
|
-
|
32
|
-
|
34
|
+
@thread_pool.post do
|
35
|
+
logger.debug("client connected: #{socket.inspect}")
|
36
|
+
Client.new(self, socket, logger).handle(&block)
|
37
|
+
end
|
33
38
|
end
|
34
39
|
|
35
40
|
def shutdown!
|
36
|
-
|
41
|
+
server&.close
|
37
42
|
end
|
38
43
|
|
39
44
|
def ssl_context
|
@@ -55,9 +60,8 @@ module Minbox
|
|
55
60
|
server
|
56
61
|
end
|
57
62
|
|
58
|
-
def certificate_for(private_key)
|
63
|
+
def certificate_for(private_key, subject = SUBJECT)
|
59
64
|
certificate = OpenSSL::X509::Certificate.new
|
60
|
-
subject = '/C=CA/ST=AB/L=Calgary/O=minbox/OU=development/CN=minbox'
|
61
65
|
certificate.subject = certificate.issuer = OpenSSL::X509::Name.parse(subject)
|
62
66
|
certificate.not_before = Time.now
|
63
67
|
certificate.not_after = certificate.not_before + 30 * 24 * 60 * 60 # 30 days
|
@@ -71,14 +75,14 @@ module Minbox
|
|
71
75
|
|
72
76
|
def apply_ski_extension_to(certificate)
|
73
77
|
extensions = OpenSSL::X509::ExtensionFactory.new
|
74
|
-
extensions.subject_certificate =
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
extensions.create_extension(
|
81
|
-
|
78
|
+
extensions.subject_certificate =
|
79
|
+
extensions.issuer_certificate = certificate
|
80
|
+
[
|
81
|
+
['subjectKeyIdentifier', 'hash', false],
|
82
|
+
['keyUsage', 'keyEncipherment,digitalSignature', true],
|
83
|
+
].each do |x|
|
84
|
+
certificate.add_extension(extensions.create_extension(x[0], x[1], x[2]))
|
85
|
+
end
|
82
86
|
end
|
83
87
|
end
|
84
88
|
end
|
data/lib/minbox/version.rb
CHANGED
data/minbox.gemspec
CHANGED
@@ -1,44 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
|
2
|
-
lib = File.expand_path(
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require
|
5
|
+
require 'minbox/version'
|
5
6
|
|
6
7
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
8
|
+
spec.name = 'minbox'
|
8
9
|
spec.version = Minbox::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
10
|
+
spec.authors = ['mo khan']
|
11
|
+
spec.email = ['mo@mokhan.ca']
|
11
12
|
|
12
|
-
spec.summary =
|
13
|
-
spec.description =
|
14
|
-
spec.homepage =
|
15
|
-
spec.license =
|
13
|
+
spec.summary = 'A minimal smtp server.'
|
14
|
+
spec.description = 'A minimal smtp server.'
|
15
|
+
spec.homepage = 'https://www.mokhan.ca/'
|
16
|
+
spec.license = 'MIT'
|
16
17
|
|
17
18
|
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
18
19
|
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
19
20
|
if spec.respond_to?(:metadata)
|
20
|
-
spec.metadata[
|
21
|
-
spec.metadata[
|
22
|
-
spec.metadata[
|
21
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
22
|
+
spec.metadata['source_code_uri'] = 'https://github.com/mokhan/minbox'
|
23
|
+
spec.metadata['changelog_uri'] = 'https://github.com/mokhan/minbox/blob/CHANGELOG.md'
|
23
24
|
else
|
24
|
-
raise
|
25
|
-
|
25
|
+
raise 'RubyGems 2.0 or newer is required to protect against ' \
|
26
|
+
'public gem pushes.'
|
26
27
|
end
|
27
28
|
|
28
29
|
# Specify which files should be added to the gem when it is released.
|
29
30
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
30
|
-
spec.files
|
31
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
31
32
|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
32
33
|
end
|
33
|
-
spec.bindir =
|
34
|
+
spec.bindir = 'exe'
|
34
35
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
35
|
-
spec.require_paths = [
|
36
|
+
spec.require_paths = ['lib']
|
36
37
|
|
37
|
-
spec.add_dependency
|
38
|
-
spec.add_dependency
|
39
|
-
spec.add_dependency
|
40
|
-
spec.
|
41
|
-
spec.
|
42
|
-
spec.
|
43
|
-
spec.add_development_dependency
|
38
|
+
spec.add_dependency 'concurrent-ruby', '~> 1.1'
|
39
|
+
spec.add_dependency 'hashie', '~> 3.6'
|
40
|
+
spec.add_dependency 'listen', '~> 3.1'
|
41
|
+
spec.add_dependency 'mail', '~> 2.7'
|
42
|
+
spec.add_dependency 'redis', '~> 4.1'
|
43
|
+
spec.add_dependency 'thor', '~> 0.20'
|
44
|
+
spec.add_development_dependency 'bundler', '~> 2.0'
|
45
|
+
spec.add_development_dependency 'bundler-audit', '~> 0.6'
|
46
|
+
spec.add_development_dependency 'faker', '~> 1.9'
|
47
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
48
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
49
|
+
spec.add_development_dependency 'rubocop', '~> 0.52'
|
50
|
+
spec.add_development_dependency 'rubocop-rspec', '~> 1.22'
|
44
51
|
end
|
metadata
CHANGED
@@ -1,15 +1,57 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: minbox
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- mo khan
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-04-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: concurrent-ruby
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: hashie
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.6'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.6'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: listen
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.1'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.1'
|
13
55
|
- !ruby/object:Gem::Dependency
|
14
56
|
name: mail
|
15
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -66,6 +108,20 @@ dependencies:
|
|
66
108
|
- - "~>"
|
67
109
|
- !ruby/object:Gem::Version
|
68
110
|
version: '2.0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: bundler-audit
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0.6'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0.6'
|
69
125
|
- !ruby/object:Gem::Dependency
|
70
126
|
name: faker
|
71
127
|
requirement: !ruby/object:Gem::Requirement
|
@@ -108,6 +164,34 @@ dependencies:
|
|
108
164
|
- - "~>"
|
109
165
|
- !ruby/object:Gem::Version
|
110
166
|
version: '3.0'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: rubocop
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - "~>"
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0.52'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - "~>"
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0.52'
|
181
|
+
- !ruby/object:Gem::Dependency
|
182
|
+
name: rubocop-rspec
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - "~>"
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: '1.22'
|
188
|
+
type: :development
|
189
|
+
prerelease: false
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - "~>"
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: '1.22'
|
111
195
|
description: A minimal smtp server.
|
112
196
|
email:
|
113
197
|
- mo@mokhan.ca
|
@@ -118,6 +202,7 @@ extra_rdoc_files: []
|
|
118
202
|
files:
|
119
203
|
- ".gitignore"
|
120
204
|
- ".rspec"
|
205
|
+
- ".rubocop.yml"
|
121
206
|
- ".ruby-version"
|
122
207
|
- ".travis.yml"
|
123
208
|
- Dockerfile
|
@@ -128,12 +213,14 @@ files:
|
|
128
213
|
- Rakefile
|
129
214
|
- bin/console
|
130
215
|
- bin/docker-build
|
216
|
+
- bin/lint
|
131
217
|
- bin/setup
|
132
218
|
- exe/minbox
|
133
219
|
- lib/minbox.rb
|
134
220
|
- lib/minbox/cli.rb
|
135
221
|
- lib/minbox/client.rb
|
136
222
|
- lib/minbox/core.rb
|
223
|
+
- lib/minbox/inbox.rb
|
137
224
|
- lib/minbox/publisher.rb
|
138
225
|
- lib/minbox/server.rb
|
139
226
|
- lib/minbox/version.rb
|