ruby-qmail 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.rdoc +66 -0
- data/Rakefile +45 -0
- data/VERSION +1 -0
- data/lib/bounce.rb +44 -0
- data/lib/config.rb +31 -0
- data/lib/netstring.rb +13 -0
- data/lib/queue.rb +248 -0
- data/lib/ruby-qmail.rb +6 -0
- data/spec/ruby-qmail_spec.rb +33 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +9 -0
- metadata +79 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Allen Fair
|
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.rdoc
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
= ruby-qmail
|
2
|
+
|
3
|
+
The RubyQmail Plugin provides support for the Qmail MTA and helpers for creating data-aware applications
|
4
|
+
While initially intended to operate in a large-scale Qmail environment, some of these features may be useful for
|
5
|
+
applications on other platforms. In fact, this plugin is not required if you are using Qmail because
|
6
|
+
ActionMailer's sendmail and smtp configurations will work for simple Qmail installations.
|
7
|
+
|
8
|
+
NOTE: This is still in an early stage of development, but has been tested to insert email into the qmail queue
|
9
|
+
via the qmail-queue and QMQP protocols.
|
10
|
+
|
11
|
+
== Using ruby-qmail
|
12
|
+
|
13
|
+
Ruby Qmail takes a message in 3 parts
|
14
|
+
* The envelope return path. This is not necessarily the same as the From: header, and is the email address to which
|
15
|
+
bounces will be sent. This should most likely be a special address used to identify the mailing being sent,
|
16
|
+
containing the message number or other identifier, and perhaps the source of the message. For example:
|
17
|
+
bounces-messageid@example.com
|
18
|
+
Ruby-Qmail used VERP (Variable Envelope Return Path) by default, so bounces will be returned in the form
|
19
|
+
bounces-messageid-recipientmailbox=recipientdomain@example.com
|
20
|
+
for individual returns. Qmail will also send a report to the return path (without the recipient address encoded)
|
21
|
+
bounces-messageid-@example.com
|
22
|
+
with addresses and error messages for all undeliverable addresses.
|
23
|
+
* Recipient List. This is either a String containing an email address (only the user@domain part), an array of
|
24
|
+
email addresses, or a file of email addresses (one per line), or any other object implementing Enumerable.
|
25
|
+
* Message Data. This is a String or Filename of a raw RFC822 message, with full message headers, body parts and
|
26
|
+
attachments, ususally composed via Tmail or Rmail.
|
27
|
+
|
28
|
+
Call the insert command
|
29
|
+
RubyQmail::Queue.insert "bounces-123@example.com", ['recipient@email.com',...], message_file
|
30
|
+
The call returns true if the message was inserted. If you want more control and information, you can run it as:
|
31
|
+
rqq = RubyQmail::Queue.new
|
32
|
+
sucess? = rqq.qmail_queue "bounces-123@example.com", ['recipient@email.com',...], message_file
|
33
|
+
puts rqq.response #=> Response message
|
34
|
+
|
35
|
+
You can also specify a set of options on the invocation line as :name=>value parameters after the message file.
|
36
|
+
RubyQmail::Queue.insert "bounces-123@example.com", recipient_file, message_file, :method=>:qmqp
|
37
|
+
Options can be
|
38
|
+
* :config_file - Location of the YAML file with these config settings for the application/system.
|
39
|
+
* :qmail_root - Location of the qmail install, usually /var/qmail
|
40
|
+
* :qmqp_port - Where any QMQP daemon process is listening, usually 628
|
41
|
+
* :logger - Object used to log any messages, such as RAILS_DEFAULT_LOGGER
|
42
|
+
* :qmail_queue - The name of the qmail-queue binary (qmail-queue or qmail-qmqpc or other).
|
43
|
+
* :qmqp_servers - The location of the time used to specify the IP address of a QMQPD process
|
44
|
+
* :ip - The IP address of a QMQP server
|
45
|
+
* :method - either :qmqp or :queue, used on the #insert method
|
46
|
+
* :noverp - Do not use VERP encoding on the return path
|
47
|
+
* :delimiter - Character to separate the email address and VERP encoding (Qmail prefers '-', others user '+')
|
48
|
+
|
49
|
+
== Feature Path
|
50
|
+
* Queue management, moving, reporting
|
51
|
+
* Bounce handling
|
52
|
+
|
53
|
+
== Note on Patches/Pull Requests
|
54
|
+
|
55
|
+
* Fork the project.
|
56
|
+
* Make your feature addition or bug fix.
|
57
|
+
* Add tests for it. This is important so I don't break it in a
|
58
|
+
future version unintentionally.
|
59
|
+
* Commit, do not mess with rakefile, version, or history.
|
60
|
+
(if you want to have your own version, that is fine but
|
61
|
+
bump version in a commit by itself I can ignore when I pull)
|
62
|
+
* Send me a pull request. Bonus points for topic branches.
|
63
|
+
|
64
|
+
== Copyright
|
65
|
+
|
66
|
+
Copyright (c) 2009 Allen Fair. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "ruby-qmail"
|
8
|
+
gem.summary = %Q{Ruby interfaces for Qmail}
|
9
|
+
gem.description = %Q{Provides methods to interact with Qmail to send/queue messages, manage bounces, and manage the queue.}
|
10
|
+
gem.email = "allen.fair@gmail.com"
|
11
|
+
gem.homepage = "http://github.com/afair/ruby-qmail"
|
12
|
+
gem.authors = ["Allen Fair"]
|
13
|
+
gem.add_development_dependency "rspec", ">= 1.2.9"
|
14
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
15
|
+
end
|
16
|
+
Jeweler::GemcutterTasks.new
|
17
|
+
rescue LoadError
|
18
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'spec/rake/spectask'
|
22
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
23
|
+
spec.libs << 'lib' << 'spec'
|
24
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
25
|
+
end
|
26
|
+
|
27
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
28
|
+
spec.libs << 'lib' << 'spec'
|
29
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
30
|
+
spec.rcov = true
|
31
|
+
end
|
32
|
+
|
33
|
+
task :spec => :check_dependencies
|
34
|
+
|
35
|
+
task :default => :spec
|
36
|
+
|
37
|
+
require 'rake/rdoctask'
|
38
|
+
Rake::RDocTask.new do |rdoc|
|
39
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
40
|
+
|
41
|
+
rdoc.rdoc_dir = 'rdoc'
|
42
|
+
rdoc.title = "ruby-qmail #{version}"
|
43
|
+
rdoc.rdoc_files.include('README*')
|
44
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
45
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/lib/bounce.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
module RubyQmail
|
2
|
+
# Bounce Handler for Qmail Bounce messages. There are typically two types of bounces
|
3
|
+
# * Remote Bounces - When email is accepted by the remote email server, then it cannot be delivered, it
|
4
|
+
# sends the message with its own error message to the Return-Path or sender address (not necessarily the
|
5
|
+
# same one in the From: header--although some non-RFC-compliant servers still will do this). For this
|
6
|
+
# processing, it is useful to enable VERP (RubyQmail does this). VERP (Variable Envelope Return Path) in
|
7
|
+
# Qmail appends the recipient's email address to each unique delivery, replacing the @ by the = symbol
|
8
|
+
# (returnpath-recipient=recipdomain@example.com).
|
9
|
+
# * Undeliverable Bounces - An email is sent back to the reuturn path with a formatted report off all undeliverable
|
10
|
+
# addresses (where the remote server could not be reached or accept delivery) and the error message
|
11
|
+
# generated by Qmail or returned by the Remote server during the SMTP session.
|
12
|
+
class BounceHandler
|
13
|
+
|
14
|
+
|
15
|
+
def initialize(bounce_io)
|
16
|
+
@bounce_io = bounce_io
|
17
|
+
end
|
18
|
+
|
19
|
+
# Parses a Qmail bounce message as IO object, then calls block with |address, message| for each bounced address
|
20
|
+
def parse(&block)
|
21
|
+
address=nil
|
22
|
+
mesage=nil
|
23
|
+
bounces=0
|
24
|
+
@bounce_io.each do |line|
|
25
|
+
break if line =~ /^--- Below this line is a copy of the message\./
|
26
|
+
if line =~ /^<(\S+)>:/
|
27
|
+
address = $1
|
28
|
+
message=''
|
29
|
+
bounces += 1
|
30
|
+
elsif bounces==0
|
31
|
+
next
|
32
|
+
#elsif line ~! /\S/ # empty
|
33
|
+
# yield address, message
|
34
|
+
elsif line =~ /Remote host said: (.+)/
|
35
|
+
yield address, $1
|
36
|
+
#message += line
|
37
|
+
end
|
38
|
+
end
|
39
|
+
bounces
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
data/lib/config.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
module RubyQmail
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
# Configuration for the Qmail system. Loads a configuration YAML file, and accepts a Hash of run-time overrides.
|
5
|
+
class Config
|
6
|
+
attr_reader :options
|
7
|
+
DEFAULTS = {
|
8
|
+
:qmqp_port => 628,
|
9
|
+
:qmail_root => '/var/qmail',
|
10
|
+
:delimiter => '-',
|
11
|
+
:logger => Logger.new("#{ENV['HOME']}/logs/ruby-qmail.log")
|
12
|
+
}
|
13
|
+
QMQP_SERVERS = '/control/qmqpservers'
|
14
|
+
QMAIL_QUEUE = '/bin/qmail-queue'
|
15
|
+
|
16
|
+
def self.load_file(config_file, options={})
|
17
|
+
@options = DEFAULTS.merge(options)
|
18
|
+
if config_file && File.exists?(config_file)
|
19
|
+
@options = YAML.load_file(config_file).merge(@options)
|
20
|
+
end
|
21
|
+
@options[:qmail_queue] ||= @options[:qmail_root] + QMAIL_QUEUE
|
22
|
+
@options[:qmqp_servers] ||= @options[:qmail_root] + QMQP_SERVERS
|
23
|
+
@options
|
24
|
+
end
|
25
|
+
|
26
|
+
def method_missing(method)
|
27
|
+
@options[method.to_sym]
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
data/lib/netstring.rb
ADDED
data/lib/queue.rb
ADDED
@@ -0,0 +1,248 @@
|
|
1
|
+
module RubyQmail
|
2
|
+
|
3
|
+
class Queue
|
4
|
+
include Process
|
5
|
+
attr_accessor( :return_path, :message, :recipients, :options, :response, :success )
|
6
|
+
QMAIL_QUEUE_SUCCESS = 0
|
7
|
+
QMAIL_ERRORS = {
|
8
|
+
-1 => "Unknown Error",
|
9
|
+
0 => "Success",
|
10
|
+
11 => "Address too long",
|
11
|
+
31 => "Mail server permanently refuses to send the message to any recipients.",
|
12
|
+
51 => "Out of memory.",
|
13
|
+
52 => "Timeout.",
|
14
|
+
53 => "Write error; e.g., disk full.",
|
15
|
+
54 => "Unable to read the message or envelope.",
|
16
|
+
55 => "Unable to read a configuration file.",
|
17
|
+
56 => "Problem making a network connection from this host.",
|
18
|
+
61 => "Problem with the qmail home directory.",
|
19
|
+
62 => "Problem with the queue directory.",
|
20
|
+
63 => "Problem with queue/pid.",
|
21
|
+
64 => "Problem with queue/mess.",
|
22
|
+
65 => "Problem with queue/intd.",
|
23
|
+
66 => "Problem with queue/todo.",
|
24
|
+
71 => "Mail server temporarily refuses to send the message to any recipients.",
|
25
|
+
72 => "Connection to mail server timed out.",
|
26
|
+
73 => "Connection to mail server rejected. ",
|
27
|
+
74 => "Connection to mail server succeeded, but communication failed.",
|
28
|
+
81 => "Internal bug; e.g., segmentation fault.",
|
29
|
+
91 => "Envelope format error"
|
30
|
+
}
|
31
|
+
|
32
|
+
# Class Method to place the message into the Qmail queue.
|
33
|
+
def self.insert(return_path, recipients, message, *options)
|
34
|
+
q = Queue.new(return_path, recipients, message, *options)
|
35
|
+
if q.options.has_key?[:ip] || q.options[:method]==:qmqp
|
36
|
+
q.qmqp
|
37
|
+
else
|
38
|
+
q.qmail_queue
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Recipients can be a filename, array or other object that responds to :each, or #to_s resolves to an email address
|
43
|
+
# Message can be a filename, string, or other object that responds to :each
|
44
|
+
def initialize(return_path=nil, recipients=nil, message=nil, *options)
|
45
|
+
parameters(return_path, recipients, message, options)
|
46
|
+
end
|
47
|
+
|
48
|
+
def parameters(return_path, recipients, message, options) #:nodoc:
|
49
|
+
@return_path = return_path if return_path
|
50
|
+
@options = RubyQmail::Config.load_file( nil, options.last || {})
|
51
|
+
@recipients = recipients if recipients
|
52
|
+
@recipients = File.new(@recipients) if @recipients.is_a?(String) && File.exists?(@recipients)
|
53
|
+
@recipients = [ @recipients.to_s ] unless @recipients.respond_to?(:each)
|
54
|
+
@message = message if message
|
55
|
+
@message = File.new(@message) if @message.is_a?(String) && File.exists?(@message)
|
56
|
+
@message = @message.split(/\n/) if @message.is_a?(String)
|
57
|
+
|
58
|
+
# Edits the return path for VERP. bounces@example.com => bounces-@example.com-@[]
|
59
|
+
if return_path && !@options.has_key?(:noverp)
|
60
|
+
rp1, rp2 = return_path.split(/@/)
|
61
|
+
@return_path = "#{rp1}#{@options[:delimiter]}@#{rp2}" if (rp1.match(/(.)$/)[1] != @options[:delimiter])
|
62
|
+
@return_path += '-@[]' unless @return_path =~ /-@\[\]$/
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# This calls the Qmail-Queue program, so requires qmail to be installed (does not require it to be currently running).
|
67
|
+
def queue(return_path=nil, recipients=nil, message=nil, *options)
|
68
|
+
parameters(return_path, recipients, message, options)
|
69
|
+
@success = run_qmail_queue() do |msg, env|
|
70
|
+
# Send the Message
|
71
|
+
@message.each { |m| msg.puts(m) }
|
72
|
+
msg.close
|
73
|
+
|
74
|
+
env.write('F' + @return_path + "\0")
|
75
|
+
@recipients.each { |r| env.write('T' + r + "\0") }
|
76
|
+
env.write("\0") # End of "file"
|
77
|
+
end
|
78
|
+
@options[:logger].info("RubyQmail Queue exited:#{@success} #{Queue.qmail_queue_error_message(@success)}")
|
79
|
+
return true if @success == QMAIL_QUEUE_SUCCESS
|
80
|
+
raise Queue.qmail_queue_error_message(@success)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Maps the qmail-queue exit code to the error message
|
84
|
+
def self.qmail_queue_error_message(code) #:nodoc:
|
85
|
+
"RubyQmail::Queue Error #{code}:" + QMAIL_ERRORS.has_key?(code) ? QMAIL_ERRORS[code]:QMAIL_ERRORS[-1]
|
86
|
+
end
|
87
|
+
|
88
|
+
# Builds the QMQP request, and opens a connection to the QMQP Server and sends
|
89
|
+
# This implemtents the QMQP protocol, so does not need Qmail installed on the host system.
|
90
|
+
# System defaults will be used if no ip or port given.
|
91
|
+
# Returns true on success, false on failure (see @response), or nul on deferral
|
92
|
+
def qmqp(return_path=nil, recipients=nil, message=nil, *options)
|
93
|
+
parameters(return_path, recipients, message, options)
|
94
|
+
|
95
|
+
begin
|
96
|
+
ip = @options[:ip] || File.readlines(QMQP_SERVERS).first.chomp
|
97
|
+
#puts "CONNECT #{:ip}, #{@options[:qmqp_port]}"
|
98
|
+
socket = TCPSocket.new(ip, @options[:qmqp_port])
|
99
|
+
raise "QMQP can not connect to #{ip}:#{@options[:qmqp_port]}" unless socket
|
100
|
+
|
101
|
+
# Build netstring of messagebody+returnpath+recipient...
|
102
|
+
nstr = (@message.map.join("\n")+"\n").to_netstring # { |m| m }.join("\t").to_netstring
|
103
|
+
nstr += @return_path.to_netstring
|
104
|
+
nstr += @recipients.map { |r| r.to_netstring }.join
|
105
|
+
socket.send( nstr.to_netstring, 0 )
|
106
|
+
|
107
|
+
@response = socket.recv(1000) # "23:Kok 1182362995 qp 21894," (its a netstring)
|
108
|
+
@success = case @response.match(/^\d+:([KZD])(.+),$/)[1]
|
109
|
+
when 'K' : true # success
|
110
|
+
when 'Z' : nil # deferral
|
111
|
+
when 'D' : false # failure
|
112
|
+
else false
|
113
|
+
end
|
114
|
+
logmsg = "RubyQmail QMQP [#{ip}:#{@options[:qmqp_port]}]: #{@response} return:#{@success}"
|
115
|
+
@options[:logger].info(logmsg)
|
116
|
+
puts logmsg
|
117
|
+
@success
|
118
|
+
rescue Exception => e
|
119
|
+
@options[:logger].error( "QMQP can not connect to #{@opt[:qmqp_ip]}:#{@options[:qmqp_port]} #{e}" )
|
120
|
+
raise e
|
121
|
+
ensure
|
122
|
+
socket.close if socket
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# # Like #qmail_queue, but writes directly to the queue, not via the qmail-queue program
|
127
|
+
# # Is this a good idea? It expects a unique PID per message.
|
128
|
+
# def qmail_queue_direct(return_path=nil, recipients=nil, message=nil, *options)
|
129
|
+
# parameters(return_path, recipients, message, options)
|
130
|
+
# end
|
131
|
+
|
132
|
+
# def sendmail(return_path=nil, recipients=nil, message=nil, *options)
|
133
|
+
# parameters(return_path, recipients, message, options)
|
134
|
+
# end
|
135
|
+
|
136
|
+
# Sends email directly via qmail-remote. It does not store in the queue, It will halt the process
|
137
|
+
# and wait for the network event to complete. If multiple recipients are passed, it will run
|
138
|
+
# qmail-remote delivery for each at a time to honor VERP return paths.
|
139
|
+
def qmail_remote(return_path=nil, recipients=nil, message=nil, *options)
|
140
|
+
parameters(return_path, recipients, message, options)
|
141
|
+
rp1, rp2 = @return_path.split(/@/,2)
|
142
|
+
rp = @return_path
|
143
|
+
@recipients.each do |recip|
|
144
|
+
unless @options[:noverp]
|
145
|
+
mailbox, host = recip.split(/@/)
|
146
|
+
rp = "#{rp1}#{mailbox}=#{host}@#{rp2}"
|
147
|
+
end
|
148
|
+
|
149
|
+
@message.rewind if @message.respond_to?(:rewind)
|
150
|
+
cmd = "#{@options[:qmail_root]}+/bin/qmail-remote #{host} #{rp} #{recip}"
|
151
|
+
@success = self.spawn_command(cmd) do |send, recv|
|
152
|
+
@message.each { |m| send.puts m }
|
153
|
+
send.close
|
154
|
+
@response = recv.readpartial(1000)
|
155
|
+
end
|
156
|
+
|
157
|
+
@options[:logger].info("RubyQmail Remote #{recip} exited:#{@success} responded:#{@response}")
|
158
|
+
end
|
159
|
+
return [ @success, @response ] # Last one
|
160
|
+
end
|
161
|
+
|
162
|
+
# Forks, sets up stdin and stdout pipes, and starts the command.
|
163
|
+
# IF a block is passed, yeilds to it with [sendpipe, receivepipe],
|
164
|
+
# returing the exit code, otherwise returns {:send=>, :recieve=>, :pid=>}
|
165
|
+
# qmail-queue does not work with this as it reads from both pipes.
|
166
|
+
def spawn_command(command, &block)
|
167
|
+
child_read, parent_write = IO.pipe # From parent to child(stdin)
|
168
|
+
parent_read, child_write = IO.pipe # From child(stdout) to parent
|
169
|
+
@child = fork
|
170
|
+
|
171
|
+
# Child process
|
172
|
+
unless @child #
|
173
|
+
$stdin.close # closes FD==0
|
174
|
+
child_read.dup # copies to FD==0
|
175
|
+
child_read.close
|
176
|
+
|
177
|
+
$stdout.close # closes FD==1
|
178
|
+
child_write.dup # copies to FD==1
|
179
|
+
child_write.close
|
180
|
+
|
181
|
+
Dir.chdir(@options[:qmail_root]) unless @options[:nochdir]
|
182
|
+
exec(command)
|
183
|
+
raise "Exec spawn_command #{command} failed"
|
184
|
+
end
|
185
|
+
|
186
|
+
# Parent Process with block
|
187
|
+
if block_given?
|
188
|
+
yield(parent_write, parent_read)
|
189
|
+
parent_write.close
|
190
|
+
parent_read.close
|
191
|
+
wait(@child)
|
192
|
+
@success = $? >> 8
|
193
|
+
return @sucess
|
194
|
+
end
|
195
|
+
|
196
|
+
# Parent process, no block
|
197
|
+
{:send=>parent_write, :receive=>parent_read, :pid=>@child}
|
198
|
+
end
|
199
|
+
|
200
|
+
# Forks, sets up stdin and stdout pipes, and starts qmail-queue.
|
201
|
+
# IF a block is passed, yields to it with [sendpipe, receivepipe],
|
202
|
+
# and returns the exist cod, otherwise returns {:msg=>pipe, :env=>pipe, :pid=>@child}
|
203
|
+
# It exits 0 on success or another code on failure.
|
204
|
+
# Qmail-queue Protocol: Reads mail message from File Descriptor 0, then reads Envelope from FD 1
|
205
|
+
# Envelope Stream: 'F' + sender_email + "\0" + ("T" + recipient_email + "\0") ... + "\0"
|
206
|
+
def run_qmail_queue(command=nil, &block)
|
207
|
+
# Set up pipes and qmail-queue child process
|
208
|
+
msg_read, msg_write = IO.pipe
|
209
|
+
env_read, env_write = IO.pipe
|
210
|
+
@child=fork # child? nil : childs_process_id
|
211
|
+
|
212
|
+
unless @child
|
213
|
+
## Set child's stdin(0) to read from msg
|
214
|
+
$stdin.close # FD=0
|
215
|
+
msg_read.dup
|
216
|
+
msg_read.close
|
217
|
+
msg_write.close
|
218
|
+
|
219
|
+
## Set child's stdout(1) to read from env
|
220
|
+
$stdout.close # FD=1
|
221
|
+
env_read.dup
|
222
|
+
env_read.close
|
223
|
+
env_write.close
|
224
|
+
|
225
|
+
# Change directory and load command
|
226
|
+
Dir.chdir(@options[:qmail_root])
|
227
|
+
exec( command || @options[:qmail_queue] )
|
228
|
+
raise "Exec qmail-queue failed"
|
229
|
+
end
|
230
|
+
|
231
|
+
# Parent Process with block
|
232
|
+
if block_given?
|
233
|
+
yield(msg_write, env_write)
|
234
|
+
# msg_write.close
|
235
|
+
env_write.close
|
236
|
+
wait(@child)
|
237
|
+
@success = $? >> 8
|
238
|
+
# puts "#{$$} parent waited for #{@child} s=#{@success} #{$?.inspect}"
|
239
|
+
return @sucess
|
240
|
+
end
|
241
|
+
|
242
|
+
# Parent process, no block
|
243
|
+
{:msg=>msg_write, :env=>env_write, :pid=>@child}
|
244
|
+
end
|
245
|
+
|
246
|
+
end
|
247
|
+
|
248
|
+
end
|
data/lib/ruby-qmail.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
require 'rubygems'
|
3
|
+
require 'ruby-debug'
|
4
|
+
|
5
|
+
describe "RubyQmail" do
|
6
|
+
|
7
|
+
before(:all) do
|
8
|
+
@rqq = RubyQmail::Queue.new()
|
9
|
+
@rpath = 'allen-whitelist@biglist.com'
|
10
|
+
@recip = %w( allen-1@biglist.com allen-2@biglist.com )
|
11
|
+
@msg = %q(To: allen-admin@biglist.com
|
12
|
+
From: allen-listmaster@biglist.com
|
13
|
+
Subject: ruby-qmail
|
14
|
+
|
15
|
+
testing 1 2 3.
|
16
|
+
later!
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should add a #to_netstring method to the string class" do
|
21
|
+
"qmail".to_netstring.should == "5:qmail,"
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should submit a message by qmail-queue" do
|
25
|
+
@rqq.queue( @rpath, @recip, @msg ).should_equal true
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should submit a message by QMQP" do
|
29
|
+
@rqq.qmqp( @rpath, @recip, @msg, :ip=>'173.161.130.227', :qmqp_port=>631 ).should_equal true
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
end
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ruby-qmail
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Allen Fair
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-10-28 00:00:00 -04:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rspec
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.2.9
|
24
|
+
version:
|
25
|
+
description: Provides methods to interact with Qmail to send/queue messages, manage bounces, and manage the queue.
|
26
|
+
email: allen.fair@gmail.com
|
27
|
+
executables: []
|
28
|
+
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files:
|
32
|
+
- LICENSE
|
33
|
+
- README.rdoc
|
34
|
+
files:
|
35
|
+
- .document
|
36
|
+
- .gitignore
|
37
|
+
- LICENSE
|
38
|
+
- README.rdoc
|
39
|
+
- Rakefile
|
40
|
+
- VERSION
|
41
|
+
- lib/bounce.rb
|
42
|
+
- lib/config.rb
|
43
|
+
- lib/netstring.rb
|
44
|
+
- lib/queue.rb
|
45
|
+
- lib/ruby-qmail.rb
|
46
|
+
- spec/ruby-qmail_spec.rb
|
47
|
+
- spec/spec.opts
|
48
|
+
- spec/spec_helper.rb
|
49
|
+
has_rdoc: true
|
50
|
+
homepage: http://github.com/afair/ruby-qmail
|
51
|
+
licenses: []
|
52
|
+
|
53
|
+
post_install_message:
|
54
|
+
rdoc_options:
|
55
|
+
- --charset=UTF-8
|
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:
|
73
|
+
rubygems_version: 1.3.5
|
74
|
+
signing_key:
|
75
|
+
specification_version: 3
|
76
|
+
summary: Ruby interfaces for Qmail
|
77
|
+
test_files:
|
78
|
+
- spec/ruby-qmail_spec.rb
|
79
|
+
- spec/spec_helper.rb
|