malm 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -4,6 +4,11 @@ gem "json", "1.5.3"
4
4
  gem "clamp", "0.2.1"
5
5
  gem "sinatra", "1.2.6"
6
6
  gem "thin", "1.2.11"
7
+ gem "mail", "2.3.0"
8
+ gem "therubyracer", "0.9.2"
9
+ gem "execjs", "1.2.0"
10
+ gem "coffee-script", "2.2.0"
11
+ gem "daemons", "1.1.4"
7
12
 
8
13
  group :development do
9
14
  gem "rspec", "~> 2.3.0"
data/Gemfile.lock CHANGED
@@ -2,15 +2,30 @@ GEM
2
2
  remote: http://rubygems.org/
3
3
  specs:
4
4
  clamp (0.2.1)
5
+ coffee-script (2.2.0)
6
+ coffee-script-source
7
+ execjs
8
+ coffee-script-source (1.1.1)
5
9
  daemons (1.1.4)
6
10
  diff-lcs (1.1.2)
7
11
  eventmachine (0.12.10)
12
+ execjs (1.2.0)
13
+ multi_json (~> 1.0)
8
14
  git (1.2.5)
15
+ i18n (0.6.0)
9
16
  jeweler (1.6.4)
10
17
  bundler (~> 1.0)
11
18
  git (>= 1.2.5)
12
19
  rake
13
20
  json (1.5.3)
21
+ libv8 (3.3.10.2)
22
+ mail (2.3.0)
23
+ i18n (>= 0.4.0)
24
+ mime-types (~> 1.16)
25
+ treetop (~> 1.4.8)
26
+ mime-types (1.16)
27
+ multi_json (1.0.3)
28
+ polyglot (0.3.1)
14
29
  rack (1.3.1)
15
30
  rake (0.9.2)
16
31
  rcov (0.9.9)
@@ -25,11 +40,15 @@ GEM
25
40
  sinatra (1.2.6)
26
41
  rack (~> 1.1)
27
42
  tilt (< 2.0, >= 1.2.2)
43
+ therubyracer (0.9.2)
44
+ libv8 (~> 3.3.10)
28
45
  thin (1.2.11)
29
46
  daemons (>= 1.0.9)
30
47
  eventmachine (>= 0.12.6)
31
48
  rack (>= 1.0.0)
32
49
  tilt (1.3.2)
50
+ treetop (1.4.9)
51
+ polyglot (>= 0.3.1)
33
52
 
34
53
  PLATFORMS
35
54
  ruby
@@ -37,9 +56,14 @@ PLATFORMS
37
56
  DEPENDENCIES
38
57
  bundler (~> 1.0.0)
39
58
  clamp (= 0.2.1)
59
+ coffee-script (= 2.2.0)
60
+ daemons (= 1.1.4)
61
+ execjs (= 1.2.0)
40
62
  jeweler (~> 1.6.2)
41
63
  json (= 1.5.3)
64
+ mail (= 2.3.0)
42
65
  rcov
43
66
  rspec (~> 2.3.0)
44
67
  sinatra (= 1.2.6)
68
+ therubyracer (= 0.9.2)
45
69
  thin (= 1.2.11)
