blitz_smtp 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --debug
3
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in blitz_smtp.gemspec
4
+ gemspec
5
+ gem 'rspec'
6
+ gem 'debugger'
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Maximilian Schneider
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # BlitzSmtp
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'blitz_smtp'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install blitz_smtp
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'blitz_smtp/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "blitz_smtp"
8
+ gem.version = BlitzSMTP::VERSION
9
+ gem.authors = ["Maximilian Schneider"]
10
+ gem.email = ["mail@maximilianschneider.net"]
11
+ gem.description = %q{BlitzSMTP implements RFC2920 (Command Pipelining) to achieve high message throughput}
12
+ gem.summary = %q{BlitzSMTP is no replacement for Net::SMTP, it's just faster}
13
+ gem.homepage = "https://github.com/mschneider/blitz_smtp"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+ end
data/lib/blitz_smtp.rb ADDED
@@ -0,0 +1,7 @@
1
+ require "blitz_smtp/version"
2
+ require "blitz_smtp/client"
3
+ require "blitz_smtp/message"
4
+ require "blitz_smtp/mock_server"
5
+
6
+ module BlitzSMTP
7
+ end
@@ -0,0 +1,87 @@
1
+ module BlitzSMTP
2
+ class Client
3
+
4
+ class AlreadyConnected < StandardError; end
5
+ class EHLOUnsupported < StandardError; end
6
+ class PipeliningUnsupported < StandardError; end
7
+ class NotConnected < StandardError; end
8
+
9
+ def initialize(address, port)
10
+ @server_address, @server_port = address, port
11
+ end
12
+
13
+ def connect
14
+ raise AlreadyConnected if connected?
15
+ @socket = TCPSocket.open @server_address, @server_port
16
+ read_response # ignore actual response FIXME
17
+ check_features
18
+ rescue
19
+ disconnect if connected?
20
+ raise
21
+ end
22
+
23
+ def disconnect
24
+ raise NotConnected unless connected?
25
+ send_command "QUIT"
26
+ read_response
27
+ @socket.close
28
+ @socket = nil
29
+ end
30
+
31
+ def connected?
32
+ not @socket.nil?
33
+ end
34
+
35
+ def send_message(from, to, message)
36
+ send_command "MAIL", "FROM:#{format_address(from)}"
37
+ send_command "RCPT", "TO:#{format_address(to)}"
38
+ send_command "DATA"
39
+ 3.times { read_response }
40
+ send_data message
41
+ read_response
42
+ end
43
+
44
+ protected
45
+
46
+ def format_address(address)
47
+ if address =~ /<.*>/
48
+ address
49
+ else
50
+ "<#{address}>"
51
+ end
52
+ end
53
+
54
+ def send_data(message)
55
+ Data.new(message).write_to(@socket)
56
+ end
57
+
58
+ def send_command(*args)
59
+ command = Command.create(*args)
60
+ command.write_to(@socket)
61
+ end
62
+
63
+ def read_response
64
+ Response.new.read_from(@socket)
65
+ end
66
+
67
+ def read_extended_response
68
+ responses = []
69
+ loop do
70
+ responses << read_response
71
+ break unless responses.last.continued?
72
+ end
73
+ responses
74
+ end
75
+
76
+ def check_features
77
+ send_command "EHLO localhost"
78
+ responses = read_extended_response
79
+ unless responses.first.status_code == 250
80
+ raise(EHLOUnsupported, "the server must implement RFC2920")
81
+ end
82
+ unless responses.any? { |r| r.message == "PIPELINING" }
83
+ raise(PipeliningUnsupported, "the server must implement RFC2920")
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,71 @@
1
+ module BlitzSMTP
2
+
3
+ class Message
4
+
5
+ class Invalid < StandardError; end
6
+
7
+ def initialize(string="")
8
+ @string = string
9
+ end
10
+
11
+ def to_s
12
+ @string
13
+ end
14
+
15
+ def terminator
16
+ "\r\n"
17
+ end
18
+
19
+ def write_to(stream)
20
+ #puts "#{stream.local_address.ip_port} << #{"#{self}#{terminator}".inspect}"
21
+ stream.print("#{self}#{terminator}")
22
+ end
23
+
24
+ def read_from(stream)
25
+ including_terminator = stream.gets(terminator)
26
+ #puts "#{stream.local_address.ip_port} >> #{(including_terminator).inspect}"
27
+ excluding_terminator = /^(.*)#{terminator}$/.match(including_terminator).to_a.last
28
+ @string = excluding_terminator
29
+ self
30
+ end
31
+ end
32
+
33
+ class Command < Message
34
+ def argument
35
+ @string[5..-1]
36
+ end
37
+
38
+ def name
39
+ @string[0..3]
40
+ end
41
+
42
+ def self.create(name, argument="")
43
+ new("#{name} #{argument}")
44
+ end
45
+ end
46
+
47
+ class Data < Message
48
+ def terminator
49
+ "\r\n.\r\n"
50
+ end
51
+ end
52
+
53
+ class Response < Message
54
+ def continued?
55
+ @string[3] == "-"
56
+ end
57
+
58
+ def message
59
+ @string[4..-1]
60
+ end
61
+
62
+ def status_code
63
+ @string[0..2].to_i
64
+ end
65
+
66
+ def self.create(status_code, message, continue=false)
67
+ space = continue ? "-" : " "
68
+ new("#{status_code}#{space}#{message}")
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,105 @@
1
+ require 'ostruct'
2
+ require 'socket'
3
+
4
+ module BlitzSMTP
5
+ class MockServer
6
+
7
+ attr_accessor :esmtp, :features, :accepted_emails
8
+
9
+ def initialize
10
+ @esmtp = true
11
+ @features = []
12
+ @accepted_emails = []
13
+ @server_socket = TCPServer.new(address, 0)
14
+ @server_thread = Thread.start do
15
+ loop do
16
+ @client_socket = @server_socket.accept
17
+ mock_smtp
18
+ end
19
+ end
20
+ end
21
+
22
+ def address
23
+ "localhost"
24
+ end
25
+
26
+ def port
27
+ @server_socket.connect_address.ip_port
28
+ end
29
+
30
+ def connected_to_client?
31
+ not @client_socket.nil?
32
+ end
33
+
34
+ def shutdown!
35
+ @server_thread.kill
36
+ @client_socket.close if connected_to_client?
37
+ @server_socket.close
38
+ end
39
+
40
+ protected
41
+
42
+ def disconnect_client
43
+ @client_socket.close
44
+ @client_socket = nil
45
+ end
46
+
47
+ def send_greeting
48
+ respond 220, self.class.to_s
49
+ end
50
+
51
+ def send_command_unknown(command)
52
+ respond 554, "received unknown command: #{command}"
53
+ end
54
+
55
+ def respond(*args)
56
+ response = Response.create(*args)
57
+ response.write_to(@client_socket)
58
+ end
59
+
60
+ def mock_smtp
61
+ send_greeting
62
+ catch(:disconnect) do
63
+ loop do
64
+ command = Command.new.read_from(@client_socket)
65
+ send("received_#{command.name.downcase}", command)
66
+ end
67
+ end
68
+ disconnect_client
69
+ end
70
+
71
+ def received_ehlo(command)
72
+ return send_command_unknown(command) unless esmtp
73
+ continued_messages = [address] + features[0..-2]
74
+ continued_messages.each { |m| respond 250, m, true }
75
+ respond 250, features.last
76
+ end
77
+
78
+ def received_mail(command)
79
+ @current_mail = { from: command.argument.to_s.gsub(/^FROM:/, '') }
80
+ respond 250, "ok"
81
+ end
82
+
83
+ def received_rcpt(command)
84
+ @current_mail[:to] = command.argument.to_s.gsub(/^TO:/, '')
85
+ respond 250, "ok"
86
+ end
87
+
88
+ def received_data(_)
89
+ respond 354, "start mail input"
90
+ @current_mail[:data] = Data.new.read_from(@client_socket).to_s
91
+ accepted_emails << OpenStruct.new(@current_mail)
92
+ respond 250, "ok"
93
+ end
94
+
95
+ def received_quit(_)
96
+ respond 221, "bye"
97
+ throw :disconnect
98
+ end
99
+
100
+ def method_missing(method, command, *_)
101
+ puts "could not identify #{command} [##{method}]"
102
+ send_command_unknown(command)
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,3 @@
1
+ module BlitzSMTP
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,78 @@
1
+ require 'spec_helper'
2
+
3
+ describe BlitzSMTP::Client do
4
+
5
+ before do
6
+ @server = BlitzSMTP::MockServer.new
7
+ @server.features = %w[PIPELINING 8BITMIME]
8
+ @client = BlitzSMTP::Client.new(@server.address, @server.port)
9
+ end
10
+
11
+ after do
12
+ @server.shutdown!
13
+ end
14
+
15
+ it "should initialize disconnected" do
16
+ @client.should_not be_connected
17
+ end
18
+
19
+ it "connects to the mock server" do
20
+ @client.connect
21
+ @client.should be_connected
22
+ @server.should be_connected_to_client
23
+ end
24
+
25
+ it "disconnects from the mock server" do
26
+ @client.connect
27
+ @client.disconnect
28
+ @client.should_not be_connected
29
+ @server.should_not be_connected_to_client
30
+ end
31
+
32
+ it "can't disconnect unless connected" do
33
+ lambda { @client.disconnect }.should \
34
+ raise_error(BlitzSMTP::Client::NotConnected)
35
+ end
36
+
37
+ it "can't connect if connected" do
38
+ @client.connect
39
+ lambda { @client.connect }.should \
40
+ raise_error(BlitzSMTP::Client::AlreadyConnected)
41
+ end
42
+
43
+ it "can connect again after disconnecting" do
44
+ @client.connect
45
+ @client.disconnect
46
+ @client.connect
47
+ @client.should be_connected
48
+ @server.should be_connected_to_client
49
+ end
50
+
51
+ it "can send an email" do
52
+ @client.connect
53
+ @client.send_message "mail@from.com", "mail@to.com", "message text"
54
+ last_mail = @server.accepted_emails.last
55
+ last_mail.from.should == "<mail@from.com>"
56
+ last_mail.to.should == "<mail@to.com>"
57
+ last_mail.data.should == "message text"
58
+ end
59
+
60
+ it "requires ESMTP" do
61
+ @server.esmtp = false
62
+ lambda { @client.connect }.should \
63
+ raise_error(BlitzSMTP::Client::EHLOUnsupported,
64
+ "the server must implement RFC2920")
65
+ @client.should_not be_connected
66
+ @server.should_not be_connected_to_client
67
+ end
68
+
69
+ it "requires the feature PIPELINING" do
70
+ @server.features.delete("PIPELINING")
71
+ lambda { @client.connect }.should \
72
+ raise_error(BlitzSMTP::Client::PipeliningUnsupported,
73
+ "the server must implement RFC2920")
74
+ @client.should_not be_connected
75
+ @server.should_not be_connected_to_client
76
+ end
77
+
78
+ end
@@ -0,0 +1,19 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+ require 'blitz_smtp'
8
+
9
+ RSpec.configure do |config|
10
+ config.treat_symbols_as_metadata_keys_with_true_values = true
11
+ config.run_all_when_everything_filtered = true
12
+ config.filter_run :focus
13
+
14
+ # Run specs in random order to surface order dependencies. If you find an
15
+ # order dependency and want to debug it, you can fix the order by providing
16
+ # the seed, which is printed after each run.
17
+ # --seed 1234
18
+ config.order = 'random'
19
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: blitz_smtp
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Maximilian Schneider
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-12-17 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: BlitzSMTP implements RFC2920 (Command Pipelining) to achieve high message
15
+ throughput
16
+ email:
17
+ - mail@maximilianschneider.net
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - .gitignore
23
+ - .rspec
24
+ - Gemfile
25
+ - LICENSE.txt
26
+ - README.md
27
+ - Rakefile
28
+ - blitz_smtp.gemspec
29
+ - lib/blitz_smtp.rb
30
+ - lib/blitz_smtp/client.rb
31
+ - lib/blitz_smtp/message.rb
32
+ - lib/blitz_smtp/mock_server.rb
33
+ - lib/blitz_smtp/version.rb
34
+ - spec/client_spec.rb
35
+ - spec/spec_helper.rb
36
+ homepage: https://github.com/mschneider/blitz_smtp
37
+ licenses: []
38
+ post_install_message:
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ none: false
44
+ requirements:
45
+ - - ! '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ requirements: []
55
+ rubyforge_project:
56
+ rubygems_version: 1.8.24
57
+ signing_key:
58
+ specification_version: 3
59
+ summary: BlitzSMTP is no replacement for Net::SMTP, it's just faster
60
+ test_files:
61
+ - spec/client_spec.rb
62
+ - spec/spec_helper.rb