ruby-qmail 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/.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
|