malm 0.0.6 → 0.1.0
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/Gemfile +0 -3
- data/Gemfile.lock +1 -14
- data/Rakefile +23 -1
- data/VERSION +1 -1
- data/bin/malm +1 -1
- data/lib/malm/main.rb +40 -0
- data/lib/malm/message.rb +87 -0
- data/lib/malm/message_db.rb +1 -1
- data/lib/malm/smtp_server.rb +11 -58
- data/lib/malm/web.rb +11 -14
- data/lib/malm.rb +2 -39
- data/malm.gemspec +19 -27
- data/spec/bin/malm_test_message +30 -0
- data/spec/malm/message_spec.rb +160 -0
- data/spec/malm/smtp_server_spec.rb +44 -0
- data/spec/spec_helper.rb +1 -1
- data/web/{static → public}/css/style.css +19 -0
- data/web/{static → public}/favicon.ico +0 -0
- data/web/public/js/.gitignore +1 -0
- data/web/{static/js → public/js/lib}/backbone-min.js +0 -0
- data/web/{static/js → public/js/lib}/jquery-1.6.2.min.js +0 -0
- data/web/src/coffee/malm.coffee +106 -0
- data/web/views/index.html.erb +79 -0
- metadata +107 -214
- data/spec/help_spec.rb +0 -7
- data/web/static/index.html +0 -35
- data/web/static/js/backbone.js +0 -1149
- data/web/static/js/jquery-1.6.2.js +0 -8981
- data/web/views/hello_world.coffee +0 -2
@@ -0,0 +1,160 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe Malm::Message do
|
4
|
+
before(:each) do
|
5
|
+
@message = Malm::Message.new
|
6
|
+
end
|
7
|
+
|
8
|
+
describe "#process_line" do
|
9
|
+
describe "in INIT state" do
|
10
|
+
it "responds with hello" do
|
11
|
+
@message.process_line(:any_old_input)[:output].should == "220 hello\r\n"
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should transition to HELO state on any input" do
|
15
|
+
@message.process_line(nil)
|
16
|
+
@message.state.should == :helo_state
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "in HELO state" do
|
21
|
+
before(:each) do
|
22
|
+
@message.process_line(nil) # transition to HELO state
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "input 'HELO'" do
|
26
|
+
it "should be OK" do
|
27
|
+
@message.process_line("HELO")[:output].should == "250 OK\r\n"
|
28
|
+
end
|
29
|
+
it "should transition to MAIL state" do
|
30
|
+
@message.process_line("HELO")
|
31
|
+
@message.state.should == :mail_state
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "input 'EHLO'" do
|
36
|
+
it "should be OK" do
|
37
|
+
@message.process_line("EHLO")[:output].should == "250 OK\r\n"
|
38
|
+
end
|
39
|
+
it "should transition to MAIL state" do
|
40
|
+
@message.process_line("EHLO")
|
41
|
+
@message.state.should == :mail_state
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "in MAIL state" do
|
47
|
+
before(:each) do
|
48
|
+
@message.process_line(nil) # transition to HELO state
|
49
|
+
@message.process_line("HELO\r\n") # transition to MAIL state
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "input MAIL FROM <...>" do
|
53
|
+
before(:each) do
|
54
|
+
@reponse = @message.process_line("MAIL FROM:<francis@example.com>\r\n")
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should be OK" do
|
58
|
+
@reponse[:output].should == "250 OK\r\n"
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should transition to RCPT state" do
|
62
|
+
@message.state.should == :rcpt_state
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should set the from address" do
|
66
|
+
@message.mail_from.should == "francis@example.com"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe "in RCPT state" do
|
72
|
+
before(:each) do
|
73
|
+
@message.process_line(nil) # transition to HELO state
|
74
|
+
@message.process_line("HELO\r\n") # transition to MAIL state
|
75
|
+
@message.process_line("MAIL FROM:<francis@example.com>\r\n") # transition to RCPT state
|
76
|
+
end
|
77
|
+
|
78
|
+
describe "input RCPT TO:<...>" do
|
79
|
+
before(:each) do
|
80
|
+
@reponse = @message.process_line("RCPT TO:<louis@example.com>\r\n")
|
81
|
+
end
|
82
|
+
|
83
|
+
it "adds the recipient" do
|
84
|
+
@message.rcpt_to.should == ["louis@example.com"]
|
85
|
+
end
|
86
|
+
|
87
|
+
it "does not transition state" do
|
88
|
+
@message.state.should == :rcpt_state
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should be OK" do
|
92
|
+
@reponse[:output].should == "250 OK\r\n"
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
describe "input DATA" do
|
98
|
+
before(:each) do
|
99
|
+
@reponse = @message.process_line("DATA\r\n")
|
100
|
+
end
|
101
|
+
|
102
|
+
it "should be 354 response" do
|
103
|
+
@reponse[:output].should == "354 Enter message, ending with \".\" on a line by itself\r\n"
|
104
|
+
end
|
105
|
+
|
106
|
+
it "should transition to DATA state" do
|
107
|
+
@message.state.should == :data_state
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
describe "in DATA state" do
|
113
|
+
before(:each) do
|
114
|
+
@message.process_line(nil) # transition to HELO state
|
115
|
+
@message.process_line("HELO\r\n") # transition to MAIL state
|
116
|
+
@message.process_line("MAIL FROM:<francis@example.com>\r\n") # transition to RCPT state
|
117
|
+
@message.process_line("RCPT TO:<louis@example.com>\r\n") # transition to DATA state
|
118
|
+
@message.process_line("DATA\r\n") # transition to DATA state
|
119
|
+
end
|
120
|
+
|
121
|
+
it "accumulates data" do
|
122
|
+
@message.process_line("line 1\r\n")
|
123
|
+
@message.process_line("line 2\r\n")
|
124
|
+
@message.process_line("line 3\r\n")
|
125
|
+
@message.email_body.should == "line 1\r\nline 2\r\nline 3\r\n"
|
126
|
+
end
|
127
|
+
|
128
|
+
describe "input ." do
|
129
|
+
before(:each) do
|
130
|
+
@message.process_line("line 1\r\n")
|
131
|
+
@response = @message.process_line(".\r\n")
|
132
|
+
end
|
133
|
+
|
134
|
+
it "should be OK" do
|
135
|
+
@response[:output].should == "250 OK\r\n"
|
136
|
+
end
|
137
|
+
|
138
|
+
it "should transition to QUIT state" do
|
139
|
+
@message.state.should == :quit_state
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
describe "#subject" do
|
147
|
+
it "should be extracted from email body" do
|
148
|
+
@message.process_line(nil) # transition to HELO state
|
149
|
+
@message.process_line("HELO\r\n") # transition to MAIL state
|
150
|
+
@message.process_line("MAIL FROM:<francis@example.com>\r\n") # transition to RCPT state
|
151
|
+
@message.process_line("RCPT TO:<louis@example.com>\r\n") # transition to DATA state
|
152
|
+
@message.process_line("DATA\r\n") # transition to DATA state
|
153
|
+
|
154
|
+
@message.process_line("Subject: This is a subject\r\n")
|
155
|
+
@message.process_line("This is NOT a subject\r\n")
|
156
|
+
|
157
|
+
@message.subject.should == "This is a subject"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe Malm::SMTPServer do
|
4
|
+
before do
|
5
|
+
@db = mock("db")
|
6
|
+
@io = mock("io")
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "SMTP conversation" do
|
10
|
+
it "should receive a message" do
|
11
|
+
@io.should_receive(:closed?).at_least(:once).and_return(false)
|
12
|
+
|
13
|
+
@io.should_receive(:gets).and_return(
|
14
|
+
"HELO smtp.example.com\r\n",
|
15
|
+
"MAIL FROM:<from@example.com>\r\n",
|
16
|
+
"RCPT TO:<receiver1@example.com>\r\n",
|
17
|
+
"RCPT TO:<receiver2@example.com>\r\n",
|
18
|
+
"DATA\r\n",
|
19
|
+
"Subject: This is a test\r\n",
|
20
|
+
"This is a line\r\n",
|
21
|
+
"This is another line\r\n",
|
22
|
+
".\r\n",
|
23
|
+
"QUIT\r\n"
|
24
|
+
)
|
25
|
+
|
26
|
+
@io.should_receive(:print).with("220 hello\r\n").ordered
|
27
|
+
@io.should_receive(:print).with("250 OK\r\n").exactly(5).times.ordered
|
28
|
+
@io.should_receive(:print).with("354 Enter message, ending with \".\" on a line by itself\r\n").ordered
|
29
|
+
@io.should_receive(:print).with("221 bye\r\n").ordered
|
30
|
+
@io.should_receive(:close).ordered
|
31
|
+
|
32
|
+
@db.should_receive(:create).with(
|
33
|
+
:subject => "This is a test",
|
34
|
+
:from => "from@example.com",
|
35
|
+
:to => ["receiver1@example.com", "receiver2@example.com"],
|
36
|
+
:body => "Subject: This is a test\r\nThis is a line\r\nThis is another line\r\n")
|
37
|
+
|
38
|
+
smtp_server = Malm::SMTPServer.new(2525)
|
39
|
+
smtp_server.message_db = @db
|
40
|
+
|
41
|
+
smtp_server.serve(@io)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
2
|
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
3
3
|
require 'rspec'
|
4
|
-
require '
|
4
|
+
require 'malm'
|
5
5
|
|
6
6
|
# Requires supporting files with custom matchers and macros, etc,
|
7
7
|
# in ./support/ and its subdirectories.
|
@@ -81,7 +81,26 @@ a:hover { color: #036; }
|
|
81
81
|
* Author:
|
82
82
|
*/
|
83
83
|
|
84
|
+
/* list/content layout shamelessly stolen from http://documentcloud.github.com/backbone/ */
|
85
|
+
#messages {
|
86
|
+
position: fixed; top: 0; left: 0; bottom: 0; width: 200px;
|
87
|
+
background: white;
|
88
|
+
overflow-y: auto;
|
89
|
+
overflow-x: hidden;
|
90
|
+
padding: 15px 0 30px 30px;
|
91
|
+
border-right: 1px solid #DDD;
|
92
|
+
box-shadow: 0 0 20px #CCC;
|
93
|
+
-webkit-box-shadow: 0 0 20px #CCC;
|
94
|
+
-moz-box-shadow: 0 0 20px #CCC;
|
95
|
+
}
|
84
96
|
|
97
|
+
#message {
|
98
|
+
position: relative;
|
99
|
+
width: 700px;
|
100
|
+
margin: 40px 0 50px 260px;
|
101
|
+
overflow-x: visible;
|
102
|
+
overflow-y: visible;
|
103
|
+
}
|
85
104
|
|
86
105
|
|
87
106
|
|
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
*.js
|
File without changes
|
File without changes
|
@@ -0,0 +1,106 @@
|
|
1
|
+
@Malm = {}
|
2
|
+
|
3
|
+
@Malm.start = () ->
|
4
|
+
messageList = new MessageList()
|
5
|
+
messageRouter = new MessageRouter()
|
6
|
+
messageListView = new MessageListView("messageRouter": messageRouter, "collection": messageList)
|
7
|
+
messageBodyView = new MessageBodyView()
|
8
|
+
|
9
|
+
messageController = new MessageController(
|
10
|
+
"messageList" : messageList
|
11
|
+
"messageListView" : messageListView
|
12
|
+
"messageBodyView" : messageBodyView
|
13
|
+
)
|
14
|
+
messageRouter.messageController = messageController
|
15
|
+
messageListView.messageController = messageController
|
16
|
+
|
17
|
+
messageList.fetch("success": () -> Backbone.history.start())
|
18
|
+
|
19
|
+
class Message extends Backbone.Model
|
20
|
+
|
21
|
+
|
22
|
+
class MessageList extends Backbone.Collection
|
23
|
+
model: Message
|
24
|
+
url: "/messages"
|
25
|
+
|
26
|
+
comparator: (message) ->
|
27
|
+
message.id * -1
|
28
|
+
|
29
|
+
|
30
|
+
class MessageListView extends Backbone.View
|
31
|
+
el: '#messagesContent'
|
32
|
+
|
33
|
+
events:
|
34
|
+
'click .reload a' : 'reload'
|
35
|
+
|
36
|
+
initialize: () ->
|
37
|
+
@messageRouter = @options.messageRouter
|
38
|
+
@collection.bind('reset', @resetMessages)
|
39
|
+
|
40
|
+
resetMessages: (messages) =>
|
41
|
+
messages.each (message) =>
|
42
|
+
view = new MessageView("model": message, "messageRouter": @messageRouter)
|
43
|
+
$(@el).append(view.el)
|
44
|
+
|
45
|
+
reload: () ->
|
46
|
+
$(".messagesItem").remove()
|
47
|
+
@messageController.reload()
|
48
|
+
|
49
|
+
|
50
|
+
class MessageView extends Backbone.View
|
51
|
+
tagName: 'div'
|
52
|
+
className: 'messagesItem'
|
53
|
+
|
54
|
+
events:
|
55
|
+
'click .contentTypeLinks a' : 'showMessage'
|
56
|
+
|
57
|
+
initialize: () ->
|
58
|
+
@template = $('#messageViewTemplate').html()
|
59
|
+
@messageRouter = @options.messageRouter
|
60
|
+
@render()
|
61
|
+
|
62
|
+
render: () ->
|
63
|
+
html = Mustache.to_html(@template,
|
64
|
+
"subject": @model.get("subject"),
|
65
|
+
"htmlUrl": @model.get("body_urls").html,
|
66
|
+
"textUrl": @model.get("body_urls").text
|
67
|
+
)
|
68
|
+
$(@el).append(html)
|
69
|
+
|
70
|
+
showMessage: (e) ->
|
71
|
+
contentType = e.target.className
|
72
|
+
@messageRouter.navigate("/messages/#{@model.id}/body.#{contentType}", true)
|
73
|
+
|
74
|
+
|
75
|
+
class MessageBodyView extends Backbone.View
|
76
|
+
el: "#messageBody"
|
77
|
+
|
78
|
+
initialize: () ->
|
79
|
+
@template = $('#messageBodyTemplate').html()
|
80
|
+
@contentType = @options.contentType
|
81
|
+
|
82
|
+
render: () ->
|
83
|
+
html = Mustache.to_html(@template, "bodyUrl": @model.get("body_urls")[@contentType])
|
84
|
+
$(@el).html(html)
|
85
|
+
|
86
|
+
class MessageController
|
87
|
+
constructor: (options) ->
|
88
|
+
@messageList = options.messageList
|
89
|
+
|
90
|
+
showMessage: (messageId, contentType) ->
|
91
|
+
message = @messageList.get(messageId)
|
92
|
+
bodyView = new MessageBodyView("model":message, "contentType": contentType)
|
93
|
+
bodyView.render()
|
94
|
+
|
95
|
+
reload: () ->
|
96
|
+
@messageList.reset()
|
97
|
+
@messageList.fetch()
|
98
|
+
|
99
|
+
|
100
|
+
class MessageRouter extends Backbone.Router
|
101
|
+
routes: {
|
102
|
+
'/messages/:id/body.:contentType' : 'showMesage'
|
103
|
+
}
|
104
|
+
|
105
|
+
showMesage: (messageId, contentType) ->
|
106
|
+
@messageController.showMessage(messageId, contentType)
|
@@ -0,0 +1,79 @@
|
|
1
|
+
<!doctype>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8">
|
5
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
6
|
+
|
7
|
+
<title>malm</title>
|
8
|
+
<meta name="description" content="malm mail server web view">
|
9
|
+
<meta name="author" content="">
|
10
|
+
|
11
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
12
|
+
<link rel="shortcut icon" href="/favicon.ico">
|
13
|
+
|
14
|
+
<link rel="stylesheet" href="css/style.css?v=2">
|
15
|
+
|
16
|
+
<script type="text/html" id="messageViewTemplate">
|
17
|
+
<p>
|
18
|
+
<div class="subject">{{subject}}</div>
|
19
|
+
<div class="contentTypeLinks">
|
20
|
+
{{#htmlUrl}}
|
21
|
+
<a class="html" href="javascript: void(0)">HTML</a>
|
22
|
+
{{/htmlUrl}}
|
23
|
+
{{#textUrl}}
|
24
|
+
<a class="text" href="javascript: void(0)">plain text</a>
|
25
|
+
{{/textUrl}}
|
26
|
+
</div>
|
27
|
+
</p>
|
28
|
+
</script>
|
29
|
+
|
30
|
+
<script type="text/html" id="messageBodyTemplate">
|
31
|
+
<span>Subject:</span> <span>{{message.subject}}</p>
|
32
|
+
<iframe src="{{bodyUrl}}" style="width:100%;height:100%"/>
|
33
|
+
</script>
|
34
|
+
|
35
|
+
|
36
|
+
</head>
|
37
|
+
|
38
|
+
<body>
|
39
|
+
|
40
|
+
<div id="container">
|
41
|
+
<header>
|
42
|
+
<h1>malm</h1>
|
43
|
+
</header>
|
44
|
+
|
45
|
+
<div id="main" role="main">
|
46
|
+
<div id="messages">
|
47
|
+
<h2>Messages</h2>
|
48
|
+
<div id="messagesContent">
|
49
|
+
<div class="reload"><a href="javascript: void(0)">Reload</a></div>
|
50
|
+
|
51
|
+
</div>
|
52
|
+
</div>
|
53
|
+
<div id="message">
|
54
|
+
<h1>Message</h1>
|
55
|
+
<div id="messageBody"><p>Click message to load content...</p></div>
|
56
|
+
</div>
|
57
|
+
</div>
|
58
|
+
|
59
|
+
<footer>
|
60
|
+
<p>I blame <a href="https://twitter.com/#!/madlep">@madlep</a> for <a href="https://github.com/madlep/malm">malm</a></p>
|
61
|
+
</footer>
|
62
|
+
</div> <!-- eo #container -->
|
63
|
+
|
64
|
+
<script src="/js/lib/jquery-1.6.2.min.js"></script>
|
65
|
+
<script src="/js/lib/underscore-min.js"></script>
|
66
|
+
<script src="/js/lib/backbone-min.js"></script>
|
67
|
+
<script src="/js/lib/mustache.js"></script>
|
68
|
+
|
69
|
+
<script src="/js/malm.js"></script>
|
70
|
+
|
71
|
+
|
72
|
+
<script type="text/javascript">
|
73
|
+
$(function(){
|
74
|
+
Malm.start();
|
75
|
+
});
|
76
|
+
</script>
|
77
|
+
|
78
|
+
</body>
|
79
|
+
</html>
|