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 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
@@ -0,0 +1,6 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require "rubygems"
4
+ require "mocksmtpd"
5
+
6
+ Mocksmtpd.new(ARGV).run
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,8 @@
1
+ <!-- ADD -->
2
+
3
+ <tr>
4
+ <td><%= mail[:date].strftime("%Y-%m-%d %H:%M:%S") %></td>
5
+ <td><a href="<%=h mail[:file] %>"><%=h mail[:subject] %></a></td>
6
+ <td><%=h mail[:sender] %></td>
7
+ <td><%=h mail[:recipients].to_a.join(",") %></td>
8
+ </tr>
@@ -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>
@@ -0,0 +1,10 @@
1
+ ServerName: mocksmtpd
2
+ Port: 25
3
+ RequestTimeout: 120
4
+ LineLengthLimit: 1024
5
+ LogLevel: INFO
6
+
7
+ LogFile: ./log/mocksmtpd.log
8
+ PidFile: ./log/mocksmtpd.pid
9
+ InboxDir: ./inbox
10
+ Umask: 2
@@ -0,0 +1,5 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+
3
+ require "test/unit"
4
+ class MocksmtpdTest < Test::Unit::TestCase
5
+ end
@@ -0,0 +1,3 @@
1
+ require 'test/unit'
2
+ require File.dirname(__FILE__) + '/../lib/mocksmtpd'
3
+
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