received 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/.gitignore +5 -0
- data/.rspec +2 -0
- data/Gemfile +7 -0
- data/LICENSE +20 -0
- data/README.md +63 -0
- data/Rakefile +10 -0
- data/bin/received +55 -0
- data/lib/received/backend/base.rb +13 -0
- data/lib/received/backend/mongodb.rb +26 -0
- data/lib/received/connection.rb +52 -0
- data/lib/received/lmtp.rb +127 -0
- data/lib/received/server.rb +104 -0
- data/lib/received/version.rb +3 -0
- data/lib/received.rb +5 -0
- data/received.gemspec +24 -0
- data/spec/lmtp_spec.rb +51 -0
- data/spec/spec_helper.rb +1 -0
- metadata +150 -0
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Roman Shterenzon
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
ReceiveD
|
2
|
+
========
|
3
|
+
|
4
|
+
ReceiveD is yet another way for receiving mail with Rails.
|
5
|
+
Why have yet another subsystem (like IMAP), when you can deliver the mail
|
6
|
+
directly to your data store?
|
7
|
+
|
8
|
+
ReceiveD is almost [RFC2033][1] compliant LMTP server built around
|
9
|
+
[eventmachine][2] and as such should be quite fast.
|
10
|
+
|
11
|
+
The receive daemon will listen on TCP or UNIX socket, and write the mail
|
12
|
+
to the backend storage.
|
13
|
+
|
14
|
+
Currently only [MongoDB][3] is supported, but writing another backend
|
15
|
+
(MySQL, Redis, etc.) is trivial.
|
16
|
+
|
17
|
+
|
18
|
+
Installation
|
19
|
+
------------
|
20
|
+
`sudo gem install received`
|
21
|
+
|
22
|
+
Modify your [Postfix][4] configuration to deliver mail via LMTP to TCP or UNIX socket.
|
23
|
+
|
24
|
+
Example main.cf:
|
25
|
+
virtual_transport = lmtp:192.168.2.106:1111
|
26
|
+
virtual_mailbox_domains = example.com
|
27
|
+
|
28
|
+
Create a YAML configuration file which has the following parameters:
|
29
|
+
{'production'=>{'host'=>hostname, 'database'=>db, 'collection'=>col}}
|
30
|
+
|
31
|
+
The mongoid.yml will do, just add the name of collection, i.e.
|
32
|
+
|
33
|
+
production:
|
34
|
+
<<: *defaults
|
35
|
+
database: foo_production
|
36
|
+
collection: inbox
|
37
|
+
|
38
|
+
The default environment is *production*, but you can specify other environment
|
39
|
+
using RAILS_ENV environment variable.
|
40
|
+
In this case, make sure you have the relevant key in your configuration file.
|
41
|
+
|
42
|
+
|
43
|
+
Running
|
44
|
+
-------
|
45
|
+
Check -h for help, port/unix socket path and config file are required.
|
46
|
+
|
47
|
+
|
48
|
+
Bugs and missing features
|
49
|
+
-------------------------
|
50
|
+
|
51
|
+
* When using UNIX socket the permissions/ownership are not changed. Use -u and -g when running
|
52
|
+
as daemon or change the permissions/ownership manually.
|
53
|
+
* ReceiveD wasn't really tested for compliance with RFC2033
|
54
|
+
* It doesn't implement [RFC2034][5] (ENHANCEDSTATUSCODES), because Postfix doesn't seem to care
|
55
|
+
* It doesn't perform any validation of the provided input, e.g. LHLO, MAIL FROM, RCPT TO
|
56
|
+
|
57
|
+
[1]: http://tools.ietf.org/html/rfc2033
|
58
|
+
[2]: http://rubyeventmachine.com/
|
59
|
+
[3]: http://www.mongodb.org/
|
60
|
+
[4]: http://www.postfix.org/
|
61
|
+
[5]: http://tools.ietf.org/html/rfc2034
|
62
|
+
|
63
|
+
Copyright (c) 2011 Roman Shterenzon, released under the MIT license
|
data/Rakefile
ADDED
data/bin/received
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'rubygems'
|
3
|
+
require 'logger'
|
4
|
+
require 'optparse'
|
5
|
+
$:<< File.expand_path('../../lib', __FILE__)
|
6
|
+
require 'received'
|
7
|
+
|
8
|
+
options = {}
|
9
|
+
OptionParser.new do |opts|
|
10
|
+
opts.banner = 'Usage: received [options]'
|
11
|
+
opts.on('-c', '--config FILE', 'Config file name (required)') {|v| options[:config] = v}
|
12
|
+
opts.on('-b', '--backend BACKEND', [:mongodb], 'Backend (default: mongodb)') {|v| options[:backend] = v}
|
13
|
+
opts.on('-d', '--daemonize', 'Become a daemon') {|v| options[:daemon] = v}
|
14
|
+
opts.on('-s', '--unix-socket PATH', 'Use UNIX socket') {|v| options[:unix_socket] = v}
|
15
|
+
opts.on('-p', '--port NUM', 'Listen to TCP port') {|v| options[:port] = v.to_i}
|
16
|
+
opts.on('-i', '--host NAME', 'Bind to this IP (default: 127.0.0.1)') {|v| options[:host] = v}
|
17
|
+
opts.on('-a', '--piddir PATH', 'Directory for pid file (default: /var/tmp)') {|v| options[:dir] = v}
|
18
|
+
opts.on('-l', '--log FILE', 'Log file name (default: piddir/received.log)') {|v| options[:logfile] = v}
|
19
|
+
opts.on('-u', '--user NAME', 'Effective user when daemon (default: nobody)') {|v| options[:user] = v}
|
20
|
+
opts.on('-g', '--group NAME', 'Effective group when daemon (default: nobody)') {|v| options[:group] = v}
|
21
|
+
opts.on('-v', '--verbose', 'Verbose logging') {options[:level] = Logger::DEBUG}
|
22
|
+
opts.on_tail('-h', '--help', 'Show this message') do
|
23
|
+
puts opts
|
24
|
+
exit
|
25
|
+
end
|
26
|
+
end.parse!
|
27
|
+
|
28
|
+
raise "Config file is required, please provide with -c config.yml" unless options[:config]
|
29
|
+
|
30
|
+
# Default backend
|
31
|
+
options[:backend] ||= 'mongodb'
|
32
|
+
|
33
|
+
options[:logger] = Logger.new(options[:logfile] || $stderr).tap do |logger|
|
34
|
+
logger.level = options[:level] || Logger::INFO
|
35
|
+
end
|
36
|
+
|
37
|
+
if options.delete(:daemon)
|
38
|
+
require 'daemons'
|
39
|
+
# Monkey patch to rename log file
|
40
|
+
class Daemons::Application
|
41
|
+
def output_logfile
|
42
|
+
(options[:log_output] && logdir) ? File.join(logdir, @group.app_name + '.log') : nil
|
43
|
+
end
|
44
|
+
end
|
45
|
+
params = {:app_name => 'received', :log_output => true, :dir_mode => :normal, :dir => options[:dir] || '/var/tmp'}
|
46
|
+
# Drop privileges if started as superuser
|
47
|
+
params.merge!({:user => options[:user] || 'nobody', :group => options[:group] || 'nobody'}) if Process.uid == 0
|
48
|
+
Daemons.daemonize(params)
|
49
|
+
end
|
50
|
+
|
51
|
+
server = Received::Server.new(options)
|
52
|
+
%w(TERM INT).each do |sig|
|
53
|
+
Signal.trap(sig) {server.stop}
|
54
|
+
end
|
55
|
+
server.serve!
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'mongo'
|
2
|
+
|
3
|
+
module Received
|
4
|
+
module Backend
|
5
|
+
class Mongodb < Base
|
6
|
+
|
7
|
+
# Initialize MongoDB storage backend
|
8
|
+
#
|
9
|
+
# @param [Hash] params
|
10
|
+
# @option params [String] host
|
11
|
+
# @option params [String] database
|
12
|
+
# @option params [String] collection
|
13
|
+
def initialize(params)
|
14
|
+
@db = Mongo::Connection.new(params['host']).db(params['database'])
|
15
|
+
@coll = @db.collection(params['collection'])
|
16
|
+
end
|
17
|
+
|
18
|
+
# Store mail in MongoDB
|
19
|
+
#
|
20
|
+
# @param [Hash] mail
|
21
|
+
def store(mail)
|
22
|
+
@coll.save(mail.merge({:ts => Time.now.to_i}), :safe => true)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'received/lmtp'
|
3
|
+
|
4
|
+
module Received
|
5
|
+
class Connection < EM::Connection
|
6
|
+
|
7
|
+
def initialize(server, backend)
|
8
|
+
@server, @backend = server, backend
|
9
|
+
@proto = LMTP.new(self)
|
10
|
+
end
|
11
|
+
|
12
|
+
def post_init
|
13
|
+
logger.debug "new connection"
|
14
|
+
@proto.start!
|
15
|
+
end
|
16
|
+
|
17
|
+
def receive_data(data)
|
18
|
+
logger.debug {"receiving data: #{data.inspect}"}
|
19
|
+
@proto.on_data(data)
|
20
|
+
end
|
21
|
+
|
22
|
+
def send_data(data)
|
23
|
+
logger.debug {"sending data: #{data.inspect}"}
|
24
|
+
super
|
25
|
+
end
|
26
|
+
|
27
|
+
# Client disconnected
|
28
|
+
def unbind
|
29
|
+
logger.debug "connection closed"
|
30
|
+
@server.remove_connection(self)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Callback, called by protocol handler
|
34
|
+
#
|
35
|
+
# @param [Hash] mail
|
36
|
+
# @option mail [String] :from
|
37
|
+
# @option mail [Array] :rcpt
|
38
|
+
# @option mail [String] :body
|
39
|
+
def mail_received(mail)
|
40
|
+
begin
|
41
|
+
@backend.store(mail)
|
42
|
+
logger.info "stored mail from: #{mail[:from]}"
|
43
|
+
rescue Exception => e
|
44
|
+
logger.error "saving failed with: #{e.message}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def logger
|
49
|
+
@server.logger
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module Received
|
2
|
+
# RFC2033
|
3
|
+
class LMTP
|
4
|
+
|
5
|
+
def initialize(conn)
|
6
|
+
@conn = conn
|
7
|
+
end
|
8
|
+
|
9
|
+
def on_data(data)
|
10
|
+
@buf += data
|
11
|
+
while line = @buf.slice!(/.+\r\n/)
|
12
|
+
line.chomp! unless @state == :data
|
13
|
+
event(line)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def start!
|
18
|
+
@state = :start
|
19
|
+
@buf = ''
|
20
|
+
@from = nil
|
21
|
+
@rcpt = []
|
22
|
+
event(nil)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
def event(ev)
|
27
|
+
@conn.logger.debug {"state was: #{@state.inspect}"}
|
28
|
+
@state = case @state
|
29
|
+
when :start
|
30
|
+
@body = []
|
31
|
+
banner
|
32
|
+
:banner_sent
|
33
|
+
when :banner_sent
|
34
|
+
if ev.start_with?('LHLO')
|
35
|
+
lhlo_response
|
36
|
+
extensions
|
37
|
+
:lhlo_received
|
38
|
+
else
|
39
|
+
error
|
40
|
+
end
|
41
|
+
when :lhlo_received
|
42
|
+
if ev =~ /MAIL FROM:<?([^>]+)/
|
43
|
+
@from = $1
|
44
|
+
ok
|
45
|
+
:mail_from_received
|
46
|
+
else
|
47
|
+
error
|
48
|
+
end
|
49
|
+
when :mail_from_received
|
50
|
+
if ev =~ /RCPT TO:<?([^>]+)/
|
51
|
+
@rcpt << $1
|
52
|
+
ok
|
53
|
+
:rcpt_to_received
|
54
|
+
else
|
55
|
+
error
|
56
|
+
end
|
57
|
+
when :rcpt_to_received
|
58
|
+
if ev =~ /RCPT TO:<?([^>]+)/
|
59
|
+
@rcpt << $1
|
60
|
+
ok
|
61
|
+
elsif ev == "DATA"
|
62
|
+
start_mail_input
|
63
|
+
:data
|
64
|
+
else
|
65
|
+
error
|
66
|
+
end
|
67
|
+
when :data
|
68
|
+
if ev == ".\r\n"
|
69
|
+
@rcpt.size.times {ok}
|
70
|
+
mail = {:from => @from, :rcpt => @rcpt, :body => @body.join}
|
71
|
+
@conn.mail_received(mail)
|
72
|
+
:data_received
|
73
|
+
else
|
74
|
+
@body << ev
|
75
|
+
:data
|
76
|
+
end
|
77
|
+
when :data_received
|
78
|
+
if ev == "QUIT"
|
79
|
+
closing_connection
|
80
|
+
:start
|
81
|
+
else
|
82
|
+
error
|
83
|
+
end
|
84
|
+
else
|
85
|
+
raise "Where am I? (#{@state.inspect})"
|
86
|
+
end || @state
|
87
|
+
@conn.logger.debug {"state now: #{@state.inspect}"}
|
88
|
+
end
|
89
|
+
|
90
|
+
def banner
|
91
|
+
emit "220 localhost LMTP server ready"
|
92
|
+
end
|
93
|
+
|
94
|
+
def lhlo_response
|
95
|
+
emit "250-localhost"
|
96
|
+
end
|
97
|
+
|
98
|
+
def start_mail_input
|
99
|
+
emit "354 End data with <CR><LF>.<CR><LF>"
|
100
|
+
end
|
101
|
+
|
102
|
+
def closing_connection
|
103
|
+
emit "221 Bye"
|
104
|
+
@conn.close_connection_after_writing
|
105
|
+
end
|
106
|
+
|
107
|
+
# FIXME: RFC2033 requires ENHANCEDSTATUSCODES,
|
108
|
+
# but it's not used in Postfix
|
109
|
+
def extensions
|
110
|
+
emit "250-8BITMIME\r\n250 PIPELINING"
|
111
|
+
end
|
112
|
+
|
113
|
+
def ok
|
114
|
+
emit "250 OK"
|
115
|
+
end
|
116
|
+
|
117
|
+
def error
|
118
|
+
emit "500 command unrecognized"
|
119
|
+
end
|
120
|
+
|
121
|
+
def emit(str)
|
122
|
+
@conn.send_data "#{str}\r\n"
|
123
|
+
# return nil, so there won't be implicit state transition
|
124
|
+
nil
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
#require 'active_support/core_ext/string/inflections'
|
2
|
+
require 'yaml'
|
3
|
+
require 'eventmachine'
|
4
|
+
require 'received/connection'
|
5
|
+
|
6
|
+
module Received
|
7
|
+
class Server
|
8
|
+
attr_reader :logger, :options
|
9
|
+
|
10
|
+
def initialize(options)
|
11
|
+
@options = options
|
12
|
+
@logger = options[:logger] || Logger.new($stderr)
|
13
|
+
@connections = []
|
14
|
+
# For how long the server will wait for connections to finish
|
15
|
+
@grace_period = options[:grace_period] || 10
|
16
|
+
create_backend
|
17
|
+
end
|
18
|
+
|
19
|
+
def serve!
|
20
|
+
EventMachine.run { start }
|
21
|
+
end
|
22
|
+
|
23
|
+
def start
|
24
|
+
unless options[:unix_socket] or options[:port]
|
25
|
+
raise "No port or UNIX socket path were provided"
|
26
|
+
end
|
27
|
+
set_title
|
28
|
+
if host = options[:unix_socket]
|
29
|
+
port = nil
|
30
|
+
else
|
31
|
+
host = options[:host] || '127.0.0.1'
|
32
|
+
port = options[:port]
|
33
|
+
end
|
34
|
+
logger.info "Starting server on #{host}#{port ? ":" + port.to_s : ''}"
|
35
|
+
@signature = EventMachine.start_server(host, port, Received::Connection, self, @backend) do |conn|
|
36
|
+
add_connection(conn)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def stop
|
41
|
+
return if stopping?
|
42
|
+
logger.info "Stopping server"
|
43
|
+
EventMachine.stop_server(@signature)
|
44
|
+
@stopped_at = Time.now
|
45
|
+
unless wait_for_connections_and_stop
|
46
|
+
# Still some connections running, schedule a check later
|
47
|
+
EventMachine.add_periodic_timer(1) { wait_for_connections_and_stop }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Checks whether the server is in stopping mode
|
52
|
+
def stopping?
|
53
|
+
!!@stopped_at
|
54
|
+
end
|
55
|
+
|
56
|
+
# Checks if the server is processing any connections
|
57
|
+
def idle?
|
58
|
+
@connections.empty?
|
59
|
+
end
|
60
|
+
|
61
|
+
def remove_connection(conn)
|
62
|
+
@connections.delete(conn)
|
63
|
+
set_title
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
# Sets the process title as seen in ps
|
69
|
+
def set_title
|
70
|
+
$0 = "received (#{@connections.size} connections)"
|
71
|
+
end
|
72
|
+
|
73
|
+
# Whether grace period is over
|
74
|
+
def grace_ended?
|
75
|
+
Time.now - @stopped_at > @grace_period
|
76
|
+
end
|
77
|
+
|
78
|
+
def wait_for_connections_and_stop
|
79
|
+
if idle? or grace_ended?
|
80
|
+
EventMachine.stop
|
81
|
+
true
|
82
|
+
else
|
83
|
+
logger.info "Waiting for #{@connections.size} connection(s) to finish..."
|
84
|
+
false
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def add_connection(conn)
|
89
|
+
@connections << conn
|
90
|
+
set_title
|
91
|
+
end
|
92
|
+
|
93
|
+
def create_backend
|
94
|
+
backend = options[:backend].to_s
|
95
|
+
require 'received/backend/' + backend
|
96
|
+
#klass = ('Received::Backend::' + backend.camelize).constantize
|
97
|
+
klass = eval('Received::Backend::' + backend.capitalize)
|
98
|
+
env = ENV['RAILS_ENV'] || 'production'
|
99
|
+
config = YAML.load(File.read(options[:config]))[env]
|
100
|
+
@backend = klass.new(config)
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
end
|
data/lib/received.rb
ADDED
data/received.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "received/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "received"
|
7
|
+
s.version = Received::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Roman Shterenzon"]
|
10
|
+
s.email = ["romanbsd@yahoo.com"]
|
11
|
+
s.homepage = ""
|
12
|
+
s.summary = %q{Receive mail from Postfix and store it somewhere}
|
13
|
+
s.description = %q{Currently stores received mail in MongoDB}
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
s.add_runtime_dependency 'activesupport'
|
20
|
+
s.add_runtime_dependency 'daemons'
|
21
|
+
s.add_runtime_dependency 'eventmachine'
|
22
|
+
s.add_runtime_dependency 'mongo', '~>1.3.0'
|
23
|
+
s.add_runtime_dependency 'bson_ext', '~>1.3.0'
|
24
|
+
end
|
data/spec/lmtp_spec.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
describe Received::LMTP do
|
5
|
+
before :each do
|
6
|
+
@mock = mock 'conn'
|
7
|
+
@mock.should_receive(:send_data).with("220 localhost LMTP server ready\r\n")
|
8
|
+
@mock.stub!(:logger).and_return(Logger.new($stderr))
|
9
|
+
@mock.logger.debug "*** Starting test ***"
|
10
|
+
@proto = Received::LMTP.new(@mock)
|
11
|
+
@proto.start!
|
12
|
+
end
|
13
|
+
|
14
|
+
it "does full receive flow" do
|
15
|
+
@mock.should_receive(:send_data).with("250-localhost\r\n")
|
16
|
+
@mock.should_receive(:send_data).with("250-8BITMIME\r\n250 PIPELINING\r\n")
|
17
|
+
@mock.should_receive(:send_data).with("250 OK\r\n").exactly(3).times
|
18
|
+
@mock.should_receive(:send_data).with("354 End data with <CR><LF>.<CR><LF>\r\n")
|
19
|
+
@mock.should_receive(:send_data).with("250 OK\r\n").exactly(2).times
|
20
|
+
@mock.should_receive(:send_data).with("221 Bye\r\n")
|
21
|
+
body = "Subject: spec\r\nspec\r\n"
|
22
|
+
@mock.should_receive(:mail_received).with({
|
23
|
+
:from => 'spec1@example.com',
|
24
|
+
:rcpt => ['spec2@example.com', 'spec3@example.com'],
|
25
|
+
:body => body
|
26
|
+
})
|
27
|
+
@mock.should_receive(:close_connection_after_writing)
|
28
|
+
|
29
|
+
["LHLO", "MAIL FROM:<spec1@example.com>", "RCPT TO:<spec2@example.com>",
|
30
|
+
"RCPT TO:<spec3@example.com>", "DATA", "#{body}.", "QUIT"].each do |line|
|
31
|
+
@mock.logger.debug "client: #{line}"
|
32
|
+
@proto.on_data(line + "\r\n")
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
it "parses multiline" do
|
38
|
+
@mock.should_receive(:send_data).with("250-localhost\r\n")
|
39
|
+
@mock.should_receive(:send_data).with("250-8BITMIME\r\n250 PIPELINING\r\n")
|
40
|
+
@mock.should_receive(:send_data).with("250 OK\r\n")
|
41
|
+
@proto.on_data("LHLO\r\nMAIL FROM:<spec@example.com>\r\n")
|
42
|
+
end
|
43
|
+
|
44
|
+
it "buffers commands up to CR/LF" do
|
45
|
+
@mock.should_receive(:send_data).with("250-localhost\r\n")
|
46
|
+
@mock.should_receive(:send_data).with("250-8BITMIME\r\n250 PIPELINING\r\n")
|
47
|
+
@mock.should_receive(:send_data).with("250 OK\r\n")
|
48
|
+
@proto.on_data("LHLO\r\nMAIL FROM")
|
49
|
+
@proto.on_data(":<spec@example.com>\r\n")
|
50
|
+
end
|
51
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'received'
|
metadata
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: received
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 0.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Roman Shterenzon
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2011-05-01 00:00:00 +03:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: activesupport
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
version: "0"
|
31
|
+
type: :runtime
|
32
|
+
version_requirements: *id001
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: daemons
|
35
|
+
prerelease: false
|
36
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
37
|
+
none: false
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 0
|
43
|
+
version: "0"
|
44
|
+
type: :runtime
|
45
|
+
version_requirements: *id002
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: eventmachine
|
48
|
+
prerelease: false
|
49
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
segments:
|
55
|
+
- 0
|
56
|
+
version: "0"
|
57
|
+
type: :runtime
|
58
|
+
version_requirements: *id003
|
59
|
+
- !ruby/object:Gem::Dependency
|
60
|
+
name: mongo
|
61
|
+
prerelease: false
|
62
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
63
|
+
none: false
|
64
|
+
requirements:
|
65
|
+
- - ~>
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
segments:
|
68
|
+
- 1
|
69
|
+
- 3
|
70
|
+
- 0
|
71
|
+
version: 1.3.0
|
72
|
+
type: :runtime
|
73
|
+
version_requirements: *id004
|
74
|
+
- !ruby/object:Gem::Dependency
|
75
|
+
name: bson_ext
|
76
|
+
prerelease: false
|
77
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
78
|
+
none: false
|
79
|
+
requirements:
|
80
|
+
- - ~>
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
segments:
|
83
|
+
- 1
|
84
|
+
- 3
|
85
|
+
- 0
|
86
|
+
version: 1.3.0
|
87
|
+
type: :runtime
|
88
|
+
version_requirements: *id005
|
89
|
+
description: Currently stores received mail in MongoDB
|
90
|
+
email:
|
91
|
+
- romanbsd@yahoo.com
|
92
|
+
executables:
|
93
|
+
- received
|
94
|
+
extensions: []
|
95
|
+
|
96
|
+
extra_rdoc_files: []
|
97
|
+
|
98
|
+
files:
|
99
|
+
- .gitignore
|
100
|
+
- .rspec
|
101
|
+
- Gemfile
|
102
|
+
- LICENSE
|
103
|
+
- README.md
|
104
|
+
- Rakefile
|
105
|
+
- bin/received
|
106
|
+
- lib/received.rb
|
107
|
+
- lib/received/backend/base.rb
|
108
|
+
- lib/received/backend/mongodb.rb
|
109
|
+
- lib/received/connection.rb
|
110
|
+
- lib/received/lmtp.rb
|
111
|
+
- lib/received/server.rb
|
112
|
+
- lib/received/version.rb
|
113
|
+
- received.gemspec
|
114
|
+
- spec/lmtp_spec.rb
|
115
|
+
- spec/spec_helper.rb
|
116
|
+
has_rdoc: true
|
117
|
+
homepage: ""
|
118
|
+
licenses: []
|
119
|
+
|
120
|
+
post_install_message:
|
121
|
+
rdoc_options: []
|
122
|
+
|
123
|
+
require_paths:
|
124
|
+
- lib
|
125
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
126
|
+
none: false
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
segments:
|
131
|
+
- 0
|
132
|
+
version: "0"
|
133
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
134
|
+
none: false
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
segments:
|
139
|
+
- 0
|
140
|
+
version: "0"
|
141
|
+
requirements: []
|
142
|
+
|
143
|
+
rubyforge_project:
|
144
|
+
rubygems_version: 1.3.7
|
145
|
+
signing_key:
|
146
|
+
specification_version: 3
|
147
|
+
summary: Receive mail from Postfix and store it somewhere
|
148
|
+
test_files:
|
149
|
+
- spec/lmtp_spec.rb
|
150
|
+
- spec/spec_helper.rb
|