celluloid-smtp 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.gitmodules +3 -0
- data/CHANGES.md +2 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +21 -0
- data/README.md +6 -0
- data/Rakefile +1 -0
- data/celluloid-smtp.gemspec +21 -0
- data/events.rb +0 -0
- data/examples/basic_server.rb +17 -0
- data/examples/test_load.rb +78 -0
- data/examples/test_messages.rb +56 -0
- data/lib/celluloid/smtp.rb +26 -0
- data/lib/celluloid/smtp/connection.rb +54 -0
- data/lib/celluloid/smtp/connection/automata.rb +70 -0
- data/lib/celluloid/smtp/connection/errors.rb +97 -0
- data/lib/celluloid/smtp/connection/events.rb +41 -0
- data/lib/celluloid/smtp/connection/parser.rb +184 -0
- data/lib/celluloid/smtp/constants.rb +15 -0
- data/lib/celluloid/smtp/extensions.rb +7 -0
- data/lib/celluloid/smtp/logging.rb +18 -0
- data/lib/celluloid/smtp/server.rb +94 -0
- data/lib/celluloid/smtp/server/handler.rb +11 -0
- data/lib/celluloid/smtp/server/protector.rb +20 -0
- data/lib/celluloid/smtp/server/transporter.rb +9 -0
- data/lib/celluloid/smtp/version.rb +3 -0
- metadata +83 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c5a69f2ed269acb76e89ee43e1e60412dc5a3a03
|
4
|
+
data.tar.gz: ce63f363549dc3772f9e4196801a7b4ea41d14dc
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3ee9844f6d97980fdda68a8cd9d5f7f41c5ffc410a95ffdb44a086fb3ea206ca51e9e0adbc67f8ad9a68b5504c81c6a1f5fb18369400b912f67802517cc35b81
|
7
|
+
data.tar.gz: 7d222884724f3df4ebfaef75ab5c1409307606b909b23f6e21aa7c9c37fb8cd8f30b1eca8ddea2dc4587b690f3dcdce35ef4848451304061057a8133a1b5bfa4
|
data/.gitignore
ADDED
data/.gitmodules
ADDED
data/CHANGES.md
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
|
3
|
+
gemspec
|
4
|
+
|
5
|
+
gem 'celluloid', github: 'celluloid/celluloid', branch: '0.17.0-prerelease', submodules: true
|
6
|
+
gem 'celluloid-io', github: 'celluloid/celluloid-io', branch: '0.17.0-dependent', submodules: true
|
7
|
+
|
8
|
+
#de For debugging. Watching for thread leaks.
|
9
|
+
gem 'cellumon', github: 'digitalextremist/cellumon', branch: 'master'
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2014 //de ( digitalextremist // )
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
# cellulloid-smtp
|
2
|
+
|
3
|
+
Evented, actor-based SMTP server built on [Celluloid::IO](http://github.com/celluloid/celluloid-io).
|
4
|
+
|
5
|
+
Inspired by [midi-smtp-server](https://github.com/4commerce-technologies-AG/midi-smtp-server),
|
6
|
+
but mostly based on [Reel](http://github.com/celluloid/reel).
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require File.expand_path("../culture/sync", __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.name = 'celluloid-smtp'
|
5
|
+
gem.version = Celluloid::SMTP::VERSION
|
6
|
+
|
7
|
+
gem.summary = "Celluloid based SMTP server."
|
8
|
+
gem.description = "A small, fast, evented, actor-based, highly customizable Ruby SMTP server."
|
9
|
+
|
10
|
+
gem.authors = ["digitalextremist //"]
|
11
|
+
gem.email = 'code@extremist.digital'
|
12
|
+
|
13
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
14
|
+
gem.files = `git ls-files`.split("\n")
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
|
17
|
+
gem.homepage = 'https://github.com/abstractive/celluloid-smtp'
|
18
|
+
gem.license = 'MIT'
|
19
|
+
|
20
|
+
gem.add_development_dependency "mail"
|
21
|
+
end
|
data/events.rb
ADDED
File without changes
|
@@ -0,0 +1,17 @@
|
|
1
|
+
$LOAD_PATH.push(File.expand_path("../../lib", __FILE__))
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'celluloid/smtp'
|
5
|
+
|
6
|
+
at_exit {
|
7
|
+
Celluloid::Internals::Logger.info "Shutting down SMTP server."
|
8
|
+
Celluloid[:smtpd].async.shutdown
|
9
|
+
sleep 0.126 * 2
|
10
|
+
Celluloid.shutdown
|
11
|
+
}
|
12
|
+
|
13
|
+
begin
|
14
|
+
Celluloid::SMTP::Server.run
|
15
|
+
rescue Interrupt
|
16
|
+
exit
|
17
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
$LOAD_PATH.push(File.expand_path("../../lib", __FILE__))
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'celluloid/current'
|
5
|
+
require 'mail'
|
6
|
+
|
7
|
+
TESTS = 1000
|
8
|
+
THREADS = 4
|
9
|
+
|
10
|
+
HOST = ARGV[0] || "localhost"
|
11
|
+
PORT = (ARGV[1] || 2525).to_i
|
12
|
+
TO = ARGV[2] || "smtp@celluloid.io"
|
13
|
+
FROM = ARGV[3] || TO
|
14
|
+
|
15
|
+
Mail.defaults do
|
16
|
+
delivery_method :smtp, address: HOST, port: PORT
|
17
|
+
end
|
18
|
+
|
19
|
+
fail "No TO address specified." unless TO
|
20
|
+
|
21
|
+
puts "Simulating sending #{TESTS}x#{THREADS} messages through #{HOST}@#{PORT}."
|
22
|
+
|
23
|
+
class Sender
|
24
|
+
include Celluloid
|
25
|
+
def tests(count)
|
26
|
+
times = count.times.map { future.test }
|
27
|
+
values = times.map(&:value)
|
28
|
+
failures = values.select { |v| v == :fail }.count
|
29
|
+
values.select! { |v| v != :fail }
|
30
|
+
average = values.inject{ |sum, t| sum + t }.to_f / values.size
|
31
|
+
return [0, failures] if failures == count
|
32
|
+
[average,failures]
|
33
|
+
end
|
34
|
+
def test
|
35
|
+
start = Time.now
|
36
|
+
mail = Mail.new do
|
37
|
+
from FROM
|
38
|
+
to TO
|
39
|
+
subject 'Test email.'
|
40
|
+
body "Test message.... #{Time.now}"
|
41
|
+
end
|
42
|
+
|
43
|
+
begin
|
44
|
+
mail.deliver
|
45
|
+
print "."
|
46
|
+
return Time.now.to_f - start.to_f
|
47
|
+
rescue => ex
|
48
|
+
print "!"
|
49
|
+
:fail
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
senders = THREADS.times.map { Sender.new }
|
55
|
+
|
56
|
+
start = Time.now
|
57
|
+
tests = senders.map {|sender| sender.future.tests(TESTS) }
|
58
|
+
|
59
|
+
values = tests.map(&:value)
|
60
|
+
total = Time.now-start
|
61
|
+
total_fail = false
|
62
|
+
|
63
|
+
puts "\n\n\n* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *"
|
64
|
+
failures = values.map { |v| v[1] }.inject{ |sum,f| sum + f }
|
65
|
+
average = values.map { |v| v[0] }.inject{ |sum, t| sum + t }.to_f / values.size
|
66
|
+
overall_average = total/(TESTS*THREADS)
|
67
|
+
if total_fail
|
68
|
+
puts "\n All #{THREADS*TESTS} messages failed."
|
69
|
+
total_fail = true
|
70
|
+
end
|
71
|
+
puts "\n Failures: #{failures}"
|
72
|
+
puts "\n Average time for #{TESTS}x#{THREADS} messages:"
|
73
|
+
puts " ~#{"%0.4f" % average} seconds per message, actual estimate." unless total_fail
|
74
|
+
puts " ~#{"%0.4f" % (overall_average)} of overall runtime, per message."
|
75
|
+
puts "\n Total time running test: #{"%0.4f" % total} seconds:"
|
76
|
+
puts " ~#{"%0.4f" % (1/average)} messages per second, actual estimate." unless total_fail
|
77
|
+
puts " ~#{"%0.4f" % (1/overall_average)} messages per second, overall."
|
78
|
+
puts "\n* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n\n\n"
|
@@ -0,0 +1,56 @@
|
|
1
|
+
$LOAD_PATH.push(File.expand_path("../../lib", __FILE__))
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'mail'
|
5
|
+
|
6
|
+
INTERVAL = 2.22
|
7
|
+
|
8
|
+
Mail.defaults do
|
9
|
+
delivery_method :smtp, address: "localhost", port: 2525
|
10
|
+
end
|
11
|
+
|
12
|
+
TO = "smtp@celluloid.io"
|
13
|
+
FROM = TO
|
14
|
+
|
15
|
+
fail "No TO address specified." unless TO
|
16
|
+
|
17
|
+
puts "Simulating sending a message every #{INTERVAL} seconds."
|
18
|
+
|
19
|
+
futures = []
|
20
|
+
@mutex = Mutex.new
|
21
|
+
begin
|
22
|
+
Thread.new {
|
23
|
+
@mutex.synchronize {
|
24
|
+
loop {
|
25
|
+
futures = Thread.new {
|
26
|
+
start = Time.now
|
27
|
+
mail = Mail.new do
|
28
|
+
from FROM
|
29
|
+
to TO
|
30
|
+
subject 'Test email.'
|
31
|
+
body "Test message.... #{start}"
|
32
|
+
end
|
33
|
+
|
34
|
+
begin
|
35
|
+
mail.deliver
|
36
|
+
print "|"
|
37
|
+
rescue Errno::ECONNREFUSED
|
38
|
+
print "X"
|
39
|
+
rescue EOFError
|
40
|
+
print "?"
|
41
|
+
rescue => ex
|
42
|
+
print "!"
|
43
|
+
STDERR.puts "Error communicating with server: #{ex} (#{ex.class})"
|
44
|
+
end
|
45
|
+
}
|
46
|
+
sleep INTERVAL
|
47
|
+
}
|
48
|
+
}
|
49
|
+
}
|
50
|
+
|
51
|
+
loop {
|
52
|
+
future = @mutex.synchronize { futures.shift }.value rescue nil
|
53
|
+
}
|
54
|
+
rescue Interrupt
|
55
|
+
puts "Done testing."
|
56
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'celluloid/current'
|
2
|
+
require 'celluloid/io'
|
3
|
+
|
4
|
+
module Celluloid
|
5
|
+
module SMTP
|
6
|
+
require 'celluloid/smtp/constants'
|
7
|
+
require 'celluloid/smtp/logging'
|
8
|
+
require 'celluloid/smtp/version'
|
9
|
+
require 'celluloid/smtp/extensions'
|
10
|
+
class Server
|
11
|
+
include SMTP::Extensions
|
12
|
+
require 'celluloid/smtp/server'
|
13
|
+
require 'celluloid/smtp/server/protector'
|
14
|
+
require 'celluloid/smtp/server/handler'
|
15
|
+
require 'celluloid/smtp/server/transporter'
|
16
|
+
end
|
17
|
+
class Connection
|
18
|
+
include SMTP::Extensions
|
19
|
+
require 'celluloid/smtp/connection/errors'
|
20
|
+
require 'celluloid/smtp/connection/events'
|
21
|
+
require 'celluloid/smtp/connection/parser'
|
22
|
+
require 'celluloid/smtp/connection/automata'
|
23
|
+
require 'celluloid/smtp/connection'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
class Celluloid::SMTP::Connection
|
2
|
+
|
3
|
+
attr_reader :socket, :automata
|
4
|
+
|
5
|
+
extend Forwardable
|
6
|
+
def_delegators :@socket, :close, :peeraddr, :print, :closed?
|
7
|
+
def_delegators :@automata, :transition
|
8
|
+
|
9
|
+
def initialize(socket, configuration)
|
10
|
+
@automata = Automata.new(self)
|
11
|
+
@configuration = configuration.dup
|
12
|
+
@socket = socket
|
13
|
+
@timestamps = {}
|
14
|
+
@context = nil
|
15
|
+
@behavior = configuration.fetch(:behavior, DEFAULT_BEHAVIOR)
|
16
|
+
transition :connection
|
17
|
+
end
|
18
|
+
|
19
|
+
def start!
|
20
|
+
@timestamps[:start] = Time.now
|
21
|
+
end
|
22
|
+
|
23
|
+
def finish!
|
24
|
+
@timestamps[:finish] = Time.now
|
25
|
+
end
|
26
|
+
|
27
|
+
def length
|
28
|
+
raise "Connection incomplete." unless @timestamps[:start] && @timestamps[:finish]
|
29
|
+
@timestamps[:finish].to_f - @timestamps[:start].to_f
|
30
|
+
end
|
31
|
+
|
32
|
+
def relaying?
|
33
|
+
@behavior == :relay
|
34
|
+
end
|
35
|
+
|
36
|
+
def delivering?
|
37
|
+
@behavior == :deliver
|
38
|
+
end
|
39
|
+
|
40
|
+
def print!(string)
|
41
|
+
print "#{string}\r\n"
|
42
|
+
end
|
43
|
+
|
44
|
+
def remote_ip
|
45
|
+
peeraddr(false)[3]
|
46
|
+
end
|
47
|
+
alias remote_addr remote_ip
|
48
|
+
|
49
|
+
def remote_host
|
50
|
+
# NOTE: This is currently a blocking operation.
|
51
|
+
peeraddr(true)[2]
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
class Celluloid::SMTP::Connection::Automata
|
2
|
+
include Celluloid::FSM
|
3
|
+
include Celluloid::SMTP::Extensions
|
4
|
+
def_delegators :@connection,
|
5
|
+
:delivering?,
|
6
|
+
:relaying?,
|
7
|
+
:closed?,
|
8
|
+
:start!,
|
9
|
+
:finish!,
|
10
|
+
:print,
|
11
|
+
:length,
|
12
|
+
:event!,
|
13
|
+
:handle!
|
14
|
+
|
15
|
+
def initialize(connection)
|
16
|
+
@connection = connection
|
17
|
+
end
|
18
|
+
|
19
|
+
default_state :initialize
|
20
|
+
|
21
|
+
state :connection, :to => [:handling, :closed] do
|
22
|
+
start!
|
23
|
+
event!(:on_connection)
|
24
|
+
transition :handling
|
25
|
+
end
|
26
|
+
|
27
|
+
state :handling, :to => [:handled, :disconnecting, :closed] do
|
28
|
+
debug "Parsing message." if DEBUG_AUTOMATA
|
29
|
+
handle!
|
30
|
+
end
|
31
|
+
|
32
|
+
state :handled, :to => [:relaying, :delivering, :disconnecting, :closed] do
|
33
|
+
debug "Finished handling." if DEBUG_AUTOMATA
|
34
|
+
if relaying?
|
35
|
+
transition :relaying
|
36
|
+
elsif delivering?
|
37
|
+
transition :delivering
|
38
|
+
else
|
39
|
+
transition :disconnecting
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
state :relaying, to: [:relayed, :closed] do
|
44
|
+
debug "Relaying message." if DEBUG_AUTOMATA
|
45
|
+
end
|
46
|
+
|
47
|
+
state :relayed, to: [:disconnecting, :closed] do
|
48
|
+
debug "Message relayed." if DEBUG_AUTOMATA
|
49
|
+
end
|
50
|
+
|
51
|
+
state :delivering, to: [:delivered, :closed] do
|
52
|
+
debug "Delivering message." if DEBUG_AUTOMATA
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
state :delivered, to: [:disconnecting, :closed] do
|
57
|
+
debug "Message delivered." if DEBUG_AUTOMATA
|
58
|
+
end
|
59
|
+
|
60
|
+
state :disconnecting, to: [:closed] do
|
61
|
+
event!(:on_disconnect)
|
62
|
+
transition :closed
|
63
|
+
end
|
64
|
+
|
65
|
+
state :closed, :to => [] do
|
66
|
+
close unless closed? rescue nil
|
67
|
+
finish!
|
68
|
+
debug "TIMER: #{"%0.4f" % length} on connection." if DEBUG_TIMING
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module Celluloid::SMTP
|
2
|
+
class Exception < ::Exception
|
3
|
+
attr_reader :code, :text
|
4
|
+
def initialize(msg=nil, code, text)
|
5
|
+
@code = code
|
6
|
+
@text = text
|
7
|
+
super msg
|
8
|
+
end
|
9
|
+
def result
|
10
|
+
"#{@code} #{@text}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Error421 < Exception
|
15
|
+
def initialize(msg="")
|
16
|
+
super msg, 421, "Service not available, closing transmission channel"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Error450 < Exception
|
21
|
+
def initialize(msg="")
|
22
|
+
super msg, 450, "Requested mail action not taken: mailbox unavailable"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Error451 < Exception
|
27
|
+
def initialize(msg="")
|
28
|
+
super msg, 451, "Requested action aborted: local error in processing"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class Error452 < Exception
|
33
|
+
def initialize(msg="")
|
34
|
+
super msg, 452, "Requested action not taken: insufficient system storage"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class Error500 < Exception
|
39
|
+
def initialize(msg="")
|
40
|
+
super msg, 500, "Syntax error, command unrecognised or error in parameters or arguments"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class Error501 < Exception
|
45
|
+
def initialize(msg="")
|
46
|
+
super msg, 501, "Syntax error in parameters or arguments"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class Error502 < Exception
|
51
|
+
def initialize(msg="")
|
52
|
+
super msg, 502, "Command not implemented"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class Error503 < Exception
|
57
|
+
def initialize(msg="")
|
58
|
+
super msg, 503, "Bad sequence of commands"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class Error504 < Exception
|
63
|
+
def initialize(msg="")
|
64
|
+
super msg, 504, "Command parameter not implemented"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class Error521 < Exception
|
69
|
+
def initialize(msg="")
|
70
|
+
super msg, 521, "Service does not accept mail"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class Error550 < Exception
|
75
|
+
def initialize(msg="")
|
76
|
+
super msg, 550, "Requested action not taken: mailbox unavailable"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class Error552 < Exception
|
81
|
+
def initialize(msg="")
|
82
|
+
super msg, 552, "Requested mail action aborted: exceeded storage allocation"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
class Error553 < Exception
|
87
|
+
def initialize(msg="")
|
88
|
+
super msg, 553, "Requested action not taken: mailbox name not allowed"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
class Error554 < Exception
|
93
|
+
def initialize(msg="")
|
94
|
+
super msg, 554, "Transaction failed"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class Celluloid::SMTP::Connection
|
2
|
+
|
3
|
+
def event!(method,*args)
|
4
|
+
start = Time.now
|
5
|
+
debug("Executing event: #{method}") if DEBUG_EVENTS
|
6
|
+
result = send(method,*args)
|
7
|
+
debug("TIMER: #{"%0.4f" %(Time.now-start)} on event: #{method}") if DEBUG_TIMING
|
8
|
+
result
|
9
|
+
rescue => ex
|
10
|
+
exception(ex, "Failure in event processor: #{method}")
|
11
|
+
nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def on_connection
|
15
|
+
debug("Client connected.") if DEBUG_EVENTS
|
16
|
+
end
|
17
|
+
|
18
|
+
def on_disconnect
|
19
|
+
debug("Disconnecting client.") if DEBUG_EVENTS
|
20
|
+
end
|
21
|
+
|
22
|
+
def on_helo(helo)
|
23
|
+
debug("HELO: #{helo}") if DEBUG_EVENTS
|
24
|
+
return helo
|
25
|
+
end
|
26
|
+
|
27
|
+
def on_mail_from(from)
|
28
|
+
debug("MAIL FROM: #{from}") if DEBUG_EVENTS
|
29
|
+
return from
|
30
|
+
end
|
31
|
+
|
32
|
+
def on_rcpt_to(to)
|
33
|
+
debug("RCPT TO: #{to}") if DEBUG_EVENTS
|
34
|
+
return to
|
35
|
+
end
|
36
|
+
|
37
|
+
def on_message(message)
|
38
|
+
debug("MESSAGE") if DEBUG_EVENTS
|
39
|
+
return message[:data]
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
class Celluloid::SMTP::Connection
|
2
|
+
|
3
|
+
include Celluloid::SMTP
|
4
|
+
|
5
|
+
def handle!
|
6
|
+
context!(true)
|
7
|
+
if process!
|
8
|
+
transition :handled
|
9
|
+
else
|
10
|
+
transition :disconnecting
|
11
|
+
end
|
12
|
+
rescue => ex
|
13
|
+
exception(ex, "Error handling command session")
|
14
|
+
transition :closed
|
15
|
+
end
|
16
|
+
|
17
|
+
[:data, :quit, :helo, :mail, :rcpt, :rset].each { |command|
|
18
|
+
define_method(:"#{command}?") { @sequence == command }
|
19
|
+
define_method(:"#{command}!") { @sequence = command }
|
20
|
+
}
|
21
|
+
|
22
|
+
def context!(first=false)
|
23
|
+
@helo = nil if first
|
24
|
+
@sequence = first ? :helo : :rset
|
25
|
+
@context = {} if first || @context.nil?
|
26
|
+
@context[:envelope] = {from: "", to: []}
|
27
|
+
@context[:message] = {delivered: -1, bytesize: -1, data: ""}
|
28
|
+
end
|
29
|
+
|
30
|
+
def envelope; @context[:envelope] ||= {} end
|
31
|
+
def message; @context[:message] ||= {} end
|
32
|
+
|
33
|
+
def process!
|
34
|
+
print! "220 #{@configuration[:hostname]} says welcome!"
|
35
|
+
begin
|
36
|
+
loop {
|
37
|
+
output = begin
|
38
|
+
data = @socket.readline
|
39
|
+
debug(">> #{data.chomp}") unless data? if DEBUG
|
40
|
+
line! data
|
41
|
+
rescue Celluloid::SMTP::Exception => ex
|
42
|
+
exception(ex, "Processing error")
|
43
|
+
rescue => ex
|
44
|
+
exception(ex, "Unknown exception")
|
45
|
+
Error500.new.result
|
46
|
+
end
|
47
|
+
unless output.empty?
|
48
|
+
debug("<< #{output}") if DEBUG
|
49
|
+
print! output
|
50
|
+
end
|
51
|
+
break if quit? || closed?
|
52
|
+
}
|
53
|
+
print! "221 Service closing transmission channel" unless closed?
|
54
|
+
return true
|
55
|
+
rescue EOFError
|
56
|
+
debug("Lost connection due to client abort.")
|
57
|
+
rescue Exception => ex
|
58
|
+
exception(ex, "Error parsing command session")
|
59
|
+
print! Error421.new.result unless closed?
|
60
|
+
end
|
61
|
+
false
|
62
|
+
end
|
63
|
+
|
64
|
+
def line!(data)
|
65
|
+
unless data?
|
66
|
+
case data
|
67
|
+
when (/^(HELO|EHLO)(\s+.*)?$/i) # HELO/EHLO
|
68
|
+
# 250 Requested mail action okay, completed
|
69
|
+
# 421 <domain> Service not available, closing transmission channel
|
70
|
+
# 500 Syntax error, command unrecognised
|
71
|
+
# 501 Syntax error in parameters or arguments
|
72
|
+
# 504 Command parameter not implemented
|
73
|
+
# 521 <domain> does not accept mail [rfc1846]
|
74
|
+
raise Error503 unless helo?
|
75
|
+
data = data.gsub(/^(HELO|EHLO)\ /i, '').strip
|
76
|
+
if return_value = event!(:on_helo, data)
|
77
|
+
data = return_value
|
78
|
+
end
|
79
|
+
@helo = data
|
80
|
+
rset!
|
81
|
+
return "250 OK"
|
82
|
+
when (/^NOOP\s*$/i) # NOOP
|
83
|
+
# 250 Requested mail action okay, completed
|
84
|
+
# 421 <domain> Service not available, closing transmission channel
|
85
|
+
# 500 Syntax error, command unrecognised
|
86
|
+
return "250 OK"
|
87
|
+
when (/^RSET\s*$/i) # RSET
|
88
|
+
# 250 Requested mail action okay, completed
|
89
|
+
# 421 <domain> Service not available, closing transmission channel
|
90
|
+
# 500 Syntax error, command unrecognised
|
91
|
+
# 501 Syntax error in parameters or arguments
|
92
|
+
raise Error503 if helo?
|
93
|
+
# handle command
|
94
|
+
context!
|
95
|
+
return "250 OK"
|
96
|
+
when (/^QUIT\s*$/i) # QUIT
|
97
|
+
# 221 <domain> Service closing transmission channel
|
98
|
+
# 500 Syntax error, command unrecognised
|
99
|
+
quit!
|
100
|
+
return ""
|
101
|
+
when (/^MAIL FROM\:/i) # MAIL
|
102
|
+
# 250 Requested mail action okay, completed
|
103
|
+
# 421 <domain> Service not available, closing transmission channel
|
104
|
+
# 451 Requested action aborted: local error in processing
|
105
|
+
# 452 Requested action not taken: insufficient system storage
|
106
|
+
# 500 Syntax error, command unrecognised
|
107
|
+
# 501 Syntax error in parameters or arguments
|
108
|
+
# 552 Requested mail action aborted: exceeded storage allocation
|
109
|
+
raise Error503 unless rset?
|
110
|
+
data = data.gsub(/^MAIL FROM\:/i, '').strip
|
111
|
+
if return_value = event!(:on_mail_from, data)
|
112
|
+
data = return_value
|
113
|
+
end
|
114
|
+
envelope[:from] = data
|
115
|
+
mail!
|
116
|
+
return "250 OK"
|
117
|
+
when (/^RCPT TO\:/i) # RCPT
|
118
|
+
# 250 Requested mail action okay, completed
|
119
|
+
# 251 User not local; will forward to <forward-path>
|
120
|
+
# 421 <domain> Service not available, closing transmission channel
|
121
|
+
# 450 Requested mail action not taken: mailbox unavailable
|
122
|
+
# 451 Requested action aborted: local error in processing
|
123
|
+
# 452 Requested action not taken: insufficient system storage
|
124
|
+
# 500 Syntax error, command unrecognised
|
125
|
+
# 501 Syntax error in parameters or arguments
|
126
|
+
# 503 Bad sequence of commands
|
127
|
+
# 521 <domain> does not accept mail [rfc1846]
|
128
|
+
# 550 Requested action not taken: mailbox unavailable
|
129
|
+
# 551 User not local; please try <forward-path>
|
130
|
+
# 552 Requested mail action aborted: exceeded storage allocation
|
131
|
+
# 553 Requested action not taken: mailbox name not allowed
|
132
|
+
raise Error503 unless mail? || rset?
|
133
|
+
data = data.gsub(/^RCPT TO\:/i, '').strip
|
134
|
+
if return_value = event!(:on_rcpt_to, data)
|
135
|
+
data = return_value
|
136
|
+
end
|
137
|
+
envelope[:to] << data
|
138
|
+
rset!
|
139
|
+
return "250 OK"
|
140
|
+
when (/^DATA\s*$/i) # DATA
|
141
|
+
# 354 Start mail input; end with <CRLF>.<CRLF>
|
142
|
+
# 250 Requested mail action okay, completed
|
143
|
+
# 421 <domain> Service not available, closing transmission channel received data
|
144
|
+
# 451 Requested action aborted: local error in processing
|
145
|
+
# 452 Requested action not taken: insufficient system storage
|
146
|
+
# 500 Syntax error, command unrecognised
|
147
|
+
# 501 Syntax error in parameters or arguments
|
148
|
+
# 503 Bad sequence of commands
|
149
|
+
# 552 Requested mail action aborted: exceeded storage allocation
|
150
|
+
# 554 Transaction failed
|
151
|
+
raise Error503 unless rset?
|
152
|
+
data!
|
153
|
+
return "354 Enter message, ending with \".\" on a data by itself"
|
154
|
+
else
|
155
|
+
raise Error500
|
156
|
+
end
|
157
|
+
else # Data mode.
|
158
|
+
if (data.chomp =~ /^\.$/) # Line with only a period; being told to exit data mode.
|
159
|
+
message[:data] += data
|
160
|
+
message[:data].gsub!(/\r\n\Z/, '').gsub!(/\.\Z/, '') # remove ending line .
|
161
|
+
begin
|
162
|
+
if return_value = event!(:on_message, @context)
|
163
|
+
message[:data] = return_value
|
164
|
+
end
|
165
|
+
message[:delivered] = Time.now.utc # save delivered time
|
166
|
+
message[:bytesize] = message[:data].bytesize # save bytesize of message data
|
167
|
+
return "250 Requested mail action okay, completed"
|
168
|
+
|
169
|
+
rescue Celluloid::SMTP::Exception
|
170
|
+
raise
|
171
|
+
rescue Exception => ex
|
172
|
+
raise Error451.new("#{ex}")
|
173
|
+
ensure
|
174
|
+
context!
|
175
|
+
end
|
176
|
+
else
|
177
|
+
message[:data] += data
|
178
|
+
return ""
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Celluloid::SMTP::Constants
|
2
|
+
DEBUG = false
|
3
|
+
DEBUG_TIMING = true
|
4
|
+
DEBUG_AUTOMATA = false
|
5
|
+
DEBUG_EVENTS = false
|
6
|
+
|
7
|
+
HANDLERS = 0
|
8
|
+
LOGGER = Celluloid::Internals::Logger
|
9
|
+
DEFAULT_HOST = '127.0.0.1'
|
10
|
+
DEFAULT_PORT = 2525
|
11
|
+
DEFAULT_BACKLOG = 100
|
12
|
+
DEFAULT_BEHAVIOR = :blackhole
|
13
|
+
DEFAULT_HOSTNAME = "localhost"
|
14
|
+
TIMEOUT = 9
|
15
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class Celluloid::SMTP::Logging
|
2
|
+
include Celluloid
|
3
|
+
|
4
|
+
[:debug, :info, :warn, :error].each { |method|
|
5
|
+
define_method(method) { |*args| async.log(method,*args) }
|
6
|
+
}
|
7
|
+
|
8
|
+
alias :console :info
|
9
|
+
|
10
|
+
def log(method, *args)
|
11
|
+
Celluloid::SMTP::Constants::LOGGER.send(method,*args)
|
12
|
+
end
|
13
|
+
|
14
|
+
def exception(ex, note)
|
15
|
+
error("#{note}: #{ex} (#{ex.class})")
|
16
|
+
ex.backtrace.each { |line| error("* #{line}") }
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Celluloid
|
2
|
+
module SMTP
|
3
|
+
class Server
|
4
|
+
include Celluloid::IO
|
5
|
+
class << self
|
6
|
+
@logger = nil
|
7
|
+
attr_accessor :logger
|
8
|
+
def launch(options)
|
9
|
+
unless @logger
|
10
|
+
Celluloid::SMTP::Logging.supervise as: :logger
|
11
|
+
@logger = Celluloid[:logger]
|
12
|
+
end
|
13
|
+
if SMTP::Constants::HANDLERS > 0
|
14
|
+
Celluloid::SMTP::Server::Handler.supervise as: :handler, size: SMTP::Constants::HANDLERS
|
15
|
+
end
|
16
|
+
supervise(as: :smtpd, args:[options])
|
17
|
+
Celluloid[:smtpd]
|
18
|
+
end
|
19
|
+
def run!(options={})
|
20
|
+
launch(options)
|
21
|
+
end
|
22
|
+
def run(options={})
|
23
|
+
launch(options)
|
24
|
+
sleep
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
finalizer :ceased
|
29
|
+
|
30
|
+
def ceased
|
31
|
+
@server.close rescue nil
|
32
|
+
warn "SMTP Server offline."
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize(options={})
|
36
|
+
@options = options
|
37
|
+
@host = options.fetch(:host, DEFAULT_HOST)
|
38
|
+
@port = options.fetch(:port, DEFAULT_PORT)
|
39
|
+
@behavior = options.fetch(:behavior, DEFAULT_BEHAVIOR)
|
40
|
+
@hostname = options.fetch(:hostname, DEFAULT_HOSTNAME)
|
41
|
+
@backlog = options.fetch(:backlog, DEFAULT_BACKLOG)
|
42
|
+
|
43
|
+
@server = Celluloid::IO::TCPServer.new(@host, @port)
|
44
|
+
@server.listen(options.fetch(:backlog, @backlog))
|
45
|
+
|
46
|
+
console("Celluloid::IO SMTP Server #{SMTP::VERSION} @ #{@host}:#{@port}")
|
47
|
+
|
48
|
+
@options[:rescue] ||= []
|
49
|
+
@options[:rescue] += [
|
50
|
+
Errno::ECONNRESET,
|
51
|
+
Errno::EPIPE,
|
52
|
+
Errno::EINPROGRESS,
|
53
|
+
Errno::ETIMEDOUT,
|
54
|
+
Errno::EHOSTUNREACH
|
55
|
+
]
|
56
|
+
async.run
|
57
|
+
end
|
58
|
+
|
59
|
+
def shutdown
|
60
|
+
@online = false
|
61
|
+
sleep 0.126
|
62
|
+
@server.close if @server rescue nil
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def run
|
68
|
+
console "Starting to handle SMTP connections, with #{HANDLERS} handlers."
|
69
|
+
@online = true
|
70
|
+
loop {
|
71
|
+
break unless @online
|
72
|
+
begin
|
73
|
+
socket = @server.accept
|
74
|
+
rescue *@options[:rescue] => ex
|
75
|
+
warn "Error accepting socket: #{ex.class}: #{ex.to_s}"
|
76
|
+
next
|
77
|
+
rescue IOError, EOFError
|
78
|
+
warn "I/O Error on socket: #{ex.class}: #{ex.to_s}"
|
79
|
+
end
|
80
|
+
async.connection(socket)
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
def connection(socket)
|
85
|
+
if HANDLERS > 0
|
86
|
+
Celluloid[:handler].socket(socket, @options)
|
87
|
+
else
|
88
|
+
self.class.protector(socket) { |io| Connection.new(io, @options) }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class Celluloid::SMTP::Server
|
2
|
+
class << self
|
3
|
+
include Celluloid::SMTP::Extensions
|
4
|
+
def protector(io)
|
5
|
+
Thread.new {
|
6
|
+
Timeout.timeout(TIMEOUT) {
|
7
|
+
yield(io)
|
8
|
+
}
|
9
|
+
}.value
|
10
|
+
rescue EOFError, IOError
|
11
|
+
warn "Premature disconnect."
|
12
|
+
rescue Timeout::Error
|
13
|
+
warn "Timeout handling connection."
|
14
|
+
rescue Exception => ex
|
15
|
+
exception(ex, "Unknown connection error")
|
16
|
+
ensure
|
17
|
+
io.close
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
metadata
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: celluloid-smtp
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.9.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- digitalextremist //
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-06-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - '>='
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '0'
|
19
|
+
name: mail
|
20
|
+
prerelease: false
|
21
|
+
type: :development
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description: A small, fast, evented, actor-based, highly customizable Ruby SMTP server.
|
28
|
+
email: code@extremist.digital
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files: []
|
32
|
+
files:
|
33
|
+
- .gitignore
|
34
|
+
- .gitmodules
|
35
|
+
- CHANGES.md
|
36
|
+
- Gemfile
|
37
|
+
- LICENSE.txt
|
38
|
+
- README.md
|
39
|
+
- Rakefile
|
40
|
+
- celluloid-smtp.gemspec
|
41
|
+
- events.rb
|
42
|
+
- examples/basic_server.rb
|
43
|
+
- examples/test_load.rb
|
44
|
+
- examples/test_messages.rb
|
45
|
+
- lib/celluloid/smtp.rb
|
46
|
+
- lib/celluloid/smtp/connection.rb
|
47
|
+
- lib/celluloid/smtp/connection/automata.rb
|
48
|
+
- lib/celluloid/smtp/connection/errors.rb
|
49
|
+
- lib/celluloid/smtp/connection/events.rb
|
50
|
+
- lib/celluloid/smtp/connection/parser.rb
|
51
|
+
- lib/celluloid/smtp/constants.rb
|
52
|
+
- lib/celluloid/smtp/extensions.rb
|
53
|
+
- lib/celluloid/smtp/logging.rb
|
54
|
+
- lib/celluloid/smtp/server.rb
|
55
|
+
- lib/celluloid/smtp/server/handler.rb
|
56
|
+
- lib/celluloid/smtp/server/protector.rb
|
57
|
+
- lib/celluloid/smtp/server/transporter.rb
|
58
|
+
- lib/celluloid/smtp/version.rb
|
59
|
+
homepage: https://github.com/abstractive/celluloid-smtp
|
60
|
+
licenses:
|
61
|
+
- MIT
|
62
|
+
metadata: {}
|
63
|
+
post_install_message:
|
64
|
+
rdoc_options: []
|
65
|
+
require_paths:
|
66
|
+
- lib
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - '>='
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '0'
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
requirements: []
|
78
|
+
rubyforge_project:
|
79
|
+
rubygems_version: 2.4.6
|
80
|
+
signing_key:
|
81
|
+
specification_version: 4
|
82
|
+
summary: Celluloid based SMTP server.
|
83
|
+
test_files: []
|