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 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