celluloid-smtp 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,4 @@
1
+ Gemfile.lock
2
+ .DS_Store
3
+ *.log
4
+ pkg/*
@@ -0,0 +1,3 @@
1
+ [submodule "culture"]
2
+ path = culture
3
+ url = http://github.com/celluloid/culture.git
@@ -0,0 +1,2 @@
1
+ 0.0.0.9
2
+ - Decided to remove "block form" compatibility for now, in favor of overriding various events.
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'
@@ -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.
@@ -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).
@@ -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
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,7 @@
1
+ module Celluloid::SMTP::Extensions
2
+ def self.included(object)
3
+ object.send(:include, Celluloid::SMTP::Constants)
4
+ object.extend Forwardable
5
+ object.def_delegators :"Celluloid::SMTP::Server.logger", :debug, :console, :warn, :error, :exception
6
+ end
7
+ 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,11 @@
1
+ class Celluloid::SMTP::Server::Handler
2
+ include Celluloid
3
+ def socket(socket, options)
4
+ async.serve(socket,options)
5
+ end
6
+ def serve(socket, options)
7
+ SMTP::Server.protector(socket) { |io|
8
+ SMTP::Connection.new(io,options)
9
+ }
10
+ end
11
+ 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
@@ -0,0 +1,9 @@
1
+ class Celluloid::SMTP::Server::Transporter
2
+ include Celluloid::IO
3
+
4
+ def deliver(message)
5
+ end
6
+
7
+ def relay(message)
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module Celluloid::SMTP
2
+ VERSION = "0.9.0"
3
+ 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: []