bulkmail 0.1 → 0.3.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/CHANGELOG +29 -1
- data/README +21 -24
- data/Rakefile +4 -3
- data/bin/bulkmail +25 -39
- data/doc/rdoc/created.rid +1 -0
- data/lib/bulkmail/dns_mx.rb +18 -0
- data/lib/bulkmail/message.rb +103 -0
- data/lib/bulkmail/sender.rb +90 -0
- data/lib/bulkmail/thread_pool.rb +113 -0
- data/lib/bulkmail.rb +4 -75
- metadata +10 -3
data/CHANGELOG
CHANGED
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
*0.3.0*
|
|
2
|
+
|
|
3
|
+
* Connecting directly to the recipient's SMTP server.
|
|
4
|
+
|
|
5
|
+
* Implemented Sender class that does the actual work.
|
|
6
|
+
|
|
7
|
+
* Implemented Message and Message::MultiPart classes for composing messages.
|
|
8
|
+
|
|
9
|
+
* Rewritten from the ground up.
|
|
10
|
+
|
|
11
|
+
*0.2.1*
|
|
12
|
+
|
|
13
|
+
* Added shuffling of addresses.
|
|
14
|
+
|
|
15
|
+
* Fixed batch slicing to actually work.
|
|
16
|
+
|
|
17
|
+
*0.2*
|
|
18
|
+
|
|
19
|
+
* Updated README.
|
|
20
|
+
|
|
21
|
+
* Set batch size at 10.
|
|
22
|
+
|
|
23
|
+
* Fixed script to use new version.
|
|
24
|
+
|
|
25
|
+
* Removed actionmailer dependancy.
|
|
26
|
+
|
|
27
|
+
* Rewrote to use Net::SMTP directly.
|
|
28
|
+
|
|
1
29
|
*0.1*
|
|
2
30
|
|
|
3
31
|
* Add INT trap.
|
|
@@ -22,4 +50,4 @@
|
|
|
22
50
|
|
|
23
51
|
* Preliminary content.
|
|
24
52
|
|
|
25
|
-
* Created basic project structure.
|
|
53
|
+
* Created basic project structure.
|
data/README
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
=
|
|
1
|
+
= About Bulkmail
|
|
2
2
|
|
|
3
|
-
Bulkmail is a simple bulk mailing utility in Ruby. It takes a content file and a recipients file, and sends the content to each address listed in the recipients file.
|
|
3
|
+
Bulkmail is a simple bulk mailing utility in Ruby. It takes a content file and a recipients file, and sends the content to each address listed in the recipients file. Bulkmail sends email by connecting directly to each recipient's SMTP server, and you therefore do not need your own SMTP server in order to send messages.
|
|
4
4
|
|
|
5
5
|
== Usage
|
|
6
6
|
|
|
@@ -11,32 +11,29 @@ Bulkmail is a simple bulk mailing utility in Ruby. It takes a content file and a
|
|
|
11
11
|
where the options are:
|
|
12
12
|
[--recipients, -r] recipients file name
|
|
13
13
|
[--content, -c] content file name
|
|
14
|
-
[--subject, -s] subject
|
|
15
14
|
[--from, -f] from
|
|
16
|
-
[--
|
|
17
|
-
[--user, -u] user name (if authentication is needed)
|
|
18
|
-
[--password, -p] password (if authentication is needed)
|
|
19
|
-
[--auth, -a] authentication type
|
|
20
|
-
[--mime, -m] message mime type.
|
|
15
|
+
[--helo, -h] SMTP HELO domain (most SMTP servers require you to identify with your domain)
|
|
21
16
|
|
|
22
|
-
You
|
|
17
|
+
You should include headers in the content file. Remember that the headers should be followed by a blank link, followed by the message body. For example:
|
|
23
18
|
|
|
24
|
-
Subject:
|
|
19
|
+
Subject: My New Thing
|
|
20
|
+
From: myemail@mydomain.com
|
|
21
|
+
Content-Type: text/html; charset=UTF-8
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
A special concert is about to take place this Thursday at...
|
|
23
|
+
<html><body><h1>My New Thing!</h1></body></html>
|
|
28
24
|
|
|
29
25
|
=== From Ruby
|
|
30
26
|
|
|
31
|
-
BulkMail
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
27
|
+
msg = BulkMail::Message({
|
|
28
|
+
:from => 'myemail@mydomain.com',
|
|
29
|
+
:subject => 'nothing really'
|
|
30
|
+
}, 'hi there')
|
|
31
|
+
|
|
32
|
+
sender = BulkMail::Sender.new({
|
|
33
|
+
:list => addresses,
|
|
34
|
+
:from => 'myemail@mydomain.com',
|
|
35
|
+
:helo => 'mydomain.com',
|
|
36
|
+
:message => msg
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
sender.start
|
data/Rakefile
CHANGED
|
@@ -7,7 +7,7 @@ require 'fileutils'
|
|
|
7
7
|
include FileUtils
|
|
8
8
|
|
|
9
9
|
NAME = "bulkmail"
|
|
10
|
-
VERS = "0.
|
|
10
|
+
VERS = "0.3.0"
|
|
11
11
|
CLEAN.include ['**/.*.sw?', '*.gem', '.config']
|
|
12
12
|
RDOC_OPTS = ['--quiet', '--title', "Bulkmail Documentation",
|
|
13
13
|
"--opname", "index.html",
|
|
@@ -26,7 +26,7 @@ Rake::RDocTask.new do |rdoc|
|
|
|
26
26
|
rdoc.options += RDOC_OPTS
|
|
27
27
|
rdoc.main = "README"
|
|
28
28
|
rdoc.title = "Bulkmail Documentation"
|
|
29
|
-
rdoc.rdoc_files.add ['README', 'CHANGELOG', 'COPYING', 'lib
|
|
29
|
+
rdoc.rdoc_files.add ['README', 'CHANGELOG', 'COPYING', 'lib/**/*']
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
spec = Gem::Specification.new do |s|
|
|
@@ -44,9 +44,10 @@ spec = Gem::Specification.new do |s|
|
|
|
44
44
|
s.homepage = 'http://code.google.com/p/bulkmail/'
|
|
45
45
|
s.executables = ['bulkmail']
|
|
46
46
|
|
|
47
|
-
s.add_dependency('actionmailer')
|
|
48
47
|
s.required_ruby_version = '>= 1.8.2'
|
|
49
48
|
|
|
49
|
+
s.add_dependency('pNet-DNS')
|
|
50
|
+
|
|
50
51
|
s.files = %w(COPYING README Rakefile) + Dir.glob("{bin,doc,lib}/**/*")
|
|
51
52
|
|
|
52
53
|
s.require_path = "lib"
|
data/bin/bulkmail
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
|
|
3
|
+
STDERR.reopen "/dev/null", "a"
|
|
4
|
+
# We do this because pNet-DNS throws out a bunch of warnings
|
|
5
|
+
|
|
3
6
|
require 'rubygems'
|
|
4
7
|
require 'optparse'
|
|
8
|
+
require 'bulkmail'
|
|
5
9
|
|
|
6
|
-
|
|
7
|
-
|
|
10
|
+
$helo = nil
|
|
11
|
+
$from = nil
|
|
12
|
+
$recipients = []
|
|
13
|
+
$content = nil
|
|
8
14
|
|
|
9
15
|
opts = OptionParser.new do |opts|
|
|
10
16
|
opts.banner = "Usage: bulkmail <options>"
|
|
@@ -12,42 +18,22 @@ opts = OptionParser.new do |opts|
|
|
|
12
18
|
opts.separator ""
|
|
13
19
|
opts.separator "Options:"
|
|
14
20
|
|
|
15
|
-
opts.on("-h", "--
|
|
16
|
-
|
|
21
|
+
opts.on("-h", "--helo HOSTNAME", "HELO host name.") do |v|
|
|
22
|
+
$helo = v
|
|
17
23
|
end
|
|
18
24
|
|
|
19
25
|
opts.on("-r", "--recipients filename", "Recipients file name") do |v|
|
|
20
|
-
|
|
26
|
+
$recipients = IO.readlines(v).map {|l| l =~ /(.*)(\r\n|\n)$/ ? $1 : l}.compact
|
|
21
27
|
end
|
|
22
28
|
|
|
23
29
|
opts.on("-c", "--content filename", "Content file name") do |v|
|
|
24
|
-
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
opts.on("-s", "--subject subject", "Subject line") do |v|
|
|
28
|
-
config[:subject] = v
|
|
30
|
+
$content = IO.read(v)
|
|
29
31
|
end
|
|
30
32
|
|
|
31
33
|
opts.on("-f", "--from from", "From address") do |v|
|
|
32
|
-
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
opts.on("-u", "--user username", "User name (for authentication)") do |v|
|
|
36
|
-
config[:user] = v
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
opts.on("-p", "--password password", "Password (for authentication)") do |v|
|
|
40
|
-
config[:password] = v
|
|
34
|
+
$from = v
|
|
41
35
|
end
|
|
42
36
|
|
|
43
|
-
opts.on("-a", "--authentication type", "Authentication type") do |v|
|
|
44
|
-
config[:authentication] = v
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
opts.on("-m", "--mime type", "MIME type") do |v|
|
|
48
|
-
config[:content_type] = v
|
|
49
|
-
end
|
|
50
|
-
|
|
51
37
|
# No argument, shows at tail. This will print an options summary.
|
|
52
38
|
# Try it and see!
|
|
53
39
|
opts.on_tail("-?", "--help", "Show this message") do
|
|
@@ -68,24 +54,24 @@ begin
|
|
|
68
54
|
opts.parse! ARGV
|
|
69
55
|
rescue => e
|
|
70
56
|
puts e.message
|
|
57
|
+
puts e.backtrace.first
|
|
71
58
|
exit
|
|
72
59
|
end
|
|
73
60
|
|
|
74
|
-
unless
|
|
61
|
+
unless $content
|
|
75
62
|
puts opts
|
|
76
63
|
exit
|
|
77
64
|
end
|
|
78
65
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
BulkMail.server_settings[:address] = config[:host] if config[:host]
|
|
82
|
-
BulkMail.server_settings[:user_name] = config[:user] if config[:user]
|
|
83
|
-
BulkMail.server_settings[:password] = config[:password] if config[:password]
|
|
84
|
-
BulkMail.server_settings[:authentication] = config[:authentication].to_sym if config[:authentication]
|
|
66
|
+
trap('INT') {exit}
|
|
85
67
|
|
|
86
|
-
|
|
68
|
+
puts "Please hold on..."
|
|
87
69
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
70
|
+
sender = BulkMail::Sender.new({
|
|
71
|
+
:list => $recipients,
|
|
72
|
+
:from => $from,
|
|
73
|
+
:message => $content,
|
|
74
|
+
:helo => $helo
|
|
75
|
+
|
|
76
|
+
})
|
|
77
|
+
sender.start
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Mon Nov 06 23:13:08 +0200 2006
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'net/smtp'
|
|
3
|
+
require 'net/dns'
|
|
4
|
+
|
|
5
|
+
module BulkMail
|
|
6
|
+
# Performs a DNS query for a domain's MX records and returns an array
|
|
7
|
+
# of SMTP servers for the domain ordered by preference. If no records
|
|
8
|
+
# are found an error is raised.
|
|
9
|
+
def self.smtp_servers_for_domain(domain)
|
|
10
|
+
res = Net::DNS::Resolver.new
|
|
11
|
+
mx = Net::DNS.mx(domain, res, 'IN')
|
|
12
|
+
if mx
|
|
13
|
+
mx.sort {|x,y| y.preference <=> x.preference}.map {|rr| rr.exchange}
|
|
14
|
+
else
|
|
15
|
+
raise RuntimeError, "Could not locate MX records for domain #{domain}."
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Symbol extensions
|
|
2
|
+
class Symbol
|
|
3
|
+
# Returns the symbol as a header string
|
|
4
|
+
def to_header
|
|
5
|
+
return @to_header if @to_header
|
|
6
|
+
s = to_s
|
|
7
|
+
@to_header = (s.upcase == s) ? s :
|
|
8
|
+
to_s.split('_').map{|w| w.capitalize}.join('-')
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class String
|
|
13
|
+
alias_method :to_header, :to_s
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
module BulkMail
|
|
17
|
+
# The Message class can be used to compose email messages with headers. Usage
|
|
18
|
+
# is pretty straightforward:
|
|
19
|
+
#
|
|
20
|
+
# m = BulkMail::Message.new({:from => my_email, :subject => 'hello'},
|
|
21
|
+
# 'Hi there!')
|
|
22
|
+
class Message
|
|
23
|
+
attr_accessor :headers, :body
|
|
24
|
+
|
|
25
|
+
DEFAULT_HEADERS = {
|
|
26
|
+
:mime_version => '1.0'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
PLAIN = 'text/plain; charset=UTF-8'.freeze
|
|
30
|
+
HTML = 'text/html; charset=UTF-8'.freeze
|
|
31
|
+
|
|
32
|
+
# Creates a new message, including the default headers in the message headers.
|
|
33
|
+
def initialize(headers, body)
|
|
34
|
+
@headers = DEFAULT_HEADERS.merge(headers)
|
|
35
|
+
@body = body
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
LINE_BREAK = "\r\n"
|
|
39
|
+
HEADER_FMT = "%s: %s\r\n".freeze
|
|
40
|
+
|
|
41
|
+
# Formats the message headers into a string.
|
|
42
|
+
def self.format_headers(headers)
|
|
43
|
+
headers.inject('') do |m, kv|
|
|
44
|
+
if kv[1]
|
|
45
|
+
m << (HEADER_FMT % [kv[0].to_header, kv[1]])
|
|
46
|
+
else
|
|
47
|
+
m
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Composes the header and body into a message string.
|
|
53
|
+
def to_s
|
|
54
|
+
msg = Message.format_headers(@headers)
|
|
55
|
+
msg << LINE_BREAK
|
|
56
|
+
msg << body
|
|
57
|
+
msg
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The MultiPart class can be used for composing multi-part messages. For
|
|
62
|
+
# example:
|
|
63
|
+
#
|
|
64
|
+
# m = BulkMail::Message::MultiPart.new(
|
|
65
|
+
# {:from => 'my_email', :subject => 'Hello'}, :alternate)
|
|
66
|
+
# m.add_part({:content_type => BulkMail::Message::PLAIN},
|
|
67
|
+
# 'You are now seeing the plain text version')
|
|
68
|
+
# m.add_part({:content_type => BulkMail::Message::HTML},
|
|
69
|
+
# '<p>You are now seeing the HTML version</p>')
|
|
70
|
+
# puts m.to_s
|
|
71
|
+
class Message::MultiPart < Message
|
|
72
|
+
attr_accessor :parts, :multi_type
|
|
73
|
+
|
|
74
|
+
# Initializes a new instance.
|
|
75
|
+
def initialize(headers, multi_type)
|
|
76
|
+
super(headers, nil)
|
|
77
|
+
@multi_type = multi_type
|
|
78
|
+
@parts = []
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Adds a part to the message.
|
|
82
|
+
def add_part(headers, body)
|
|
83
|
+
@parts << Message.new(headers, body)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
BOUNDARY = '!!@@##$$'.freeze
|
|
87
|
+
MULTI_PART_CONTENT_TYPE_FMT = "multipart/%s; charset=UTF-8; boundary=\"#{BOUNDARY}\"".freeze
|
|
88
|
+
BOUNDARY_LINE = "\r\n--#{BOUNDARY}\r\n".freeze
|
|
89
|
+
|
|
90
|
+
# Composes the parts into an email message.
|
|
91
|
+
def to_s
|
|
92
|
+
@headers[:content_type] = MULTI_PART_CONTENT_TYPE_FMT % @multi_type
|
|
93
|
+
msg = Message.format_headers(@headers)
|
|
94
|
+
@parts.each do |part|
|
|
95
|
+
msg << BOUNDARY_LINE
|
|
96
|
+
msg << part.to_s
|
|
97
|
+
end
|
|
98
|
+
msg << BOUNDARY_LINE
|
|
99
|
+
msg
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
require 'net/smtp'
|
|
2
|
+
|
|
3
|
+
module BulkMail
|
|
4
|
+
# Sender instances perform the actual work of sending an email. Each sender
|
|
5
|
+
# sets up a thread pool (by default with up to 20 threads) for performing the
|
|
6
|
+
# work. When creating a Sender instance, a hash of options is supplied:
|
|
7
|
+
#
|
|
8
|
+
# BulkMail::Sender.new({
|
|
9
|
+
# :list => addresses,
|
|
10
|
+
# :from => my_address,
|
|
11
|
+
# :helo => my_helo_domain,
|
|
12
|
+
# :concurrent => 50, # size of thread pool
|
|
13
|
+
# :message => message
|
|
14
|
+
# })
|
|
15
|
+
#
|
|
16
|
+
# To start sending, invoke Sender#start. The start method will block until
|
|
17
|
+
# all addresses have been processed.
|
|
18
|
+
class Sender
|
|
19
|
+
# Initializes a new instance with the supplied options hash, and creates a
|
|
20
|
+
# thread pool (with a default maximum size of 20).
|
|
21
|
+
def initialize(opts)
|
|
22
|
+
@opts = opts
|
|
23
|
+
@pool = ThreadPool.new(@opts[:concurrent] || 20)
|
|
24
|
+
@smtp_servers = {}
|
|
25
|
+
@mutex = Mutex.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Starts processing the addresses by sending each one a message, and waits
|
|
29
|
+
# for the thread pool to finish its work.
|
|
30
|
+
def start
|
|
31
|
+
@opts[:list].each {|a| @pool.process {send(a)}}
|
|
32
|
+
@pool.join
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# returns an array of SMTP servers for the email address. If an invalid
|
|
36
|
+
# address is specified, an error is raised. SMTP servers for each email
|
|
37
|
+
# domain are memoized to improve performance.
|
|
38
|
+
def smtp_servers_for_addr(addr)
|
|
39
|
+
if (addr =~ /<(.*)@(.*)>/) or (addr =~ /(.*)@(.*)/)
|
|
40
|
+
domain = $2
|
|
41
|
+
else
|
|
42
|
+
raise RuntimeError, "Invalid email address."
|
|
43
|
+
end
|
|
44
|
+
@mutex.synchronize do
|
|
45
|
+
@smtp_servers[domain] ||= BulkMail.smtp_servers_for_domain(domain)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Sends an email to a recipient by trying any of the SMTP servers available
|
|
50
|
+
# for the recipient.
|
|
51
|
+
def send(addr)
|
|
52
|
+
servers = smtp_servers_for_addr(addr)
|
|
53
|
+
if servers.empty?
|
|
54
|
+
report_addr(addr, :error, 'Unknown domain')
|
|
55
|
+
return
|
|
56
|
+
end
|
|
57
|
+
servers.each do |server|
|
|
58
|
+
begin
|
|
59
|
+
send_message(server, addr)
|
|
60
|
+
report_addr(addr, :success)
|
|
61
|
+
return
|
|
62
|
+
rescue => e
|
|
63
|
+
report_addr(addr, :error, e.message)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Opens a connection to an SMTP server and sends the message.
|
|
69
|
+
def send_message(server, addr)
|
|
70
|
+
Net::SMTP.start(
|
|
71
|
+
server, nil, @opts[:helo], nil, nil, nil) do |smtp|
|
|
72
|
+
smtp.send_message @opts[:message].to_s, @opts[:from], addr
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Reports success or failure for the recipient.
|
|
77
|
+
def report_addr(addr, status, error = nil)
|
|
78
|
+
@mutex.synchronize do
|
|
79
|
+
case status
|
|
80
|
+
when :success:
|
|
81
|
+
puts "Sent mail to #{addr}"
|
|
82
|
+
File.open('sent.bulkmail', 'a') {|f| f.puts "#{addr}"}
|
|
83
|
+
when :error:
|
|
84
|
+
puts "Failed to send to #{addr}: #{error}"
|
|
85
|
+
File.open('failed.bulkmail', 'a') {|f| f.puts "#{addr}: #{error}"}
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
require 'thread'
|
|
2
|
+
|
|
3
|
+
# This class implements a pool of threads. The pool hands out blocks to be
|
|
4
|
+
# processed to any available worker thread. If no worker thread is available,
|
|
5
|
+
# the pool blocks until any of the workers have become available. The pool
|
|
6
|
+
# size can be specified when constructing the pool. For exmaple:
|
|
7
|
+
#
|
|
8
|
+
# pool = ThreadPool.new(10)
|
|
9
|
+
# 10.times {pool.process {sleep 3}}
|
|
10
|
+
# 10.times {pool.process {sleep 3}} # this will block for about 3 seconds.
|
|
11
|
+
# pool.join # this will block for about 3 more seconds.
|
|
12
|
+
class ThreadPool
|
|
13
|
+
# The Worker class starts a thread for performing work. A worker thread
|
|
14
|
+
# sleeps until it receives a block for processing, at which time it is
|
|
15
|
+
# considered busy. Once the block has been processed, the worker is
|
|
16
|
+
# considered available again.
|
|
17
|
+
class Worker
|
|
18
|
+
# Starts the worker thread.
|
|
19
|
+
def initialize
|
|
20
|
+
@mutex = Mutex.new
|
|
21
|
+
@thread = Thread.new do
|
|
22
|
+
while true
|
|
23
|
+
sleep 0.001
|
|
24
|
+
block = get_block
|
|
25
|
+
if block
|
|
26
|
+
block.call
|
|
27
|
+
reset_block
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Returns the block to be processed, if any.
|
|
34
|
+
def get_block
|
|
35
|
+
@mutex.synchronize {@block}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Sets the block to be processed.
|
|
39
|
+
def set_block(block)
|
|
40
|
+
@mutex.synchronize {@block = block}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Resets the block instance variable to signify that the worker
|
|
44
|
+
# is available.
|
|
45
|
+
def reset_block
|
|
46
|
+
@mutex.synchronize {@block = nil}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Returns true if the worker is busy processing a block.
|
|
50
|
+
def busy?
|
|
51
|
+
@mutex.synchronize {!@block.nil?}
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
attr_accessor :max_size
|
|
56
|
+
attr_reader :workers
|
|
57
|
+
|
|
58
|
+
# Creates a new pool with a default maximum size of 10.
|
|
59
|
+
def initialize(max_size = 10)
|
|
60
|
+
@max_size = max_size
|
|
61
|
+
@workers = []
|
|
62
|
+
@mutex = Mutex.new
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Returns the number of workers in the pool.
|
|
66
|
+
def size
|
|
67
|
+
@mutex.synchronize {@workers.size}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Returns true if any of the workers in the pool are busy.
|
|
71
|
+
def busy?
|
|
72
|
+
@mutex.synchronize {@workers.any? {|w| w.busy?}}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Blocks until all workers have become available.
|
|
76
|
+
def join
|
|
77
|
+
sleep 0.01 while busy?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Hands the block to an available worker. If no workers are available, this
|
|
81
|
+
# method will block until one becomes available.
|
|
82
|
+
def process(&block)
|
|
83
|
+
wait_for_worker.set_block(block)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Waits for a worker to become available.
|
|
87
|
+
def wait_for_worker
|
|
88
|
+
while true
|
|
89
|
+
worker = find_available_worker
|
|
90
|
+
return worker if worker
|
|
91
|
+
sleep 0.01
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Returns an available worker or a new worker if the maximum pool size
|
|
96
|
+
# has not been reached.
|
|
97
|
+
def find_available_worker
|
|
98
|
+
@mutex.synchronize {free_worker || create_worker}
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Returns the first available worker.
|
|
102
|
+
def free_worker
|
|
103
|
+
@workers.each {|w| return w unless w.busy?}; nil
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Creates a worker if the maximum pool size has not been reached.
|
|
107
|
+
def create_worker
|
|
108
|
+
return nil if @workers.size >= @max_size
|
|
109
|
+
worker = Worker.new
|
|
110
|
+
@workers << worker
|
|
111
|
+
worker
|
|
112
|
+
end
|
|
113
|
+
end
|
data/lib/bulkmail.rb
CHANGED
|
@@ -1,75 +1,4 @@
|
|
|
1
|
-
require '
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
module BulkMail
|
|
6
|
-
class Mailer < ActionMailer::Base #:nodoc
|
|
7
|
-
def msg(addr, sub, content, from_addr, c_type, additional_headers)
|
|
8
|
-
recipients addr
|
|
9
|
-
subject sub if sub
|
|
10
|
-
body content
|
|
11
|
-
from from_addr if from_addr
|
|
12
|
-
content_type c_type if c_type
|
|
13
|
-
headers additional_headers if additional_headers
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
# The Job class performs the bulk-mailing work. Each instance starts a thread
|
|
18
|
-
# That goes over the list of recipients and sends each one an email. A job
|
|
19
|
-
# can also be queried for its progress, returning a number between 0 and 1.
|
|
20
|
-
class Job
|
|
21
|
-
attr_accessor :progress, :errors, :thread, :delivered, :total
|
|
22
|
-
|
|
23
|
-
def initialize(opts)
|
|
24
|
-
@recipients = IO.readlines(opts[:recipients_file])
|
|
25
|
-
@subject = opts[:subject]
|
|
26
|
-
@from = opts[:from]
|
|
27
|
-
@content_type = opts[:content_type]
|
|
28
|
-
@content = load_content(opts[:content_file])
|
|
29
|
-
@progress = 0
|
|
30
|
-
@delivered = 0
|
|
31
|
-
@total = @recipients.size
|
|
32
|
-
@errors = []
|
|
33
|
-
@thread = Thread.new {process}
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def load_content(fn)
|
|
37
|
-
content = IO.readlines(fn)
|
|
38
|
-
while line = content.shift
|
|
39
|
-
break if line.empty?
|
|
40
|
-
if line =~ /^(.+):\s?(.*)$/
|
|
41
|
-
@headers ||= {}
|
|
42
|
-
@headers[$1] = $2
|
|
43
|
-
else
|
|
44
|
-
content.unshift(line)
|
|
45
|
-
break
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
content.join
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def process
|
|
52
|
-
@recipients.each_with_index do |addr, idx|
|
|
53
|
-
begin
|
|
54
|
-
Mailer.deliver_msg(addr, @subject, @content, @from, @content_type, @headers)
|
|
55
|
-
@progress = idx / @recipients.size
|
|
56
|
-
@delivered += 1
|
|
57
|
-
puts "Sent email to #{addr}"
|
|
58
|
-
rescue => e
|
|
59
|
-
puts e.message
|
|
60
|
-
@errors << e
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
@progress = 1
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def self.send(opts)
|
|
68
|
-
Job.new(opts)
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def self.server_settings
|
|
72
|
-
ActionMailer::Base.server_settings
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'bulkmail/thread_pool')
|
|
2
|
+
require File.join(File.dirname(__FILE__), 'bulkmail/dns_mx')
|
|
3
|
+
require File.join(File.dirname(__FILE__), 'bulkmail/message')
|
|
4
|
+
require File.join(File.dirname(__FILE__), 'bulkmail/sender')
|
metadata
CHANGED
|
@@ -3,8 +3,8 @@ rubygems_version: 0.9.0
|
|
|
3
3
|
specification_version: 1
|
|
4
4
|
name: bulkmail
|
|
5
5
|
version: !ruby/object:Gem::Version
|
|
6
|
-
version:
|
|
7
|
-
date:
|
|
6
|
+
version: 0.3.0
|
|
7
|
+
date: 2007-01-12 00:00:00 +02:00
|
|
8
8
|
summary: Ruby bulk-mailer.
|
|
9
9
|
require_paths:
|
|
10
10
|
- lib
|
|
@@ -33,7 +33,14 @@ files:
|
|
|
33
33
|
- README
|
|
34
34
|
- Rakefile
|
|
35
35
|
- bin/bulkmail
|
|
36
|
+
- doc/rdoc
|
|
37
|
+
- doc/rdoc/created.rid
|
|
38
|
+
- lib/bulkmail
|
|
36
39
|
- lib/bulkmail.rb
|
|
40
|
+
- lib/bulkmail/dns_mx.rb
|
|
41
|
+
- lib/bulkmail/message.rb
|
|
42
|
+
- lib/bulkmail/sender.rb
|
|
43
|
+
- lib/bulkmail/thread_pool.rb
|
|
37
44
|
- CHANGELOG
|
|
38
45
|
test_files: []
|
|
39
46
|
|
|
@@ -63,7 +70,7 @@ requirements: []
|
|
|
63
70
|
|
|
64
71
|
dependencies:
|
|
65
72
|
- !ruby/object:Gem::Dependency
|
|
66
|
-
name:
|
|
73
|
+
name: pNet-DNS
|
|
67
74
|
version_requirement:
|
|
68
75
|
version_requirements: !ruby/object:Gem::Version::Requirement
|
|
69
76
|
requirements:
|