boomerang-mocksmtpd 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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