rq-ruby1.8 3.4.3
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 +22 -0
- data/Gemfile.lock +22 -0
- data/INSTALL +166 -0
- data/LICENSE +10 -0
- data/Makefile +6 -0
- data/README +1183 -0
- data/Rakefile +37 -0
- data/TODO +24 -0
- data/TUTORIAL +230 -0
- data/VERSION +1 -0
- data/bin/rq +902 -0
- data/bin/rqmailer +865 -0
- data/example/a.rb +7 -0
- data/extconf.rb +198 -0
- data/gemspec.rb +40 -0
- data/install.rb +210 -0
- data/lib/rq.rb +155 -0
- data/lib/rq/arrayfields.rb +371 -0
- data/lib/rq/backer.rb +31 -0
- data/lib/rq/configfile.rb +82 -0
- data/lib/rq/configurator.rb +40 -0
- data/lib/rq/creator.rb +54 -0
- data/lib/rq/cron.rb +144 -0
- data/lib/rq/defaultconfig.txt +5 -0
- data/lib/rq/deleter.rb +51 -0
- data/lib/rq/executor.rb +40 -0
- data/lib/rq/feeder.rb +527 -0
- data/lib/rq/ioviewer.rb +48 -0
- data/lib/rq/job.rb +51 -0
- data/lib/rq/jobqueue.rb +947 -0
- data/lib/rq/jobrunner.rb +110 -0
- data/lib/rq/jobrunnerdaemon.rb +193 -0
- data/lib/rq/lister.rb +47 -0
- data/lib/rq/locker.rb +43 -0
- data/lib/rq/lockfile.rb +564 -0
- data/lib/rq/logging.rb +124 -0
- data/lib/rq/mainhelper.rb +189 -0
- data/lib/rq/orderedautohash.rb +39 -0
- data/lib/rq/orderedhash.rb +240 -0
- data/lib/rq/qdb.rb +733 -0
- data/lib/rq/querier.rb +98 -0
- data/lib/rq/rails.rb +80 -0
- data/lib/rq/recoverer.rb +28 -0
- data/lib/rq/refresher.rb +80 -0
- data/lib/rq/relayer.rb +283 -0
- data/lib/rq/resource.rb +22 -0
- data/lib/rq/resourcemanager.rb +40 -0
- data/lib/rq/resubmitter.rb +100 -0
- data/lib/rq/rotater.rb +98 -0
- data/lib/rq/sleepcycle.rb +46 -0
- data/lib/rq/snapshotter.rb +40 -0
- data/lib/rq/sqlite.rb +286 -0
- data/lib/rq/statuslister.rb +48 -0
- data/lib/rq/submitter.rb +113 -0
- data/lib/rq/toucher.rb +182 -0
- data/lib/rq/updater.rb +94 -0
- data/lib/rq/usage.rb +1222 -0
- data/lib/rq/util.rb +304 -0
- data/rdoc.sh +17 -0
- data/rq-ruby1.8.gemspec +120 -0
- data/test/.gitignore +1 -0
- data/test/test_rq.rb +145 -0
- data/white_box/crontab +2 -0
- data/white_box/joblist +8 -0
- data/white_box/killrq +18 -0
- data/white_box/rq_killer +27 -0
- metadata +208 -0
data/bin/rqmailer
ADDED
@@ -0,0 +1,865 @@
|
|
1
|
+
#! /opt/local/bin/ruby
|
2
|
+
|
3
|
+
### built-in
|
4
|
+
require 'openssl'
|
5
|
+
require 'net/smtp'
|
6
|
+
require 'io/wait'
|
7
|
+
require 'erb'
|
8
|
+
require 'socket'
|
9
|
+
|
10
|
+
### rubyforge
|
11
|
+
require 'rubygems'
|
12
|
+
require 'main'
|
13
|
+
require 'open4'
|
14
|
+
#require 'mailfactory' # patched version inlined below
|
15
|
+
|
16
|
+
Main {
|
17
|
+
option('config', 'c'){
|
18
|
+
argument_required
|
19
|
+
}
|
20
|
+
|
21
|
+
option('template', 't'){
|
22
|
+
argument_required
|
23
|
+
}
|
24
|
+
|
25
|
+
option('belch', 'b'){
|
26
|
+
}
|
27
|
+
|
28
|
+
option('send', 's'){
|
29
|
+
}
|
30
|
+
|
31
|
+
option('queue', 'q'){
|
32
|
+
argument_required
|
33
|
+
}
|
34
|
+
|
35
|
+
def run #--{{{
|
36
|
+
indicating_that_rqmailer_failed do
|
37
|
+
setup
|
38
|
+
load_config
|
39
|
+
load_template
|
40
|
+
generate_cmd
|
41
|
+
run_cmd
|
42
|
+
expand_template
|
43
|
+
mail_message
|
44
|
+
end
|
45
|
+
exit @exit_status || 42
|
46
|
+
end #--}}}
|
47
|
+
|
48
|
+
def setup #--{{{
|
49
|
+
@config = {}
|
50
|
+
@rq = ENV['RQ']
|
51
|
+
@rq_data = ENV['RQ_DATA']
|
52
|
+
if(@rq.nil? and @rq_data.nil? and argv.first and test(?d, argv.first))
|
53
|
+
@rq_data = File.expand_path argv.shift
|
54
|
+
@rq = true
|
55
|
+
end
|
56
|
+
@exit_status = 0
|
57
|
+
@exception = nil
|
58
|
+
@template_expander = TemplateExpander.instance
|
59
|
+
@stdin = @stdout = @stderr = nil
|
60
|
+
@belch = params['belch'].given?
|
61
|
+
@send = params['send'].given?
|
62
|
+
@hostname = Socket.gethostname rescue 'localhost.localdomain'
|
63
|
+
@mail = MailFactory.new
|
64
|
+
end #--}}}
|
65
|
+
|
66
|
+
def load_config #--{{{
|
67
|
+
if param['config'].given?
|
68
|
+
@config = YAML.load IO.read(param['config'].value)
|
69
|
+
else
|
70
|
+
if @rq_data
|
71
|
+
config = File.join @rq_data, 'config'
|
72
|
+
@config = YAML.load IO.read(config)
|
73
|
+
else
|
74
|
+
abort 'no config'
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end #--}}}
|
78
|
+
|
79
|
+
def load_template #--{{{
|
80
|
+
if param['template'].given?
|
81
|
+
@template = IO.read param['template'].value
|
82
|
+
else
|
83
|
+
if @rq_data
|
84
|
+
template = File.join @rq_data, 'template'
|
85
|
+
@template = IO.read(template)
|
86
|
+
else
|
87
|
+
@template = DATA.read
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end #--}}}
|
91
|
+
|
92
|
+
def generate_cmd #--{{{
|
93
|
+
stdin = argv.first == '-' && argv.delete('-')
|
94
|
+
@cmd =
|
95
|
+
if stdin
|
96
|
+
STDIN.read
|
97
|
+
else
|
98
|
+
@config['command'] || @config['cmd'] || argv.join(' ')
|
99
|
+
end
|
100
|
+
abort 'no cmd' if @cmd.to_s.strip.empty?
|
101
|
+
end #--}}}
|
102
|
+
|
103
|
+
def run_cmd #--{{{
|
104
|
+
if @rq
|
105
|
+
@stdin = STDIN if STDIN.ready?
|
106
|
+
@stdout = STDOUT
|
107
|
+
@stderr = STDERR
|
108
|
+
STDOUT.flush; STDERR.flush
|
109
|
+
@exit_status = Open4.spawn @cmd, 0=>@stdin, 1=>@stdout, 2=>@stderr
|
110
|
+
STDOUT.flush; STDERR.flush
|
111
|
+
else
|
112
|
+
@stdin = STDIN if STDIN.ready?
|
113
|
+
@stdin = IO.read(@stdin) if @stdin
|
114
|
+
@stdout = ''
|
115
|
+
@stderr = ''
|
116
|
+
@exit_status = Open4.spawn @cmd, 0=>@stdin, 1=>@stdout, 2=>@stderr
|
117
|
+
end
|
118
|
+
update_config_with_cmd_info
|
119
|
+
end #--}}}
|
120
|
+
|
121
|
+
def errmsg e #--{{{
|
122
|
+
m, c, bt = e.message, e.class, (e.backtrace || []).join("\n")
|
123
|
+
"#{ m } (#{ c })\n#{ bt }"
|
124
|
+
end #--}}}
|
125
|
+
|
126
|
+
def indicating_that_rqmailer_failed #--{{{
|
127
|
+
begin
|
128
|
+
yield
|
129
|
+
rescue Exception => e
|
130
|
+
@exception = e
|
131
|
+
STDERR.puts errmsg(e)
|
132
|
+
@exit_status = 42
|
133
|
+
end
|
134
|
+
end #--}}}
|
135
|
+
|
136
|
+
def update_config_with_cmd_info #--{{{
|
137
|
+
if @rq
|
138
|
+
%w( stdin stdout stderr ).each do |iof|
|
139
|
+
path = instance_variable_get "@rq_#{ iof }"
|
140
|
+
buf = IO.read path rescue ''
|
141
|
+
@config[iof] = buf
|
142
|
+
@config["#{ iof }_path"] = path
|
143
|
+
end
|
144
|
+
else
|
145
|
+
@config['stdin'] = @stdin
|
146
|
+
@config['stdin_path'] = '-'
|
147
|
+
@config['stdout'] = @stdout
|
148
|
+
@config['stdout_path'] = '-'
|
149
|
+
@config['stderr'] = @stderr
|
150
|
+
@config['stderr_path'] = '-'
|
151
|
+
end
|
152
|
+
|
153
|
+
@config['cmd'] = @config['command'] = @cmd
|
154
|
+
@config['exit_status'] = @config['exitstatus'] = @exit_status
|
155
|
+
end #--}}}
|
156
|
+
|
157
|
+
def expand_template #--{{{
|
158
|
+
@template_expander.configure @config
|
159
|
+
@template_expander.configure 'mail' => @mail
|
160
|
+
mconfig = @config['mail'] || {}
|
161
|
+
@template_expander.configure mconfig
|
162
|
+
@message = @template_expander.expand @template
|
163
|
+
end #--}}}
|
164
|
+
|
165
|
+
def mail_message #--{{{
|
166
|
+
build_mail!
|
167
|
+
|
168
|
+
deliver = @config.has_key?('smtp') ? 'smtp' : 'sendmail'
|
169
|
+
|
170
|
+
if deliver == 'smtp'
|
171
|
+
hash = @config['smtp'] || {}
|
172
|
+
server = hash['server']
|
173
|
+
port = Integer hash['port']
|
174
|
+
hostname = hash['hostname'] || @hostname || 'localhost.localdomain'
|
175
|
+
account = hash['account']
|
176
|
+
password = hash['password']
|
177
|
+
authtype = hash['authtype'] || :plain
|
178
|
+
tls = hash['tls'] ? true : false
|
179
|
+
else
|
180
|
+
sendmail = @config['sendmail'] || '/usr/sbin/sendmail -i -t'
|
181
|
+
end
|
182
|
+
|
183
|
+
if @belch
|
184
|
+
if @send
|
185
|
+
case deliver
|
186
|
+
when 'smtp'
|
187
|
+
smtp = Net::SMTP.new server, port
|
188
|
+
smtp.set_debug_output STDERR
|
189
|
+
smtp.start hostname, account, password, authtype, tls do |connection|
|
190
|
+
connection.send_message @mail, account, @mail.recipients
|
191
|
+
end
|
192
|
+
when 'sendmail'
|
193
|
+
IO.popen(sendmail, 'w+') do |pipe|
|
194
|
+
pipe.write @mail.to_s.gsub(%r/\r/,'')
|
195
|
+
pipe.flush
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
STDOUT.puts @mail
|
200
|
+
else
|
201
|
+
case deliver
|
202
|
+
when 'smtp'
|
203
|
+
debug_output = open File.join(@rq_data, 'smtp'), 'w'
|
204
|
+
at_exit{ debug_output.close rescue nil }
|
205
|
+
smtp = Net::SMTP.new server, port
|
206
|
+
smtp.set_debug_output debug_output
|
207
|
+
smtp.start hostname, account, password, authtype, tls do |connection|
|
208
|
+
connection.send_message @mail, account, @mail.recipients
|
209
|
+
end
|
210
|
+
when 'sendmail'
|
211
|
+
IO.popen(sendmail, 'w+') do |pipe|
|
212
|
+
pipe.write @mail.to_s.gsub(%r/\r/,'')
|
213
|
+
pipe.flush
|
214
|
+
end
|
215
|
+
end
|
216
|
+
open(File.join(@rq_data, 'mail'), 'w'){|fd| fd.write @mail} rescue nil
|
217
|
+
STDERR.puts @mail
|
218
|
+
end
|
219
|
+
|
220
|
+
end #--}}}
|
221
|
+
|
222
|
+
def build_mail! #--{{{
|
223
|
+
mconfig = @config['mail'] || @config
|
224
|
+
|
225
|
+
attributes = %w(
|
226
|
+
to
|
227
|
+
from
|
228
|
+
cc
|
229
|
+
bcc
|
230
|
+
subject
|
231
|
+
)
|
232
|
+
attributes.each do |attribute|
|
233
|
+
up, down = attribute.downcase.capitalize, attribute.downcase
|
234
|
+
[up, down].each do |attribute|
|
235
|
+
if mconfig.has_key?(attribute)
|
236
|
+
value = [mconfig[attribute]].flatten.join ','
|
237
|
+
@mail.send "#{ up }=", value
|
238
|
+
@mail.send "#{ down }=", value
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
methods = %w(
|
244
|
+
attach
|
245
|
+
attachment
|
246
|
+
attachments
|
247
|
+
Attach
|
248
|
+
Attachment
|
249
|
+
Attachments
|
250
|
+
)
|
251
|
+
methods.each do |method|
|
252
|
+
if mconfig.has_key?(method)
|
253
|
+
values = [mconfig[method]].flatten
|
254
|
+
values.each do |value|
|
255
|
+
@mail.attach value
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
class << @mail
|
261
|
+
def recipients
|
262
|
+
to.to_s.split %r/,/
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
@mail.subject ||= "rqmailer"
|
267
|
+
@mail.text = @message
|
268
|
+
@mail
|
269
|
+
end #--}}}
|
270
|
+
|
271
|
+
class TemplateExpander #--{{{
|
272
|
+
alias_method "__instance_variable_set__", "instance_variable_set"
|
273
|
+
alias_method "__instance_eval__", "instance_eval"
|
274
|
+
instance_methods.each{|m| undef_method unless m[%r/__/]}
|
275
|
+
alias_method "instance_eval", "__instance_eval__"
|
276
|
+
alias_method "instance_variable_set", "__instance_variable_set__"
|
277
|
+
|
278
|
+
def self.new *a, &b
|
279
|
+
@instance ||= super
|
280
|
+
end
|
281
|
+
def self.instance *a, &b
|
282
|
+
new *a, &b
|
283
|
+
end
|
284
|
+
def self.const_missing c
|
285
|
+
msg = c.to_s.downcase
|
286
|
+
if @instance.respond_to? msg
|
287
|
+
@instance.send msg
|
288
|
+
else
|
289
|
+
super
|
290
|
+
end
|
291
|
+
end
|
292
|
+
def singleton_class &b #--{{{
|
293
|
+
@sc ||=
|
294
|
+
class << self
|
295
|
+
self
|
296
|
+
end
|
297
|
+
b ? @sc.module_eval(&b) : @sc
|
298
|
+
end #--}}}
|
299
|
+
def set key, value #--{{{
|
300
|
+
key = key.to_s.downcase
|
301
|
+
instance_variable_set "@#{ key }", value
|
302
|
+
singleton_class do
|
303
|
+
define_method(key){ instance_variable_get "@#{ key }" }
|
304
|
+
define_method(key.capitalize){ instance_variable_get "@#{ key }" }
|
305
|
+
end
|
306
|
+
end #--}}}
|
307
|
+
def configure hash #--{{{
|
308
|
+
hash.each{|k,v| set k, v}
|
309
|
+
end #--}}}
|
310
|
+
def expand message #--{{{
|
311
|
+
ERB.new(message).result binding
|
312
|
+
end #--}}}
|
313
|
+
def lazy #--{{{
|
314
|
+
singleton_class{ define_method m, &b }
|
315
|
+
end #--}}}
|
316
|
+
def method_missing m, *a, &b
|
317
|
+
super unless defined? @mail
|
318
|
+
@mail.send m, *a, &b
|
319
|
+
end
|
320
|
+
end #--}}}
|
321
|
+
}
|
322
|
+
|
323
|
+
|
324
|
+
BEGIN {
|
325
|
+
|
326
|
+
### hacking in smtp tls support
|
327
|
+
#--{{{
|
328
|
+
# Original code believed public domain from ruby-talk or ruby-core email.
|
329
|
+
# Modifications by Kyle Maxwell <kyle@kylemaxwell.com> used under MIT license.
|
330
|
+
|
331
|
+
require "openssl"
|
332
|
+
require "net/smtp"
|
333
|
+
|
334
|
+
# :stopdoc:
|
335
|
+
|
336
|
+
class Net::SMTP
|
337
|
+
|
338
|
+
class << self
|
339
|
+
send :remove_method, :start
|
340
|
+
end
|
341
|
+
|
342
|
+
def self.start( address, port = nil,
|
343
|
+
helo = 'localhost.localdomain',
|
344
|
+
user = nil, secret = nil, authtype = nil, use_tls = false,
|
345
|
+
&block) # :yield: smtp
|
346
|
+
new(address, port).start(helo, user, secret, authtype, use_tls, &block)
|
347
|
+
end
|
348
|
+
|
349
|
+
alias tls_old_start start
|
350
|
+
|
351
|
+
def start( helo = 'localhost.localdomain',
|
352
|
+
user = nil, secret = nil, authtype = nil, use_tls = false ) # :yield: smtp
|
353
|
+
start_method = use_tls ? :do_tls_start : :do_start
|
354
|
+
if block_given?
|
355
|
+
begin
|
356
|
+
send start_method, helo, user, secret, authtype
|
357
|
+
return yield(self)
|
358
|
+
ensure
|
359
|
+
do_finish
|
360
|
+
end
|
361
|
+
else
|
362
|
+
send start_method, helo, user, secret, authtype
|
363
|
+
return self
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
367
|
+
private
|
368
|
+
|
369
|
+
def do_tls_start(helodomain, user, secret, authtype)
|
370
|
+
raise IOError, 'SMTP session already started' if @started
|
371
|
+
check_auth_args user, secret, authtype if user or secret
|
372
|
+
|
373
|
+
sock = timeout(@open_timeout) { TCPSocket.open(@address, @port) }
|
374
|
+
@socket = Net::InternetMessageIO.new(sock)
|
375
|
+
@socket.read_timeout = 60 #@read_timeout
|
376
|
+
@socket.debug_output = @debug_output
|
377
|
+
|
378
|
+
check_response(critical { recv_response() })
|
379
|
+
do_helo(helodomain)
|
380
|
+
|
381
|
+
raise 'openssl library not installed' unless defined?(OpenSSL)
|
382
|
+
starttls
|
383
|
+
ssl = OpenSSL::SSL::SSLSocket.new(sock)
|
384
|
+
ssl.sync_close = true
|
385
|
+
ssl.connect
|
386
|
+
@socket = Net::InternetMessageIO.new(ssl)
|
387
|
+
@socket.read_timeout = 60 #@read_timeout
|
388
|
+
@socket.debug_output = @debug_output
|
389
|
+
do_helo(helodomain)
|
390
|
+
|
391
|
+
authenticate user, secret, authtype if user
|
392
|
+
@started = true
|
393
|
+
ensure
|
394
|
+
unless @started
|
395
|
+
# authentication failed, cancel connection.
|
396
|
+
@socket.close if not @started and @socket and not @socket.closed?
|
397
|
+
@socket = nil
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
def do_helo(helodomain)
|
402
|
+
begin
|
403
|
+
if @esmtp
|
404
|
+
ehlo helodomain
|
405
|
+
else
|
406
|
+
helo helodomain
|
407
|
+
end
|
408
|
+
rescue Net::ProtocolError
|
409
|
+
if @esmtp
|
410
|
+
@esmtp = false
|
411
|
+
@error_occured = false
|
412
|
+
retry
|
413
|
+
end
|
414
|
+
raise
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
def starttls
|
419
|
+
getok('STARTTLS')
|
420
|
+
end
|
421
|
+
|
422
|
+
alias tls_old_quit quit
|
423
|
+
|
424
|
+
def quit
|
425
|
+
begin
|
426
|
+
getok('QUIT')
|
427
|
+
rescue EOFError
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
end unless Net::SMTP.private_method_defined? :do_tls_start or
|
432
|
+
Net::SMTP.method_defined? :tls?
|
433
|
+
#--}}}
|
434
|
+
|
435
|
+
### inlining mailfactory
|
436
|
+
#--{{{
|
437
|
+
# = Overview:
|
438
|
+
# A simple to use module for generating RFC compliant MIME mail
|
439
|
+
# ---
|
440
|
+
# = License:
|
441
|
+
# Author:: David Powers
|
442
|
+
# Copyright:: May, 2005
|
443
|
+
# License:: Ruby License
|
444
|
+
# ---
|
445
|
+
# = Usage:
|
446
|
+
# require 'net/smtp'
|
447
|
+
# require 'rubygems'
|
448
|
+
# require 'mailfactory'
|
449
|
+
#
|
450
|
+
#
|
451
|
+
# mail = MailFactory.new()
|
452
|
+
# mail.to = "test@test.com"
|
453
|
+
# mail.from = "sender@sender.com"
|
454
|
+
# mail.subject = "Here are some files for you!"
|
455
|
+
# mail.text = "This is what people with plain text mail readers will see"
|
456
|
+
# mail.html = "A little something <b>special</b> for people with HTML readers"
|
457
|
+
# mail.attach("/etc/fstab")
|
458
|
+
# mail.attach("/some/other/file")
|
459
|
+
#
|
460
|
+
# Net::SMTP.start('smtp1.testmailer.com', 25, 'mail.from.domain', fromaddress, password, :cram_md5) { |smtp|
|
461
|
+
# mail.to = toaddress
|
462
|
+
# smtp.send_message(mail.to_s(), fromaddress, toaddress)
|
463
|
+
# }
|
464
|
+
|
465
|
+
|
466
|
+
|
467
|
+
require 'base64'
|
468
|
+
require 'pathname'
|
469
|
+
|
470
|
+
# try to bring in the mime/types module, make a dummy module if it can't be found
|
471
|
+
begin
|
472
|
+
begin
|
473
|
+
require 'rubygems'
|
474
|
+
rescue LoadError
|
475
|
+
end
|
476
|
+
require 'mime/types'
|
477
|
+
rescue LoadError
|
478
|
+
module MIME
|
479
|
+
class Types
|
480
|
+
def Types::type_for(filename)
|
481
|
+
return('')
|
482
|
+
end
|
483
|
+
end
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
# An easy class for creating a mail message
|
488
|
+
class MailFactory
|
489
|
+
|
490
|
+
def initialize()
|
491
|
+
@headers = Array.new()
|
492
|
+
@attachments = Array.new()
|
493
|
+
@attachmentboundary = generate_boundary()
|
494
|
+
@bodyboundary = generate_boundary()
|
495
|
+
@html = nil
|
496
|
+
@text = nil
|
497
|
+
end
|
498
|
+
|
499
|
+
|
500
|
+
# adds a header to the bottom of the headers
|
501
|
+
def add_header(header, value)
|
502
|
+
@headers << "#{header}: #{value}"
|
503
|
+
end
|
504
|
+
|
505
|
+
|
506
|
+
# removes the named header - case insensitive
|
507
|
+
def remove_header(header)
|
508
|
+
@headers.each_index() { |i|
|
509
|
+
if(@headers[i] =~ /^#{Regexp.escape(header)}:/i)
|
510
|
+
@headers.delete_at(i)
|
511
|
+
end
|
512
|
+
}
|
513
|
+
end
|
514
|
+
|
515
|
+
|
516
|
+
# sets a header (removing any other versions of that header)
|
517
|
+
def set_header(header, value)
|
518
|
+
remove_header(header)
|
519
|
+
add_header(header, value)
|
520
|
+
end
|
521
|
+
|
522
|
+
|
523
|
+
def replyto=(newreplyto)
|
524
|
+
remove_header("Reply-To")
|
525
|
+
add_header("Reply-To", newreplyto)
|
526
|
+
end
|
527
|
+
|
528
|
+
|
529
|
+
def replyto()
|
530
|
+
return(get_header("Reply-To")[0])
|
531
|
+
end
|
532
|
+
|
533
|
+
def self.hattr h
|
534
|
+
h = h.to_s
|
535
|
+
module_eval <<-code
|
536
|
+
def #{ h.downcase }=(value)
|
537
|
+
remove_header("#{ h.downcase.capitalize }")
|
538
|
+
add_header("#{ h.downcase.capitalize }", value)
|
539
|
+
end
|
540
|
+
def #{ h.downcase }()
|
541
|
+
return(get_header("#{ h.downcase.capitalize }")[0])
|
542
|
+
end
|
543
|
+
code
|
544
|
+
end
|
545
|
+
%w( to from cc bcc subject ).each{|h| hattr h}
|
546
|
+
|
547
|
+
|
548
|
+
# sets the plain text body of the message
|
549
|
+
def text=(newtext)
|
550
|
+
@text = newtext
|
551
|
+
end
|
552
|
+
|
553
|
+
|
554
|
+
# sets the HTML body of the message. Only the body of the
|
555
|
+
# html should be provided
|
556
|
+
def html=(newhtml)
|
557
|
+
@html = "<html>\n<head>\n<meta content=\"text/html;charset=ISO-8859-1\" http-equiv=\"Content-Type\">\n</head>\n<body bgcolor=\"#ffffff\" text=\"#000000\">\n#{newhtml}\n</body>\n</html>"
|
558
|
+
end
|
559
|
+
|
560
|
+
|
561
|
+
# sets the HTML body of the message. The entire HTML section should be provided
|
562
|
+
def rawhtml=(newhtml)
|
563
|
+
@html = newhtml
|
564
|
+
end
|
565
|
+
|
566
|
+
|
567
|
+
# implement method missing to provide helper methods for setting and getting headers.
|
568
|
+
# Headers with '-' characters may be set/gotten as 'x_mailer' or 'XMailer' (splitting
|
569
|
+
# will occur between capital letters or on '_' chracters)
|
570
|
+
def method_missing(methId, *args)
|
571
|
+
name = methId.id2name()
|
572
|
+
|
573
|
+
# mangle the name if we have to
|
574
|
+
if(name =~ /_/)
|
575
|
+
name = name.gsub(/_/, '-')
|
576
|
+
elsif(name =~ /[A-Z]/)
|
577
|
+
name = name.gsub(/([a-zA-Z])([A-Z])/, '\1-\2')
|
578
|
+
end
|
579
|
+
|
580
|
+
# determine if it sets or gets, and do the right thing
|
581
|
+
if(name =~ /=$/)
|
582
|
+
if(args.length != 1)
|
583
|
+
super(methId, args)
|
584
|
+
end
|
585
|
+
set_header(name[/^(.*)=$/, 1], args[0])
|
586
|
+
else
|
587
|
+
if(args.length != 0)
|
588
|
+
super(methId, args)
|
589
|
+
end
|
590
|
+
headers = get_header(name)
|
591
|
+
return(get_header(name))
|
592
|
+
end
|
593
|
+
end
|
594
|
+
|
595
|
+
|
596
|
+
# returns the value (or values) of the named header in an array
|
597
|
+
def get_header(header)
|
598
|
+
headers = Array.new()
|
599
|
+
headerregex = /^#{Regexp.escape(header)}:/i
|
600
|
+
@headers.each() { |h|
|
601
|
+
if(headerregex.match(h))
|
602
|
+
headers << h[/^[^:]+:(.*)/i, 1].strip()
|
603
|
+
end
|
604
|
+
}
|
605
|
+
|
606
|
+
return(headers)
|
607
|
+
end
|
608
|
+
|
609
|
+
|
610
|
+
# returns true if the email is multipart
|
611
|
+
def multipart?()
|
612
|
+
if(@attachments.length > 0 or @html != nil)
|
613
|
+
return(true)
|
614
|
+
else
|
615
|
+
return(false)
|
616
|
+
end
|
617
|
+
end
|
618
|
+
|
619
|
+
|
620
|
+
# builds an email and returns it as a string. Takes the following options:
|
621
|
+
# <tt>:messageid</tt>:: Adds a message id to the message based on the from header (defaults to false)
|
622
|
+
# <tt>:date</tt>:: Adds a date to the message if one is not present (defaults to true)
|
623
|
+
def construct(options = Hash.new)
|
624
|
+
if(options[:date] == nil)
|
625
|
+
options[:date] = true
|
626
|
+
end
|
627
|
+
|
628
|
+
if(options[:messageid])
|
629
|
+
# add a unique message-id
|
630
|
+
remove_header("Message-ID")
|
631
|
+
sendingdomain = get_header('from')[0].to_s()[/@([-a-zA-Z0-9._]+)/,1].to_s()
|
632
|
+
add_header("Message-ID", "<#{Time.now.to_f()}.#{Process.euid()}.#{String.new.object_id()}@#{sendingdomain}>")
|
633
|
+
end
|
634
|
+
|
635
|
+
if(options[:date])
|
636
|
+
if(get_header("Date").length == 0)
|
637
|
+
add_header("Date", Time.now.strftime("%a, %d %b %Y %H:%M:%S %z"))
|
638
|
+
end
|
639
|
+
end
|
640
|
+
|
641
|
+
# Add a mime header if we don't already have one and we have multiple parts
|
642
|
+
if(multipart?())
|
643
|
+
if(get_header("MIME-Version").length == 0)
|
644
|
+
add_header("MIME-Version", "1.0")
|
645
|
+
end
|
646
|
+
|
647
|
+
if(get_header("Content-Type").length == 0)
|
648
|
+
if(@attachments.length == 0)
|
649
|
+
add_header("Content-Type", "multipart/alternative;boundary=\"#{@bodyboundary}\"")
|
650
|
+
else
|
651
|
+
add_header("Content-Type", "multipart/mixed; boundary=\"#{@attachmentboundary}\"")
|
652
|
+
end
|
653
|
+
end
|
654
|
+
end
|
655
|
+
|
656
|
+
return("#{headers_to_s()}#{body_to_s()}")
|
657
|
+
end
|
658
|
+
|
659
|
+
|
660
|
+
# returns a formatted email - equivalent to construct(:messageid => true)
|
661
|
+
def to_s()
|
662
|
+
return(construct(:messageid => true))
|
663
|
+
end
|
664
|
+
|
665
|
+
|
666
|
+
# generates a unique boundary string
|
667
|
+
def generate_boundary()
|
668
|
+
randomstring = Array.new()
|
669
|
+
1.upto(25) {
|
670
|
+
whichglyph = rand(100)
|
671
|
+
if(whichglyph < 40)
|
672
|
+
randomstring << (rand(25) + 65).chr()
|
673
|
+
elsif(whichglyph < 70)
|
674
|
+
randomstring << (rand(25) + 97).chr()
|
675
|
+
elsif(whichglyph < 90)
|
676
|
+
randomstring << (rand(10) + 48).chr()
|
677
|
+
elsif(whichglyph < 95)
|
678
|
+
randomstring << '.'
|
679
|
+
else
|
680
|
+
randomstring << '_'
|
681
|
+
end
|
682
|
+
}
|
683
|
+
return("----=_NextPart_#{randomstring.join()}")
|
684
|
+
end
|
685
|
+
|
686
|
+
|
687
|
+
# adds an attachment to the mail. Type may be given as a mime type. If it
|
688
|
+
# is left off and the MIME::Types module is available it will be determined automagically.
|
689
|
+
# If the optional attachemntheaders is given, then they will be added to the attachment
|
690
|
+
# boundary in the email, which can be used to produce Content-ID markers. attachmentheaders
|
691
|
+
# can be given as an Array or a String.
|
692
|
+
def add_attachment(filename, type=nil, attachmentheaders = nil)
|
693
|
+
attachment = Hash.new()
|
694
|
+
attachment['filename'] = Pathname.new(filename).basename
|
695
|
+
if(type == nil)
|
696
|
+
attachment['mimetype'] = MIME::Types.type_for(filename).to_s
|
697
|
+
else
|
698
|
+
attachment['mimetype'] = type
|
699
|
+
end
|
700
|
+
|
701
|
+
# Open in rb mode to handle Windows, which mangles binary files opened in a text mode
|
702
|
+
File.open(filename, "rb") { |fp|
|
703
|
+
attachment['attachment'] = file_encode(fp.read())
|
704
|
+
}
|
705
|
+
|
706
|
+
if(attachmentheaders != nil)
|
707
|
+
if(!attachmentheaders.kind_of?(Array))
|
708
|
+
attachmentheaders = attachmentheaders.split(/\r?\n/)
|
709
|
+
end
|
710
|
+
attachment['headers'] = attachmentheaders
|
711
|
+
end
|
712
|
+
|
713
|
+
@attachments << attachment
|
714
|
+
end
|
715
|
+
|
716
|
+
|
717
|
+
# adds an attachment to the mail as emailfilename. Type may be given as a mime type. If it
|
718
|
+
# is left off and the MIME::Types module is available it will be determined automagically.
|
719
|
+
# file may be given as an IO stream (which will be read until the end) or as a filename.
|
720
|
+
# If the optional attachemntheaders is given, then they will be added to the attachment
|
721
|
+
# boundary in the email, which can be used to produce Content-ID markers. attachmentheaders
|
722
|
+
# can be given as an Array of a String.
|
723
|
+
def add_attachment_as(file, emailfilename, type=nil, attachmentheaders = nil)
|
724
|
+
attachment = Hash.new()
|
725
|
+
attachment['filename'] = emailfilename
|
726
|
+
|
727
|
+
if(type != nil)
|
728
|
+
attachment['mimetype'] = type.to_s()
|
729
|
+
elsif(file.kind_of?(String) or file.kind_of?(Pathname))
|
730
|
+
attachment['mimetype'] = MIME::Types.type_for(file.to_s()).to_s
|
731
|
+
else
|
732
|
+
attachment['mimetype'] = ''
|
733
|
+
end
|
734
|
+
|
735
|
+
if(file.kind_of?(String) or file.kind_of?(Pathname))
|
736
|
+
# Open in rb mode to handle Windows, which mangles binary files opened in a text mode
|
737
|
+
File.open(file.to_s(), "rb") { |fp|
|
738
|
+
attachment['attachment'] = file_encode(fp.read())
|
739
|
+
}
|
740
|
+
elsif(file.respond_to?(:read))
|
741
|
+
attachment['attachment'] = file_encode(file.read())
|
742
|
+
else
|
743
|
+
raise(Exception, "file is not a supported type (must be a String, Pathnamem, or support read method)")
|
744
|
+
end
|
745
|
+
|
746
|
+
if(attachmentheaders != nil)
|
747
|
+
if(!attachmentheaders.kind_of?(Array))
|
748
|
+
attachmentheaders = attachmentheaders.split(/\r?\n/)
|
749
|
+
end
|
750
|
+
attachment['headers'] = attachmentheaders
|
751
|
+
end
|
752
|
+
|
753
|
+
@attachments << attachment
|
754
|
+
end
|
755
|
+
|
756
|
+
|
757
|
+
alias attach add_attachment
|
758
|
+
alias attach_as add_attachment_as
|
759
|
+
|
760
|
+
protected
|
761
|
+
|
762
|
+
# returns the @headers as a properly formatted string
|
763
|
+
def headers_to_s()
|
764
|
+
return("#{@headers.join("\r\n")}\r\n\r\n")
|
765
|
+
end
|
766
|
+
|
767
|
+
|
768
|
+
# returns the body as a properly formatted string
|
769
|
+
def body_to_s()
|
770
|
+
body = Array.new()
|
771
|
+
|
772
|
+
# simple message with one part
|
773
|
+
if(!multipart?())
|
774
|
+
return(@text)
|
775
|
+
else
|
776
|
+
body << "This is a multi-part message in MIME format.\r\n\r\n--#{@attachmentboundary}\r\nContent-Type: multipart/alternative; boundary=\"#{@bodyboundary}\""
|
777
|
+
|
778
|
+
if(@attachments.length > 0)
|
779
|
+
# text part
|
780
|
+
body << "#{buildbodyboundary('text/plain; charset=ISO-8859-1; format=flowed', '7bit')}\r\n\r\n#{@text}"
|
781
|
+
|
782
|
+
# html part if one is provided
|
783
|
+
if @html
|
784
|
+
body << "#{buildbodyboundary('text/html; charset=ISO-8859-1', '7bit')}\r\n\r\n#{@html}"
|
785
|
+
end
|
786
|
+
|
787
|
+
body << "--#{@bodyboundary}--"
|
788
|
+
|
789
|
+
# and, the attachments
|
790
|
+
if(@attachments.length > 0)
|
791
|
+
@attachments.each() { |attachment|
|
792
|
+
body << "#{buildattachmentboundary(attachment)}\r\n\r\n#{attachment['attachment']}"
|
793
|
+
}
|
794
|
+
body << "\r\n--#{@attachmentboundary}--"
|
795
|
+
end
|
796
|
+
else
|
797
|
+
# text part
|
798
|
+
body << "#{buildbodyboundary('text/plain; charset=ISO-8859-1; format=flowed', '7bit')}\r\n\r\n#{@text}"
|
799
|
+
|
800
|
+
# html part
|
801
|
+
body << "#{buildbodyboundary('text/html; charset=ISO-8859-1', '7bit')}\r\n\r\n#{@html}"
|
802
|
+
|
803
|
+
body << "--#{@bodyboundary}--"
|
804
|
+
end
|
805
|
+
|
806
|
+
return(body.join("\r\n\r\n"))
|
807
|
+
end
|
808
|
+
end
|
809
|
+
|
810
|
+
|
811
|
+
# builds a boundary string for including attachments in the body, expects an attachment hash as built by
|
812
|
+
# add_attachment and add_attachment_as
|
813
|
+
def buildattachmentboundary(attachment)
|
814
|
+
disposition = "Content-Disposition: inline; filename=\"#{attachment['filename']}\""
|
815
|
+
boundary = "--#{@attachmentboundary}\r\nContent-Type: #{attachment['mimetype']}; name=\"#{attachment['filename']}\"\r\nContent-Transfer-Encoding: base64\r\n#{disposition}"
|
816
|
+
if(attachment['headers'])
|
817
|
+
boundary = boundary + "\r\n#{attachment['headers'].join("\r\n")}"
|
818
|
+
end
|
819
|
+
|
820
|
+
return(boundary)
|
821
|
+
end
|
822
|
+
|
823
|
+
|
824
|
+
# builds a boundary string for inclusion in the body of a message
|
825
|
+
def buildbodyboundary(type, encoding)
|
826
|
+
return("--#{@bodyboundary}\r\nContent-Type: #{type}\r\nContent-Transfer-Encoding: #{encoding}")
|
827
|
+
end
|
828
|
+
|
829
|
+
|
830
|
+
# returns a base64 encoded version of the contents of str
|
831
|
+
def file_encode(str)
|
832
|
+
collection = Array.new()
|
833
|
+
enc = Base64.encode64(str)
|
834
|
+
# while(enc.length > 60)
|
835
|
+
# collection << enc.slice!(0..59)
|
836
|
+
# end
|
837
|
+
# collection << enc
|
838
|
+
# return(collection.join("\n"))
|
839
|
+
return(enc)
|
840
|
+
end
|
841
|
+
|
842
|
+
end
|
843
|
+
|
844
|
+
#--}}}
|
845
|
+
|
846
|
+
}
|
847
|
+
|
848
|
+
|
849
|
+
__END__
|
850
|
+
### rqmailer
|
851
|
+
|
852
|
+
CMD:
|
853
|
+
<%= cmd %>
|
854
|
+
|
855
|
+
EXIT_STATUS:
|
856
|
+
<%= exit_status %>
|
857
|
+
|
858
|
+
STDIN:
|
859
|
+
<%= stdin %>
|
860
|
+
|
861
|
+
STDOUT:
|
862
|
+
<%= stdout %>
|
863
|
+
|
864
|
+
STDERR:
|
865
|
+
<%= stderr %>
|