minbox 0.1.4 → 0.1.5

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: 439eb9bd7163b38e9c6455fce216b733349e6100dd75c10b96c85361eacc2746
4
- data.tar.gz: 9cdbefe1a8444a7a54ba0e0cfa196aaa957e65c68cf1c3a82c969e2ead289277
3
+ metadata.gz: f483bc9bcd047c68ad7203630dc57f223c3cce4c182c783cf099ef551b02c937
4
+ data.tar.gz: 2b313d59d6d84b3216250f15295a83800ad1eff6b54b5714a6492f7396293aae
5
5
  SHA512:
6
- metadata.gz: deacd82240ae942c4b8d18a03745dd64c3d12d6e52aa6104247764c83ff5429b03e8b36e2f49394a3c7bb1d238a5974632931b78d4943a7038ca2d9bcc7aae19
7
- data.tar.gz: 57007460c9b32be3fd34e1883d3891f080ba7e3cffb9a5cc043a4b3f158c0f3ee5558a1b8ae79d9117684c4a65132f0d5f4f3ec2e891944cf7619ba5a15d1986
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
@@ -6,5 +6,7 @@ RUN apk update && \
6
6
  rm -fr /var/cache/apk/* && \
7
7
  apk del build-base
8
8
  WORKDIR /opt/minbox
9
+ EXPOSE 25
10
+ VOLUME ["/opt/minbox/tmp"]
9
11
  RUN gem install minbox
10
12
  CMD ["minbox", "server", "localhost", "25", "--output=stdout file"]
data/Gemfile CHANGED
@@ -1,4 +1,6 @@
1
- source "https://rubygems.org"
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
2
4
 
3
5
  # Specify your gem's dependencies in minbox.gemspec
4
6
  gemspec
data/Gemfile.lock CHANGED
@@ -1,7 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- minbox (0.1.4)
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
- concurrent-ruby (1.1.4)
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
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/minbox`. To experiment with that code, run `bin/console` for an interactive prompt.
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
- TODO: Delete this and the text above, and describe your gem
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
- TODO: Write usage instructions here
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/[USERNAME]/minbox.
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
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
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 :default => :spec
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 "bundler/setup"
4
- require "minbox"
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 "irb"
14
+ require 'irb'
14
15
  IRB.start(__FILE__)
data/bin/docker-build CHANGED
@@ -5,6 +5,7 @@ mkdir -p pkg/
5
5
  cp Dockerfile pkg/
6
6
  docker pull ruby:2.6-alpine
7
7
  docker image build \
8
+ --no-cache \
8
9
  -t "mokhan/minbox:latest" \
9
10
  -f pkg/Dockerfile pkg/
10
11
 
data/bin/lint ADDED
@@ -0,0 +1,11 @@
1
+ #!/bin/sh
2
+
3
+ set -e
4
+
5
+ [ -z "$DEBUG" ] || set -x
6
+
7
+ echo [$(date "+%H:%M:%S")] "==> Running setup…"
8
+ bin/setup
9
+
10
+ echo [$(date "+%H:%M:%S")] "==> Running linters…"
11
+ bundle exec rake lint
data/exe/minbox CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
- require "minbox/cli"
4
+ require 'minbox/cli'
4
5
 
5
6
  Minbox::Cli::Application.start(ARGV)
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 "minbox/core"
6
- require "minbox/publisher"
7
- require "minbox/client"
8
- require "minbox/server"
9
- require "minbox/version"
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 "minbox"
12
-
13
- desc 'client <HOST> <PORT>', 'SMTP client'
14
- def client(host = 'localhost', port = 25)
15
- mail = Mail.new do
16
- from 'Your Name <me@example.org>'
17
- to 'Destination Address <them@example.com>'
18
- subject 'test message'
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(mail.to_s, 'me+1@example.org', 'them+1@example.com')
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]).listen! do |mail|
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
- module Minbox
2
- class Client
3
- attr_reader :server, :socket, :logger
1
+ # frozen_string_literal: true
4
2
 
5
- def initialize(server, socket, logger)
6
- @server = server
7
- @logger = logger
8
- @socket = socket
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
- def handle(&block)
12
- write "220 #{server.host} ESMTP"
13
- while connected? && (line = read)
14
- case line
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
- private
23
+ class Noop
24
+ def run(client, _line)
25
+ client.write '250 OK'
26
+ end
27
+ end
38
28
 
39
- def quit
40
- write "221 Bye"
41
- close
29
+ class Quit
30
+ def run(client, _line)
31
+ client.write '221 Bye'
32
+ client.close
42
33
  end
34
+ end
43
35
 
44
- def data(line, &block)
45
- write "354 End data with <CR><LF>.<CR><LF>"
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 "250 OK"
53
- block.call(Mail.new(body.join)) unless body.empty?
45
+ client.write '250 OK'
46
+ yield(Mail.new(body.join)) unless body.empty?
54
47
  end
48
+ end
55
49
 
56
- def rcpt_to(line)
57
- write "250 OK"
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
- def mail_from(line)
61
- write "250 OK"
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
- def ehlo(line)
65
- _ehlo, _client_domain = line.split(" ")
66
- write "250-#{server.host} offers a warm hug of welcome"
67
- write "250-8BITMIME"
68
- write "250-ENHANCEDSTATUSCODES"
69
- #write "250 STARTTLS"
70
- write "250-AUTH PLAIN LOGIN"
71
- write "250 OK"
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
- def helo(line)
75
- _ehlo, _client_domain = line.split(" ")
76
- write "250 #{server.host}"
77
- end
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
- def reset
88
- write '250 OK'
89
- end
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 noop
92
- write '250 OK'
108
+ def initialize(server, socket, logger)
109
+ @server = server
110
+ @logger = logger
111
+ @socket = socket
93
112
  end
94
113
 
95
- def auth_plain(line)
96
- data = line.gsub(/AUTH PLAIN ?/i, '')
97
- if data.strip == ''
98
- write '334'
99
- data = read
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
- parts = Base64.decode64(data).split("\0")
102
- username, password = parts[-2], parts[-1]
103
- logger.debug("#{username}:#{password}")
104
- return write '535 Authenticated failed - protocol error' unless username && password
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 auth_login(line)
109
- username = line.gsub!(/AUTH LOGIN ?/i, '')
110
- if username.strip == ''
111
- write '334 VXNlcm5hbWU6'
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Minbox
2
4
  def self.logger
3
5
  @logger ||= Logger.new(STDOUT)
@@ -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
@@ -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("minbox", mail.to_s)
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, "tmp")
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
- attr_reader :host, :port, :logger, :key
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 = 'localhost', port = 25, tls = false, logger = Minbox.logger)
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("Server started!")
24
+ logger.debug('Server started!')
22
25
 
23
26
  loop do
24
- handle(@server.accept, &block)
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
- logger.debug("client connected: #{socket.inspect}")
32
- Client.new(self, socket, logger).handle(&block)
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
- @server&.close
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 = certificate
75
- extensions.issuer_certificate = certificate
76
- certificate.add_extension(
77
- extensions.create_extension('subjectKeyIdentifier', 'hash', false)
78
- )
79
- certificate.add_extension(
80
- extensions.create_extension('keyUsage', 'keyEncipherment,digitalSignature', true)
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Minbox
2
- VERSION = "0.1.4"
4
+ VERSION = '0.1.5'
3
5
  end
data/minbox.gemspec CHANGED
@@ -1,44 +1,51 @@
1
+ # frozen_string_literal: true
1
2
 
2
- lib = File.expand_path("../lib", __FILE__)
3
+ lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "minbox/version"
5
+ require 'minbox/version'
5
6
 
6
7
  Gem::Specification.new do |spec|
7
- spec.name = "minbox"
8
+ spec.name = 'minbox'
8
9
  spec.version = Minbox::VERSION
9
- spec.authors = ["mo khan"]
10
- spec.email = ["mo@mokhan.ca"]
10
+ spec.authors = ['mo khan']
11
+ spec.email = ['mo@mokhan.ca']
11
12
 
12
- spec.summary = %q{A minimal smtp server.}
13
- spec.description = %q{A minimal smtp server.}
14
- spec.homepage = "https://www.mokhan.ca/"
15
- spec.license = "MIT"
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["homepage_uri"] = spec.homepage
21
- spec.metadata["source_code_uri"] = "https://github.com/mokhan/minbox"
22
- spec.metadata["changelog_uri"] = "https://github.com/mokhan/minbox/blob/CHANGELOG.md"
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 "RubyGems 2.0 or newer is required to protect against " \
25
- "public gem pushes."
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 = Dir.chdir(File.expand_path('..', __FILE__)) do
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 = "exe"
34
+ spec.bindir = 'exe'
34
35
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
35
- spec.require_paths = ["lib"]
36
+ spec.require_paths = ['lib']
36
37
 
37
- spec.add_dependency "mail", "~> 2.7"
38
- spec.add_dependency "redis", "~> 4.1"
39
- spec.add_dependency "thor", "~> 0.20"
40
- spec.add_development_dependency "bundler", "~> 2.0"
41
- spec.add_development_dependency "faker", "~> 1.9"
42
- spec.add_development_dependency "rake", "~> 10.0"
43
- spec.add_development_dependency "rspec", "~> 3.0"
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
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-03-10 00:00:00.000000000 Z
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