boomerang-mocksmtpd 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/ChangeLog +17 -0
- data/README +59 -0
- data/Rakefile +144 -0
- data/bin/mocksmtpd +6 -0
- data/lib/mocksmtpd.rb +312 -0
- data/lib/smtpserver.rb +257 -0
- data/templates/html/index.erb +46 -0
- data/templates/html/index_entry.erb +8 -0
- data/templates/html/mail.erb +16 -0
- data/templates/mocksmtpd.conf.erb +10 -0
- data/test/mocksmtpd_test.rb +5 -0
- data/test/test_helper.rb +3 -0
- metadata +78 -0
data/ChangeLog
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
== 0.0.3 / 2008-11-08
|
2
|
+
|
3
|
+
* rescue Process.eid NotImplementedError.
|
4
|
+
* warn when Process.eid can't be changed.
|
5
|
+
* read log level from config file.
|
6
|
+
* change param name from Loglevel to LogLevel
|
7
|
+
* add debug log.
|
8
|
+
|
9
|
+
== 0.0.2 / 2008-11-03
|
10
|
+
|
11
|
+
* release gem version.
|
12
|
+
|
13
|
+
== 0.0.1 / 2008-11-03
|
14
|
+
|
15
|
+
* moved into github
|
16
|
+
* initial release
|
17
|
+
|
data/README
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
|
2
|
+
= mocksmtpd
|
3
|
+
|
4
|
+
|
5
|
+
== Description
|
6
|
+
|
7
|
+
Mocksmtpd is a SMTP server for developping and testing web application. This SMTP server does not send mail anywhere. Otherwise, save all mails as HTML format.
|
8
|
+
|
9
|
+
You can test mail using browser testing tools like Selenium.
|
10
|
+
|
11
|
+
=== Screenshot
|
12
|
+
|
13
|
+
http://koseki2.tumblr.com/post/57148631
|
14
|
+
http://koseki2.tumblr.com/post/57148564
|
15
|
+
|
16
|
+
== Installation
|
17
|
+
|
18
|
+
=== Archive Installation
|
19
|
+
|
20
|
+
rake install
|
21
|
+
|
22
|
+
=== Gem Installation
|
23
|
+
|
24
|
+
gem sources -a http://gems.github.com
|
25
|
+
gem install koseki-mocksmtpd
|
26
|
+
|
27
|
+
== Quick Start
|
28
|
+
|
29
|
+
$ mocksmtpd init
|
30
|
+
$ cd ./mocksmtpd
|
31
|
+
$ sudo mocksmtpd
|
32
|
+
|
33
|
+
== Usaage
|
34
|
+
|
35
|
+
mocksmtpd init [dirname] ... create log,inbox dir and config file.
|
36
|
+
mocksmtpd ... start as console mode.
|
37
|
+
mocksmtpd start ... start as daemon.
|
38
|
+
mocksmtpd stop ... stop running daemon.
|
39
|
+
|
40
|
+
=== Options
|
41
|
+
|
42
|
+
The default config file is ./mocksmtpd.conf, but you can specify any other file using -f option.
|
43
|
+
|
44
|
+
-f / --config=FILE ... Specify config file.
|
45
|
+
--help ... Show help.
|
46
|
+
--silent ... Disable console output.
|
47
|
+
--version ... Show version.
|
48
|
+
|
49
|
+
== Features/Problems
|
50
|
+
|
51
|
+
|
52
|
+
== Synopsis
|
53
|
+
|
54
|
+
|
55
|
+
== Copyright
|
56
|
+
|
57
|
+
Author:: KOSEKI Kengo <koseki@gmail.com>
|
58
|
+
Copyright:: Copyright (c) 2008
|
59
|
+
License:: Ruby Licence
|
data/Rakefile
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/clean'
|
4
|
+
require 'rake/testtask'
|
5
|
+
require 'rake/packagetask'
|
6
|
+
require 'rake/gempackagetask'
|
7
|
+
require 'rake/rdoctask'
|
8
|
+
require 'rake/contrib/rubyforgepublisher'
|
9
|
+
require 'rake/contrib/sshpublisher'
|
10
|
+
require 'fileutils'
|
11
|
+
require 'lib/mocksmtpd'
|
12
|
+
include FileUtils
|
13
|
+
|
14
|
+
NAME = "mocksmtpd"
|
15
|
+
AUTHOR = "KOSEKI Kengo"
|
16
|
+
EMAIL = "koseki@gmail.com"
|
17
|
+
DESCRIPTION = "Mock SMTP server for development/testing."
|
18
|
+
RUBYFORGE_PROJECT = "mocksmtpd"
|
19
|
+
HOMEPATH = "http://github.com/koseki/mocksmtpd/"
|
20
|
+
BIN_FILES = %w(mocksmtpd)
|
21
|
+
|
22
|
+
VERS = Mocksmtpd::VERSION
|
23
|
+
REV = File.read(".svn/entries")[/committed-rev="(d+)"/, 1] rescue nil
|
24
|
+
CLEAN.include ['**/.*.sw?', '*.gem', '.config']
|
25
|
+
RDOC_OPTS = [
|
26
|
+
'--title', "#{NAME} documentation",
|
27
|
+
"--charset", "utf-8",
|
28
|
+
"--opname", "index.html",
|
29
|
+
"--line-numbers",
|
30
|
+
"--main", "README",
|
31
|
+
"--inline-source",
|
32
|
+
]
|
33
|
+
|
34
|
+
task :default => [:test]
|
35
|
+
task :package => [:clean]
|
36
|
+
|
37
|
+
Rake::TestTask.new("test") do |t|
|
38
|
+
t.libs << "test"
|
39
|
+
t.pattern = "test/**/*_test.rb"
|
40
|
+
t.verbose = true
|
41
|
+
end
|
42
|
+
|
43
|
+
spec = Gem::Specification.new do |s|
|
44
|
+
s.name = NAME
|
45
|
+
s.version = VERS
|
46
|
+
s.platform = Gem::Platform::RUBY
|
47
|
+
s.has_rdoc = true
|
48
|
+
s.extra_rdoc_files = ["README", "ChangeLog"]
|
49
|
+
s.rdoc_options += RDOC_OPTS + ['--exclude', '^(examples|extras)/']
|
50
|
+
s.summary = DESCRIPTION
|
51
|
+
s.description = DESCRIPTION
|
52
|
+
s.author = AUTHOR
|
53
|
+
s.email = EMAIL
|
54
|
+
s.homepage = HOMEPATH
|
55
|
+
s.executables = BIN_FILES
|
56
|
+
s.rubyforge_project = RUBYFORGE_PROJECT
|
57
|
+
s.bindir = "bin"
|
58
|
+
s.require_path = "lib"
|
59
|
+
#s.autorequire = ""
|
60
|
+
s.test_files = Dir["test/*_test.rb"]
|
61
|
+
|
62
|
+
#s.add_dependency('activesupport', '>=1.3.1')
|
63
|
+
#s.required_ruby_version = '>= 1.8.2'
|
64
|
+
|
65
|
+
s.files = %w(README ChangeLog Rakefile) +
|
66
|
+
Dir.glob("{bin,doc,test,lib,templates,generator,extras,website,script}/**/*") +
|
67
|
+
Dir.glob("ext/**/*.{h,c,rb}") +
|
68
|
+
Dir.glob("examples/**/*.rb") +
|
69
|
+
Dir.glob("tools/*.rb") +
|
70
|
+
Dir.glob("rails/*.rb")
|
71
|
+
|
72
|
+
s.extensions = FileList["ext/**/extconf.rb"].to_a
|
73
|
+
end
|
74
|
+
|
75
|
+
Rake::GemPackageTask.new(spec) do |p|
|
76
|
+
p.need_tar = true
|
77
|
+
p.gem_spec = spec
|
78
|
+
end
|
79
|
+
|
80
|
+
task :install do
|
81
|
+
name = "#{NAME}-#{VERS}.gem"
|
82
|
+
sh %{rake package}
|
83
|
+
sh %{sudo gem install pkg/#{name}}
|
84
|
+
end
|
85
|
+
|
86
|
+
task :uninstall => [:clean] do
|
87
|
+
sh %{sudo gem uninstall #{NAME}}
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
Rake::RDocTask.new do |rdoc|
|
92
|
+
rdoc.rdoc_dir = 'html'
|
93
|
+
rdoc.options += RDOC_OPTS
|
94
|
+
rdoc.template = "resh"
|
95
|
+
#rdoc.template = "#{ENV['template']}.rb" if ENV['template']
|
96
|
+
if ENV['DOC_FILES']
|
97
|
+
rdoc.rdoc_files.include(ENV['DOC_FILES'].split(/,\s*/))
|
98
|
+
else
|
99
|
+
rdoc.rdoc_files.include('README', 'ChangeLog')
|
100
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
101
|
+
rdoc.rdoc_files.include('ext/**/*.c')
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
desc "Publish to RubyForge"
|
106
|
+
task :rubyforge => [:rdoc, :package] do
|
107
|
+
require 'rubyforge'
|
108
|
+
Rake::RubyForgePublisher.new(RUBYFORGE_PROJECT, '').upload
|
109
|
+
end
|
110
|
+
|
111
|
+
desc 'Package and upload the release to rubyforge.'
|
112
|
+
task :release => [:clean, :package] do |t|
|
113
|
+
v = ENV["VERSION"] or abort "Must supply VERSION=x.y.z"
|
114
|
+
abort "Versions don't match #{v} vs #{VERS}" unless v == VERS
|
115
|
+
pkg = "pkg/#{NAME}-#{VERS}"
|
116
|
+
|
117
|
+
require 'rubyforge'
|
118
|
+
rf = RubyForge.new.configure
|
119
|
+
puts "Logging in"
|
120
|
+
rf.login
|
121
|
+
|
122
|
+
c = rf.userconfig
|
123
|
+
# c["release_notes"] = description if description
|
124
|
+
# c["release_changes"] = changes if changes
|
125
|
+
c["preformatted"] = true
|
126
|
+
|
127
|
+
files = [
|
128
|
+
"#{pkg}.tgz",
|
129
|
+
"#{pkg}.gem"
|
130
|
+
].compact
|
131
|
+
|
132
|
+
puts "Releasing #{NAME} v. #{VERS}"
|
133
|
+
rf.add_release RUBYFORGE_PROJECT, NAME, VERS, *files
|
134
|
+
end
|
135
|
+
|
136
|
+
desc 'Show information about the gem.'
|
137
|
+
task :debug_gem do
|
138
|
+
puts spec.to_ruby
|
139
|
+
end
|
140
|
+
|
141
|
+
desc 'Update gem spec'
|
142
|
+
task :gemspec do
|
143
|
+
open("#{NAME}.gemspec", 'w').write spec.to_ruby
|
144
|
+
end
|
data/bin/mocksmtpd
ADDED
data/lib/mocksmtpd.rb
ADDED
@@ -0,0 +1,312 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__) # for test/development
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'pathname'
|
5
|
+
require 'yaml'
|
6
|
+
require 'erb'
|
7
|
+
require 'nkf'
|
8
|
+
require 'smtpserver'
|
9
|
+
|
10
|
+
class Mocksmtpd
|
11
|
+
VERSION = '0.0.3'
|
12
|
+
TEMPLATE_DIR = Pathname.new(File.dirname(__FILE__)) + "../templates"
|
13
|
+
|
14
|
+
include ERB::Util
|
15
|
+
|
16
|
+
def initialize(argv)
|
17
|
+
@opt = OptionParser.new
|
18
|
+
@opt.banner = "Usage: #$0 [options] [start|stop|init PATH]"
|
19
|
+
@opt.on("-f FILE", "--config=FILE", "Specify mocksmtpd.conf") do |v|
|
20
|
+
@conf_file = v
|
21
|
+
end
|
22
|
+
|
23
|
+
@opt.on("--version", "Show version string `#{VERSION}'") do
|
24
|
+
puts VERSION
|
25
|
+
exit
|
26
|
+
end
|
27
|
+
|
28
|
+
@opt.on("--silent", "Suppress all output") do
|
29
|
+
@silent = true
|
30
|
+
end
|
31
|
+
|
32
|
+
@opt.parse!(argv)
|
33
|
+
|
34
|
+
if argv.empty?
|
35
|
+
@command = "console"
|
36
|
+
else
|
37
|
+
@command = argv.shift
|
38
|
+
commands = %w(start stop init)
|
39
|
+
unless commands.include? @command
|
40
|
+
opterror "No such command: #{@command}"
|
41
|
+
exit 1
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
if @command == "init"
|
46
|
+
@init_dir = argv.shift || "mocksmtpd"
|
47
|
+
if test(?e, @init_dir)
|
48
|
+
opterror("Init path already exists: #{@init_dir}")
|
49
|
+
exit 1
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def opterror(msg)
|
55
|
+
puts("Error: #{msg}")
|
56
|
+
puts(@opt.help)
|
57
|
+
end
|
58
|
+
|
59
|
+
def load_conf
|
60
|
+
@conf_file = Pathname.new(@conf_file || "./mocksmtpd.conf")
|
61
|
+
unless @conf_file.exist? && @conf_file.readable?
|
62
|
+
opterror "Can't load config file: #{@conf_file}"
|
63
|
+
exit 1
|
64
|
+
end
|
65
|
+
@conf_file = @conf_file.realpath
|
66
|
+
|
67
|
+
@conf = {}
|
68
|
+
YAML.load_file(@conf_file).each do |k,v|
|
69
|
+
@conf[k.intern] = v
|
70
|
+
end
|
71
|
+
|
72
|
+
@inbox = resolve_conf_path(@conf[:InboxDir])
|
73
|
+
@logfile = resolve_conf_path(@conf[:LogFile])
|
74
|
+
@pidfile = resolve_conf_path(@conf[:PidFile])
|
75
|
+
|
76
|
+
@templates = load_templates
|
77
|
+
end
|
78
|
+
|
79
|
+
def resolve_conf_path(path)
|
80
|
+
result = nil
|
81
|
+
if path[0] == ?/
|
82
|
+
result = Pathname.new(path)
|
83
|
+
else
|
84
|
+
result = @conf_file.parent + path
|
85
|
+
end
|
86
|
+
return result.cleanpath
|
87
|
+
end
|
88
|
+
|
89
|
+
def run
|
90
|
+
send(@command)
|
91
|
+
end
|
92
|
+
|
93
|
+
def load_templates
|
94
|
+
result = {}
|
95
|
+
result[:mail] = template("html/mail")
|
96
|
+
result[:index] = template("html/index")
|
97
|
+
result[:index_entry] = template("html/index_entry")
|
98
|
+
return result
|
99
|
+
end
|
100
|
+
|
101
|
+
def template(name)
|
102
|
+
path = TEMPLATE_DIR + "#{name}.erb"
|
103
|
+
src = path.read
|
104
|
+
return ERB.new(src, nil, "%-")
|
105
|
+
end
|
106
|
+
|
107
|
+
def init
|
108
|
+
Dir.mkdir(@init_dir)
|
109
|
+
puts "Created: #{@init_dir}/" unless @silent
|
110
|
+
path = Pathname.new(@init_dir)
|
111
|
+
Dir.mkdir(path + "inbox")
|
112
|
+
puts "Created: #{path + 'inbox'}/" unless @silent
|
113
|
+
Dir.mkdir(path + "log")
|
114
|
+
puts "Created: #{path + 'log'}/" unless @silent
|
115
|
+
|
116
|
+
open(path + "mocksmtpd.conf", "w") do |io|
|
117
|
+
io << template("mocksmtpd.conf").result(binding)
|
118
|
+
end
|
119
|
+
puts "Created: #{path + 'mocksmtpd.conf'}" unless @silent
|
120
|
+
end
|
121
|
+
|
122
|
+
def stop
|
123
|
+
load_conf
|
124
|
+
unless @pidfile.exist?
|
125
|
+
puts "ERROR: pid file does not exist: #{@pidfile}"
|
126
|
+
exit 1
|
127
|
+
end
|
128
|
+
unless @pidfile.readable?
|
129
|
+
puts "ERROR: Can't read pid file: #{@pidfile}"
|
130
|
+
exit 1
|
131
|
+
end
|
132
|
+
|
133
|
+
pid = File.read(@pidfile)
|
134
|
+
print "Stopping #{pid}..." unless @silent
|
135
|
+
#system "kill -TERM #{pid}"
|
136
|
+
system "taskkill /F /PID #{pid} 1>NUL"
|
137
|
+
puts "done" unless @silent
|
138
|
+
end
|
139
|
+
|
140
|
+
def create_logger(file = nil)
|
141
|
+
file = file.to_s.strip
|
142
|
+
file = nil if file.empty?
|
143
|
+
lvstr = @conf[:LogLevel].to_s.strip
|
144
|
+
lvstr = "ERROR" if @silent
|
145
|
+
lvstr = "INFO" unless %w{FATAL ERROR WARN INFO DEBUG}.include?(lvstr)
|
146
|
+
level = WEBrick::BasicLog.const_get(lvstr)
|
147
|
+
logger = WEBrick::Log.new(file, level)
|
148
|
+
logger.debug("Logger initialized")
|
149
|
+
return logger
|
150
|
+
end
|
151
|
+
|
152
|
+
def start
|
153
|
+
load_conf
|
154
|
+
@logger = create_logger(@logfile)
|
155
|
+
@daemon = true
|
156
|
+
smtpd
|
157
|
+
end
|
158
|
+
|
159
|
+
def console
|
160
|
+
load_conf
|
161
|
+
@logger = create_logger
|
162
|
+
@daemon = false
|
163
|
+
smtpd
|
164
|
+
end
|
165
|
+
|
166
|
+
def create_pid_file
|
167
|
+
if @pidfile.exist?
|
168
|
+
pid = @pidfile.read
|
169
|
+
@logger.warn("pid file already exists: pid=#{pid}")
|
170
|
+
exit 1
|
171
|
+
end
|
172
|
+
pid = Process.pid
|
173
|
+
open(@pidfile, "w") do |io|
|
174
|
+
io << pid
|
175
|
+
end
|
176
|
+
@logger.debug("pid file saved: pid=#{pid} file=#{@pidfile}")
|
177
|
+
end
|
178
|
+
|
179
|
+
def delete_pid_file
|
180
|
+
File.delete(@pidfile)
|
181
|
+
@logger.debug("pid file deleted: file=#{@pidfile}")
|
182
|
+
end
|
183
|
+
|
184
|
+
def init_permission
|
185
|
+
File.umask(@conf[:Umask]) unless @conf[:Umask].nil?
|
186
|
+
stat = File::Stat.new(@conf_file)
|
187
|
+
uid = stat.uid
|
188
|
+
gid = stat.gid
|
189
|
+
begin
|
190
|
+
Process.egid = gid
|
191
|
+
Process.euid = uid
|
192
|
+
rescue NotImplementedError => e
|
193
|
+
@logger.debug("Process.euid= not implemented.")
|
194
|
+
rescue Errno::EPERM => e
|
195
|
+
@logger.warn("could not change euid/egid. #{e}")
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def smtpd
|
200
|
+
start_cb = Proc.new do
|
201
|
+
@logger.info("Inbox: #{@inbox}")
|
202
|
+
@logger.debug("LogFile: #{@logfile}")
|
203
|
+
@logger.debug("PidFile: #{@pidfile}")
|
204
|
+
|
205
|
+
begin
|
206
|
+
init_permission
|
207
|
+
create_pid_file #if @daemon
|
208
|
+
rescue => e
|
209
|
+
@logger.error("Start: #{e}")
|
210
|
+
raise e
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
stop_cb = Proc.new do
|
215
|
+
begin
|
216
|
+
delete_pid_file #if @daemon
|
217
|
+
rescue => e
|
218
|
+
@logger.error("Stop: #{e}")
|
219
|
+
raise e
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
data_cb = Proc.new do |src, sender, recipients|
|
224
|
+
recieve_mail(src, sender, recipients)
|
225
|
+
end
|
226
|
+
|
227
|
+
@conf[:ServerType] = @daemon ? WEBrick::Daemon : nil
|
228
|
+
@conf[:Logger] = @logger
|
229
|
+
@conf[:StartCallback] = start_cb
|
230
|
+
@conf[:StopCallback] = stop_cb
|
231
|
+
@conf[:DataHook] = data_cb
|
232
|
+
|
233
|
+
server = SMTPServer.new(@conf)
|
234
|
+
|
235
|
+
[:INT, :TERM].each do |signal|
|
236
|
+
Signal.trap(signal) { server.shutdown }
|
237
|
+
end
|
238
|
+
|
239
|
+
server.start
|
240
|
+
end
|
241
|
+
|
242
|
+
def recieve_mail(src, sender, recipients)
|
243
|
+
@logger.info "mail recieved from #{sender}"
|
244
|
+
|
245
|
+
mail = parse_mail(src, sender, recipients)
|
246
|
+
|
247
|
+
save_mail(mail)
|
248
|
+
save_index(mail)
|
249
|
+
end
|
250
|
+
|
251
|
+
def parse_mail(src, sender, recipients)
|
252
|
+
src = NKF.nkf("-wm", src)
|
253
|
+
subject = src.match(/^Subject:\s*(.+)/i).to_a[1].to_s.strip
|
254
|
+
date = src.match(/^Date:\s*(.+)/i).to_a[1].to_s.strip
|
255
|
+
|
256
|
+
src = ERB::Util.h(src)
|
257
|
+
src = src.gsub(%r{https?://[-_.!~*\'()a-zA-Z0-9;\/?:\@&=+\$,%#]+},'<a href="\0">\0</a>')
|
258
|
+
src = src.gsub(/(?:\r\n|\r|\n)/, "<br />\n")
|
259
|
+
|
260
|
+
if date.empty?
|
261
|
+
date = Time.now
|
262
|
+
else
|
263
|
+
date = Time.parse(date)
|
264
|
+
end
|
265
|
+
|
266
|
+
mail = {
|
267
|
+
:source => src,
|
268
|
+
:sender => sender,
|
269
|
+
:recipients => recipients,
|
270
|
+
:subject => subject,
|
271
|
+
:date => date,
|
272
|
+
}
|
273
|
+
|
274
|
+
format = "%Y%m%d%H%M%S"
|
275
|
+
fname = date.strftime(format) + ".html"
|
276
|
+
while @inbox.join(fname).exist?
|
277
|
+
date += 1
|
278
|
+
fname = date.strftime(format) + ".html"
|
279
|
+
end
|
280
|
+
|
281
|
+
mail[:file] = fname
|
282
|
+
mail[:path] = @inbox.join(fname)
|
283
|
+
|
284
|
+
return mail
|
285
|
+
end
|
286
|
+
|
287
|
+
def save_mail(mail)
|
288
|
+
open(mail[:path], "w") do |io|
|
289
|
+
io << @templates[:mail].result(binding)
|
290
|
+
end
|
291
|
+
@logger.debug("mail saved: #{mail[:path]}")
|
292
|
+
end
|
293
|
+
|
294
|
+
def save_index(mail)
|
295
|
+
path = @inbox + "index.html"
|
296
|
+
unless File.exist?(path)
|
297
|
+
open(path, "w") do |io|
|
298
|
+
io << @templates[:index].result(binding)
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
htmlsrc = File.read(path)
|
303
|
+
add = @templates[:index_entry].result(binding)
|
304
|
+
|
305
|
+
htmlsrc.sub!(/<!-- ADD -->/, add)
|
306
|
+
open(path, "w") do |io|
|
307
|
+
io << htmlsrc
|
308
|
+
end
|
309
|
+
@logger.debug("index saved: #{path}")
|
310
|
+
end
|
311
|
+
|
312
|
+
end
|
data/lib/smtpserver.rb
ADDED
@@ -0,0 +1,257 @@
|
|
1
|
+
# http://tmtm.org/ja/ruby/smtpd/
|
2
|
+
# http://rubyist.g.hatena.ne.jp/muscovyduck/20070707/p1
|
3
|
+
|
4
|
+
require 'webrick'
|
5
|
+
require 'tempfile'
|
6
|
+
|
7
|
+
module GetsSafe
|
8
|
+
def gets_safe(rs = nil, timeout = @timeout, maxlength = @maxlength)
|
9
|
+
rs = $/ unless rs
|
10
|
+
f = self.kind_of?(IO) ? self : STDIN
|
11
|
+
@gets_safe_buf = '' unless @gets_safe_buf
|
12
|
+
until @gets_safe_buf.include? rs do
|
13
|
+
if maxlength and @gets_safe_buf.length > maxlength then
|
14
|
+
raise Errno::E2BIG, 'too long'
|
15
|
+
end
|
16
|
+
if IO.select([f], nil, nil, timeout) == nil then
|
17
|
+
raise Errno::ETIMEDOUT, 'timeout exceeded'
|
18
|
+
end
|
19
|
+
begin
|
20
|
+
@gets_safe_buf << f.sysread(4096)
|
21
|
+
rescue EOFError, Errno::ECONNRESET
|
22
|
+
return @gets_safe_buf.empty? ? nil : @gets_safe_buf.slice!(0..-1)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
p = @gets_safe_buf.index rs
|
26
|
+
if maxlength and p > maxlength then
|
27
|
+
raise Errno::E2BIG, 'too long'
|
28
|
+
end
|
29
|
+
return @gets_safe_buf.slice!(0, p+rs.length)
|
30
|
+
end
|
31
|
+
attr_accessor :timeout, :maxlength
|
32
|
+
end
|
33
|
+
|
34
|
+
class SMTPD
|
35
|
+
class Error < StandardError; end
|
36
|
+
|
37
|
+
def initialize(sock, domain)
|
38
|
+
@sock = sock
|
39
|
+
@domain = domain
|
40
|
+
@error_interval = 5
|
41
|
+
class << @sock
|
42
|
+
include GetsSafe
|
43
|
+
end
|
44
|
+
@helo_hook = nil
|
45
|
+
@mail_hook = nil
|
46
|
+
@rcpt_hook = nil
|
47
|
+
@data_hook = nil
|
48
|
+
@data_each_line = nil
|
49
|
+
@rset_hook = nil
|
50
|
+
@noop_hook = nil
|
51
|
+
@quit_hook = nil
|
52
|
+
end
|
53
|
+
attr_writer :helo_hook, :mail_hook, :rcpt_hook, :data_hook,
|
54
|
+
:data_each_line, :rset_hook, :noop_hook, :quit_hook
|
55
|
+
|
56
|
+
def start
|
57
|
+
@helo_name = nil
|
58
|
+
@sender = nil
|
59
|
+
@recipients = []
|
60
|
+
catch(:close) do
|
61
|
+
puts_safe "220 #{@domain} service ready"
|
62
|
+
while comm = @sock.gets_safe do
|
63
|
+
catch :next_comm do
|
64
|
+
comm.sub!(/\r?\n/, '')
|
65
|
+
comm, arg = comm.split(/\s+/,2)
|
66
|
+
break if comm == nil
|
67
|
+
case comm.upcase
|
68
|
+
when 'EHLO' then comm_helo arg
|
69
|
+
when 'HELO' then comm_helo arg
|
70
|
+
when 'MAIL' then comm_mail arg
|
71
|
+
when 'RCPT' then comm_rcpt arg
|
72
|
+
when 'DATA' then comm_data arg
|
73
|
+
when 'RSET' then comm_rset arg
|
74
|
+
when 'NOOP' then comm_noop arg
|
75
|
+
when 'QUIT' then comm_quit arg
|
76
|
+
else
|
77
|
+
error '502 Error: command not implemented'
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def line_length_limit=(n)
|
85
|
+
@sock.maxlength = n
|
86
|
+
end
|
87
|
+
|
88
|
+
def input_timeout=(n)
|
89
|
+
@sock.timeout = n
|
90
|
+
end
|
91
|
+
|
92
|
+
attr_reader :line_length_limit, :input_timeout
|
93
|
+
attr_accessor :error_interval
|
94
|
+
attr_accessor :use_file, :max_size
|
95
|
+
|
96
|
+
private
|
97
|
+
def comm_helo(arg)
|
98
|
+
if arg == nil or arg.split.size != 1 then
|
99
|
+
error '501 Syntax: HELO hostname'
|
100
|
+
end
|
101
|
+
@helo_hook.call(arg) if @helo_hook
|
102
|
+
@helo_name = arg
|
103
|
+
reply "250 #{@domain}"
|
104
|
+
end
|
105
|
+
|
106
|
+
def comm_mail(arg)
|
107
|
+
if @sender != nil then
|
108
|
+
error '503 Error: nested MAIL command'
|
109
|
+
end
|
110
|
+
if arg !~ /^FROM:/i then
|
111
|
+
error '501 Syntax: MAIL FROM: <address>'
|
112
|
+
end
|
113
|
+
sender = parse_addr $'
|
114
|
+
if sender == nil then
|
115
|
+
error '501 Syntax: MAIL FROM: <address>'
|
116
|
+
end
|
117
|
+
@mail_hook.call(sender) if @mail_hook
|
118
|
+
@sender = sender
|
119
|
+
reply '250 Ok'
|
120
|
+
end
|
121
|
+
|
122
|
+
def comm_rcpt(arg)
|
123
|
+
if @sender == nil then
|
124
|
+
error '503 Error: need MAIL command'
|
125
|
+
end
|
126
|
+
if arg !~ /^TO:/i then
|
127
|
+
error '501 Syntax: RCPT TO: <address>'
|
128
|
+
end
|
129
|
+
rcpt = parse_addr $'
|
130
|
+
if rcpt == nil then
|
131
|
+
error '501 Syntax: RCPT TO: <address>'
|
132
|
+
end
|
133
|
+
@rcpt_hook.call(rcpt) if @rcpt_hook
|
134
|
+
@recipients << rcpt
|
135
|
+
reply '250 Ok'
|
136
|
+
end
|
137
|
+
|
138
|
+
def comm_data(arg)
|
139
|
+
if @recipients.size == 0 then
|
140
|
+
error '503 Error: need RCPT command'
|
141
|
+
end
|
142
|
+
if arg != nil then
|
143
|
+
error '501 Syntax: DATA'
|
144
|
+
end
|
145
|
+
reply '354 End data with <CR><LF>.<CR><LF>'
|
146
|
+
if @data_hook
|
147
|
+
tmpf = @use_file ? Tempfile.new('smtpd') : ''
|
148
|
+
end
|
149
|
+
size = 0
|
150
|
+
loop do
|
151
|
+
l = @sock.gets_safe
|
152
|
+
if l == nil then
|
153
|
+
raise SMTPD::Error, 'unexpected EOF'
|
154
|
+
end
|
155
|
+
if l.chomp == '.' then break end
|
156
|
+
if l[0] == ?. then
|
157
|
+
l[0,1] = ''
|
158
|
+
end
|
159
|
+
size += l.size
|
160
|
+
if @max_size and @max_size < size then
|
161
|
+
error '552 Error: message too large'
|
162
|
+
end
|
163
|
+
@data_each_line.call(l) if @data_each_line
|
164
|
+
tmpf << l if @data_hook
|
165
|
+
end
|
166
|
+
if @data_hook then
|
167
|
+
if @use_file then
|
168
|
+
tmpf.pos = 0
|
169
|
+
@data_hook.call(tmpf, @sender, @recipients)
|
170
|
+
tmpf.close(true)
|
171
|
+
else
|
172
|
+
@data_hook.call(tmpf, @sender, @recipients)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
reply '250 Ok'
|
176
|
+
@sender = nil
|
177
|
+
@recipients = []
|
178
|
+
end
|
179
|
+
|
180
|
+
def comm_rset(arg)
|
181
|
+
if arg != nil then
|
182
|
+
error '501 Syntax: RSET'
|
183
|
+
end
|
184
|
+
@rset_hook.call(@sender, @recipients) if @rset_hook
|
185
|
+
reply '250 Ok'
|
186
|
+
@sender = nil
|
187
|
+
@recipients = []
|
188
|
+
end
|
189
|
+
|
190
|
+
def comm_noop(arg)
|
191
|
+
if arg != nil then
|
192
|
+
error '501 Syntax: NOOP'
|
193
|
+
end
|
194
|
+
@noop_hook.call(@sender, @recipients) if @noop_hook
|
195
|
+
reply '250 Ok'
|
196
|
+
end
|
197
|
+
|
198
|
+
def comm_quit(arg)
|
199
|
+
if arg != nil then
|
200
|
+
error '501 Syntax: QUIT'
|
201
|
+
end
|
202
|
+
@quit_hook.call(@sender, @recipients) if @quit_hook
|
203
|
+
reply '221 Bye'
|
204
|
+
throw :close
|
205
|
+
end
|
206
|
+
|
207
|
+
def parse_addr(str)
|
208
|
+
str = str.strip
|
209
|
+
if str == '' then
|
210
|
+
return nil
|
211
|
+
end
|
212
|
+
if str =~ /^<(.*)>$/ then
|
213
|
+
return $1.gsub(/\s+/, '')
|
214
|
+
end
|
215
|
+
if str =~ /\s/ then
|
216
|
+
return nil
|
217
|
+
end
|
218
|
+
str
|
219
|
+
end
|
220
|
+
|
221
|
+
def reply(msg)
|
222
|
+
puts_safe msg
|
223
|
+
end
|
224
|
+
|
225
|
+
def error(msg)
|
226
|
+
sleep @error_interval if @error_interval
|
227
|
+
puts_safe msg
|
228
|
+
throw :next_comm
|
229
|
+
end
|
230
|
+
|
231
|
+
def puts_safe(str)
|
232
|
+
begin
|
233
|
+
@sock.puts str + "\r\n"
|
234
|
+
rescue
|
235
|
+
raise SMTPD::Error, "cannot send to client: '#{str.gsub(/\s+/," ")}': #{$!.to_s}"
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
SMTPDError = SMTPD::Error
|
241
|
+
|
242
|
+
class SMTPServer < WEBrick::GenericServer
|
243
|
+
def run(sock)
|
244
|
+
server = SMTPD.new(sock, @config[:ServerName])
|
245
|
+
server.input_timeout = @config[:RequestTimeout]
|
246
|
+
server.line_length_limit = @config[:LineLengthLimit]
|
247
|
+
server.helo_hook = @config[:HeloHook]
|
248
|
+
server.mail_hook = @config[:MailHook]
|
249
|
+
server.rcpt_hook = @config[:RcptHook]
|
250
|
+
server.data_hook = @config[:DataHook]
|
251
|
+
server.data_each_line = @config[:DataEachLine]
|
252
|
+
server.rset_hook = @config[:RsetHook]
|
253
|
+
server.noop_hook = @config[:NoopHook]
|
254
|
+
server.quit_hook = @config[:QuitHook]
|
255
|
+
server.start
|
256
|
+
end
|
257
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
3
|
+
<html xmlns="http://www.w3.org/1999/xhtml">
|
4
|
+
<head>
|
5
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
6
|
+
<link rel="index" href="./index.html" />
|
7
|
+
<title>Inbox</title>
|
8
|
+
<style type="text/css">
|
9
|
+
body {
|
10
|
+
background:#eee;
|
11
|
+
}
|
12
|
+
table {
|
13
|
+
border: 1px #999 solid;
|
14
|
+
border-collapse: collapse;
|
15
|
+
}
|
16
|
+
th, td {
|
17
|
+
border: 1px #999 solid;
|
18
|
+
padding: 6px 12px;
|
19
|
+
}
|
20
|
+
th {
|
21
|
+
background: #ccc;
|
22
|
+
}
|
23
|
+
td {
|
24
|
+
background: white;
|
25
|
+
}
|
26
|
+
</style>
|
27
|
+
</head>
|
28
|
+
<body>
|
29
|
+
<h1>Inbox</h1>
|
30
|
+
<table>
|
31
|
+
<thead>
|
32
|
+
<tr>
|
33
|
+
<th>Date</th>
|
34
|
+
<th>Subject</th>
|
35
|
+
<th>From</th>
|
36
|
+
<th>To</th>
|
37
|
+
</tr>
|
38
|
+
</thead>
|
39
|
+
|
40
|
+
<tbody>
|
41
|
+
<!-- ADD -->
|
42
|
+
|
43
|
+
</tbody>
|
44
|
+
</table>
|
45
|
+
</body>
|
46
|
+
</html>
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
3
|
+
<html xmlns="http://www.w3.org/1999/xhtml">
|
4
|
+
<head>
|
5
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
6
|
+
<link rel="index" href="./index.html" />
|
7
|
+
<title><%=h mail[:subject] %> (<%= mail[:date].to_s %>)</title>
|
8
|
+
</head>
|
9
|
+
<body style="background:#eee">
|
10
|
+
<h1 id="subject"><%=h mail[:subject] %></h1>
|
11
|
+
<div><p id="date" style="font-size:0.8em;"><%= mail[:date].to_s %></div>
|
12
|
+
<div id="source" style="border: solid 1px #666; background:white; padding:2em;">
|
13
|
+
<p><%= mail[:source] %></p>
|
14
|
+
</div>
|
15
|
+
</body>
|
16
|
+
</html>
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: boomerang-mocksmtpd
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- BB
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-11-08 00:00:00 +01:00
|
13
|
+
default_executable: mocksmtpd
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Mock SMTP server for development/testing.
|
17
|
+
email: b@gmail.com
|
18
|
+
executables:
|
19
|
+
- mocksmtpd
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README
|
24
|
+
- ChangeLog
|
25
|
+
files:
|
26
|
+
- README
|
27
|
+
- ChangeLog
|
28
|
+
- Rakefile
|
29
|
+
- bin/mocksmtpd
|
30
|
+
- test/mocksmtpd_test.rb
|
31
|
+
- test/test_helper.rb
|
32
|
+
- lib/mocksmtpd.rb
|
33
|
+
- lib/smtpserver.rb
|
34
|
+
- templates/html/index.erb
|
35
|
+
- templates/html/index_entry.erb
|
36
|
+
- templates/html/mail.erb
|
37
|
+
- templates/mocksmtpd.conf.erb
|
38
|
+
has_rdoc: true
|
39
|
+
homepage: http://github.com/boomerang/mocksmtpd/
|
40
|
+
licenses: []
|
41
|
+
|
42
|
+
post_install_message:
|
43
|
+
rdoc_options:
|
44
|
+
- --title
|
45
|
+
- mocksmtpd documentation
|
46
|
+
- --charset
|
47
|
+
- utf-8
|
48
|
+
- --opname
|
49
|
+
- index.html
|
50
|
+
- --line-numbers
|
51
|
+
- --main
|
52
|
+
- README
|
53
|
+
- --inline-source
|
54
|
+
- --exclude
|
55
|
+
- ^(examples|extras)/
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: "0"
|
63
|
+
version:
|
64
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: "0"
|
69
|
+
version:
|
70
|
+
requirements: []
|
71
|
+
|
72
|
+
rubyforge_project: mocksmtpd
|
73
|
+
rubygems_version: 1.3.5
|
74
|
+
signing_key:
|
75
|
+
specification_version: 2
|
76
|
+
summary: Mock SMTP server for development/testing.
|
77
|
+
test_files:
|
78
|
+
- test/mocksmtpd_test.rb
|