data/README.md ADDED
@@ -0,0 +1,37 @@
1
+ Malm
2
+ ====
3
+ Easy SMTP server for local development
4
+ --------------------------------------
5
+ Malm is a super simple SMTP trap that will catch everything sent to it, not forward it on, but instead display on a local web server.
6
+
7
+ This is useful for when you're doing local development that requires your app to send mail, but you don't want to go through all the hassle of setting up a full featured mail server (and then pointing your mail client at it).
8
+
9
+ Install
10
+ -------
11
+ gem install malm
12
+
13
+ Basic Usage
14
+ -----------
15
+ The following command starts Malm up. Running on it's defaults of listening for SMTP on port 2525, client web app running on port 4567, and starting as a process in the foreground
16
+
17
+ malm
18
+
19
+ If you want to start up on regular SMTP port 25, you'll probably need to run it as sudo
20
+
21
+ sudo malm -p 25
22
+
23
+ For more info try
24
+
25
+ sudo malm --help
26
+
27
+ Viewing Messages
28
+ ----------------
29
+ Once it's up and running, it works just like any other SMTP server - your app sends it mail, then happily goes about it's business. To view the mail messages Malm has picked up, point your web browser at [http://localhost:4567](http://localhost:4567)
30
+
31
+ Who the hell?
32
+ -------------
33
+ I blame [@madlep](https://twitter.com/#!/madlep)
34
+
35
+ Contributing
36
+ ------------
37
+ Fork + pull request. Nice and easy.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.4
1
+ 0.0.5
data/bin/malm CHANGED
@@ -1,52 +1,48 @@
1
1
  #!/usr/bin/env ruby
2
2
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
3
- require "malm_smtp_server"
4
- require "malm_web"
5
- require "clamp"
6
- require 'message_db'
3
+ require 'malm'
4
+ require 'clamp'
5
+ require 'daemons'
7
6
 
8
7
  class MalmCommand < Clamp::Command
8
+ option "--start", :flag, "Start malm as a daemon process. PID will be written to '<PID DIRECTORY>/malm-<SMTP PORT>-<WEB PORT>.pid'"
9
+ option "--stop", :flag, "Stop malm running as a daemon process. Process id '<PID DIRECTORY>/malm-<SMTP PORT>-<WEB PORT>.pid' will be stopped"
10
+ option "--restart", :flag, "Restart malm running as a daemon process"
11
+ option "--piddir", "PID DIRECTORY", "Where to put pids", :default => "/var/run"
9
12
  option ["-l", "--log"], "FILE", "file to log mail messages to (optional)"
10
-
11
- option ["-p", "--stmpport"], "STMP PORT", "SMTP port to listen on", :default => 2525 do |port|
13
+ option ["-p", "--smtpport"], "SMTP PORT", "SMTP port to listen on", :default => 2525 do |port|
12
14
  Integer(port)
13
15
  end
14
-
15
16
  option ["-w", "--webport"], "WEB PORT", "Port for client web app to view malmed messages", :default => 4567 do |port|
16
17
  Integer(port)
17
18
  end
18
19
 
19
- def execute
20
- db = create_db
21
- smtp = run_smtp!(db)
22
- web = run_web!(db)
23
- end
24
-
25
- private
26
- def create_db
27
- MessageDb.new
28
- end
29
-
30
- def run_smtp!(db)
31
- smtp_server = MalmSMTPServer.new(stmpport)
32
- smtp_server.mail_log = log
33
- smtp_server.message_db = db
20
+ def execute
21
+ malm_proc = proc{
22
+ malm = Malm.new(:log => log, :smtpport => smtpport, :webport => webport)
23
+ malm.run!
24
+ }
34
25
 
35
- begin
36
- smtp_server.start
37
- rescue Errno::EACCES
38
- STDERR.puts("Don't have permission to start SMTP server on port #{stmpport}. Maybe run with sudo?")
39
- exit 1
26
+ if start? || stop? || restart?
27
+ mode = nil
28
+ mode ||= start? ? "start" : nil
29
+ mode ||= stop? ? "stop" : nil
30
+ mode ||= restart? ? "restart" : nil
31
+
32
+ daemon_options = {
33
+ :ARGV => [mode],
34
+ :dir_mode => :normal,
35
+ :dir => piddir,
36
+ :multiple => true,
37
+ :monitor => true,
38
+ :app_name => "malm-#{smtpport}-#{webport}"
39
+ }
40
+
41
+ Daemons.run_proc("malm-#{smtpport}-#{webport}", daemon_options, &malm_proc)
42
+ else
43
+ malm_proc.call
40
44
  end
41
- smtp_server
42
- end
43
-
44
- def run_web!(db)
45
- MalmWeb.set :port, webport
46
- MalmWeb.set :message_db, db
47
- MalmWeb.run!
48
45
  end
49
46
  end
50
-
51
47
  MalmCommand.run
52
48
 
@@ -0,0 +1,33 @@
1
+ require 'thread'
2
+
3
+ class Malm
4
+ class MessageDb
5
+
6
+ def initialize
7
+ @sempahore = Mutex.new
8
+
9
+ @message_db = []
10
+ end
11
+
12
+ def create(message)
13
+ @sempahore.synchronize do
14
+ message[:id] = @message_db.size
15
+ @message_db << message
16
+ @message_db.size
17
+ end
18
+ end
19
+
20
+ def find_all
21
+ @sempahore.synchronize do
22
+ @message_db.dup
23
+ end
24
+ end
25
+
26
+ def find(id)
27
+ @sempahore.synchronize do
28
+ @message_db[id].dup
29
+ end
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,95 @@
1
+ # based on copy + paste from http://snippets.dzone.com/posts/show/5152
2
+ require 'gserver'
3
+
4
+ class Malm
5
+ class SMTPServer < GServer
6
+
7
+ attr_accessor :mail_log
8
+ attr_accessor :message_db
9
+
10
+ class Session
11
+ attr_accessor :data_mode, :data_mode, :email_body, :mail_from, :rcpt_to, :subject
12
+ alias :data_mode? :data_mode
13
+
14
+ def initialize
15
+ @data_mode = false
16
+ @email_body = ""
17
+ end
18
+
19
+ end
20
+
21
+ def serve(io)
22
+ session = Session.new
23
+ puts "Connected"
24
+ io.print "220 hello\r\n"
25
+ loop do
26
+ data = io.gets
27
+ puts ">>" + data
28
+ ok, op = process_line(data, session)
29
+ puts "<<" + op
30
+ io.print op
31
+ break unless ok
32
+ break if io.closed?
33
+ end
34
+ begin
35
+ io.close
36
+ db_insert(session)
37
+ rescue => e
38
+ log "something screwed up..."
39
+ log e.backtrace
40
+ end
41
+ end
42
+
43
+ def process_line(line, session)
44
+ if (session.data_mode?) && (line.chomp =~ /^\.$/)
45
+ session.data_mode = false
46
+ return true, "250 OK\r\n"
47
+ elsif session.data_mode?
48
+ session.email_body += line
49
+ return true, ""
50
+ elsif (line =~ /^(HELO|EHLO)/)
51
+ return true, "250 and..?\r\n"
52
+ elsif (line =~ /^QUIT/)
53
+ return false, "221 bye\r\n"
54
+ elsif (line =~ /^MAIL FROM\:/)
55
+ session.mail_from = (/^MAIL FROM\:<(.+)>.*$/).match(line)[1]
56
+ return true, "250 OK\r\n"
57
+ elsif (line =~ /^RCPT TO\:/)
58
+ session.rcpt_to = (/^RCPT TO\:<(.+)>.*$/).match(line)[1]
59
+ return true, "250 OK\r\n"
60
+ elsif (line =~ /^DATA/)
61
+ session.data_mode = true
62
+ return true, "354 Enter message, ending with \".\" on a line by itself\r\n"
63
+ else
64
+ return true, "500 ERROR\r\n"
65
+ end
66
+ end
67
+
68
+ def db_insert(session)
69
+ subject_regex = /^Subject\: (.+)$/
70
+
71
+ subject_match = subject_regex.match(session.email_body)
72
+ subject = subject_match ? subject_match[1] : ""
73
+ subject.strip!
74
+
75
+ message = {:subject => subject, :from => session.mail_from, :to => session.rcpt_to, :body => session.email_body}
76
+
77
+ @mail_log_fd.puts(message.inspect) if @mail_log_fd
78
+ @message_db.create(message) if @message_db
79
+
80
+ log("Message received: #{message.inspect}")
81
+ end
82
+
83
+ protected
84
+ def starting
85
+ @mail_log_fd = File.open(@mail_log, "a") if @mail_log
86
+ super
87
+ end
88
+
89
+ def stopping
90
+ @mail_log_fd.close if @mail_log_fd
91
+ super
92
+ end
93
+
94
+ end
95
+ end
data/lib/malm/web.rb ADDED
@@ -0,0 +1,63 @@
1
+ require 'thin'
2
+ require 'sinatra/base'
3
+ require 'json'
4
+ require 'v8'
5
+ require 'execjs'
6
+ require 'coffee_script'
7
+ require 'mail'
8
+
9
+ class Malm
10
+ class Web < Sinatra::Base
11
+
12
+ set :public, File.join(File.dirname(__FILE__), "..", "..", "web", "static")
13
+ set :views, File.join(File.dirname(__FILE__), "..", "..", "web", "views")
14
+
15
+ get "/messages.json" do
16
+ content_type :json
17
+ settings.message_db.find_all.map{|m|
18
+ m = m.dup
19
+ m.delete(:body)
20
+ m[:body_urls] = {:html => url("/messages/#{m[:id]}/body.html"), :text => url("/messages/#{m[:id]}/body.text")}
21
+ m
22
+ }.to_json
23
+ end
24
+
25
+ get "/messages/:id.json" do
26
+ content_type :json
27
+ find_message(params[:id]).to_json
28
+ end
29
+
30
+ get "/messages/:id/body.:type" do
31
+ render_message(params[:id], params[:type])
32
+ end
33
+
34
+ get "/coffee/:script.coffee" do
35
+ content_type "text/javascript"
36
+ coffee params[:script].to_sym
37
+ end
38
+
39
+ private
40
+ def find_message(id)
41
+ id = Integer(id)
42
+ settings.message_db.find(id)
43
+ end
44
+
45
+ def render_message(id, type)
46
+ content_type type
47
+
48
+ supported = ["text", "html"]
49
+ unless supported.include?(type.to_s)
50
+ halt 415, "don't know how to display message #{id} as #{type}. Try one of #{supported.join(",")}"
51
+ return
52
+ end
53
+
54
+ msg_obj = find_message(id)
55
+ if msg_obj
56
+ Mail::Message.new(msg_obj[:body]).send("#{type}_part".to_sym).body.to_s
57
+ else
58
+ status 404
59
+ "don't know about message #{id}"
60
+ end
61
+ end
62
+ end
63
+ end
data/lib/malm.rb ADDED
@@ -0,0 +1,42 @@
1
+ require 'malm/message_db'
2
+ require 'malm/smtp_server'
3
+ require 'malm/web'
4
+
5
+ class Malm
6
+ attr_accessor :log, :smtpport, :webport
7
+
8
+ def initialize(options={})
9
+ options.each{|k,v| send("#{k}=", v)}
10
+ end
11
+
12
+ def run!
13
+ create_db
14
+ run_smtp!
15
+ run_web!
16
+ end
17
+
18
+ private
19
+ def create_db
20
+ @db = Malm::MessageDb.new
21
+ end
22
+
23
+ def run_smtp!
24
+ smtp_server = Malm::SMTPServer.new(@smtpport)
25
+ smtp_server.mail_log = @log
26
+ smtp_server.message_db = @db
27
+
28
+ begin
29
+ smtp_server.start
30
+ rescue Errno::EACCES
31
+ STDERR.puts("Don't have permission to start SMTP server on port #{smtpport}. Maybe run with sudo?")
32
+ exit 1
33
+ end
34
+ smtp_server
35
+ end
36
+
37
+ def run_web!
38
+ Malm::Web.set :port, @webport
39
+ Malm::Web.set :message_db, @db
40
+ Malm::Web.run!
41
+ end
42
+ end
data/malm.gemspec CHANGED
@@ -5,17 +5,18 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{malm}
8
- s.version = "0.0.4"
8
+ s.version = "0.0.5"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["madlep"]
12
- s.date = %q{2011-07-14}
12
+ s.date = %q{2011-07-23}
13
13
  s.default_executable = %q{malm}
14
14
  s.description = %q{SMTP server with web interface for easy local development. Sets up a little mail server that you can send messages to, and provides a web front end to let you see what your app did.}
15
15
  s.email = %q{julian.doherty.ml@gmail.com}
16
16
  s.executables = ["malm"]
17
17
  s.extra_rdoc_files = [
18
- "LICENSE.txt"
18
+ "LICENSE.txt",
19
+ "README.md"
19
20
  ]
20
21
  s.files = [
21
22
  ".document",
@@ -23,21 +24,31 @@ Gem::Specification.new do |s|
23
24
  "Gemfile",
24
25
  "Gemfile.lock",
25
26
  "LICENSE.txt",
27
+ "README.md",
26
28
  "Rakefile",
27
29
  "VERSION",
28
30
  "bin/malm",
29
- "lib/malm_smtp_server.rb",
30
- "lib/malm_web.rb",
31
- "lib/message_db.rb",
31
+ "lib/malm.rb",
32
+ "lib/malm/message_db.rb",
33
+ "lib/malm/smtp_server.rb",
34
+ "lib/malm/web.rb",
32
35
  "malm.gemspec",
33
36
  "spec/bin/malm_test_message",
34
37
  "spec/help_spec.rb",
35
- "spec/spec_helper.rb"
38
+ "spec/spec_helper.rb",
39
+ "web/static/css/style.css",
40
+ "web/static/favicon.ico",
41
+ "web/static/index.html",
42
+ "web/static/js/backbone-min.js",
43
+ "web/static/js/backbone.js",
44
+ "web/static/js/jquery-1.6.2.js",
45
+ "web/static/js/jquery-1.6.2.min.js",
46
+ "web/views/hello_world.coffee"
36
47
  ]
37
48
  s.homepage = %q{http://github.com/madlep/malm}
38
49
  s.licenses = ["MIT"]
39
50
  s.require_paths = ["lib"]
40
- s.rubygems_version = %q{1.4.2}
51
+ s.rubygems_version = %q{1.6.2}
41
52
  s.summary = %q{Easy SMTP server for local development}
42
53
 
43
54
  if s.respond_to? :specification_version then
@@ -48,6 +59,11 @@ Gem::Specification.new do |s|
48
59
  s.add_runtime_dependency(%q<clamp>, ["= 0.2.1"])
49
60
  s.add_runtime_dependency(%q<sinatra>, ["= 1.2.6"])
50
61
  s.add_runtime_dependency(%q<thin>, ["= 1.2.11"])
62
+ s.add_runtime_dependency(%q<mail>, ["= 2.3.0"])
63
+ s.add_runtime_dependency(%q<therubyracer>, ["= 0.9.2"])
64
+ s.add_runtime_dependency(%q<execjs>, ["= 1.2.0"])
65
+ s.add_runtime_dependency(%q<coffee-script>, ["= 2.2.0"])
66
+ s.add_runtime_dependency(%q<daemons>, ["= 1.1.4"])
51
67
  s.add_development_dependency(%q<rspec>, ["~> 2.3.0"])
52
68
  s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
53
69
  s.add_development_dependency(%q<jeweler>, ["~> 1.6.2"])
@@ -57,6 +73,11 @@ Gem::Specification.new do |s|
57
73
  s.add_dependency(%q<clamp>, ["= 0.2.1"])
58
74
  s.add_dependency(%q<sinatra>, ["= 1.2.6"])
59
75
  s.add_dependency(%q<thin>, ["= 1.2.11"])
76
+ s.add_dependency(%q<mail>, ["= 2.3.0"])
77
+ s.add_dependency(%q<therubyracer>, ["= 0.9.2"])
78
+ s.add_dependency(%q<execjs>, ["= 1.2.0"])
79
+ s.add_dependency(%q<coffee-script>, ["= 2.2.0"])
80
+ s.add_dependency(%q<daemons>, ["= 1.1.4"])
60
81
  s.add_dependency(%q<rspec>, ["~> 2.3.0"])
61
82
  s.add_dependency(%q<bundler>, ["~> 1.0.0"])
62
83
  s.add_dependency(%q<jeweler>, ["~> 1.6.2"])
@@ -67,6 +88,11 @@ Gem::Specification.new do |s|
67
88
  s.add_dependency(%q<clamp>, ["= 0.2.1"])
68
89
  s.add_dependency(%q<sinatra>, ["= 1.2.6"])
69
90
  s.add_dependency(%q<thin>, ["= 1.2.11"])
91
+ s.add_dependency(%q<mail>, ["= 2.3.0"])
92
+ s.add_dependency(%q<therubyracer>, ["= 0.9.2"])
93
+ s.add_dependency(%q<execjs>, ["= 1.2.0"])
94
+ s.add_dependency(%q<coffee-script>, ["= 2.2.0"])
95
+ s.add_dependency(%q<daemons>, ["= 1.1.4"])
70
96
  s.add_dependency(%q<rspec>, ["~> 2.3.0"])
71
97
  s.add_dependency(%q<bundler>, ["~> 1.0.0"])
72
98
  s.add_dependency(%q<jeweler>, ["~> 1.6.2"])