celluloid-smtp 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|