bulkmail 0.2 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +17 -1
- data/README +18 -20
- data/Rakefile +4 -2
- data/bin/bulkmail +19 -44
- data/doc/rdoc/created.rid +1 -1
- data/lib/bulkmail.rb +4 -110
- 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
- metadata +17 -20
- data/doc/rdoc/classes/Array.html +0 -155
- data/doc/rdoc/classes/BulkMail.html +0 -310
- data/doc/rdoc/classes/String.html +0 -178
- data/doc/rdoc/classes/Symbol.html +0 -155
- data/doc/rdoc/files/CHANGELOG.html +0 -165
- data/doc/rdoc/files/COPYING.html +0 -129
- data/doc/rdoc/files/README.html +0 -160
- data/doc/rdoc/files/lib/bulkmail_rb.html +0 -108
- data/doc/rdoc/fr_class_index.html +0 -30
- data/doc/rdoc/fr_file_index.html +0 -30
- data/doc/rdoc/fr_method_index.html +0 -35
- data/doc/rdoc/index.html +0 -24
- data/doc/rdoc/rdoc-style.css +0 -208
data/CHANGELOG
CHANGED
@@ -1,3 +1,19 @@
|
|
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
|
+
|
1
17
|
*0.2*
|
2
18
|
|
3
19
|
* Updated README.
|
@@ -34,4 +50,4 @@
|
|
34
50
|
|
35
51
|
* Preliminary content.
|
36
52
|
|
37
|
-
* 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,31 +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
19
|
Subject: My New Thing
|
20
|
+
From: myemail@mydomain.com
|
25
21
|
Content-Type: text/html; charset=UTF-8
|
26
22
|
|
27
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
|
-
|
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|
|
@@ -46,6 +46,8 @@ spec = Gem::Specification.new do |s|
|
|
46
46
|
|
47
47
|
s.required_ruby_version = '>= 1.8.2'
|
48
48
|
|
49
|
+
s.add_dependency('pNet-DNS')
|
50
|
+
|
49
51
|
s.files = %w(COPYING README Rakefile) + Dir.glob("{bin,doc,lib}/**/*")
|
50
52
|
|
51
53
|
s.require_path = "lib"
|
data/bin/bulkmail
CHANGED
@@ -1,28 +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'
|
5
8
|
require 'bulkmail'
|
6
9
|
|
7
|
-
$
|
10
|
+
$helo = nil
|
11
|
+
$from = nil
|
8
12
|
$recipients = []
|
9
13
|
$content = nil
|
10
|
-
$headers = {}
|
11
|
-
|
12
|
-
def parse_file(fn)
|
13
|
-
lines = IO.readlines(fn)
|
14
|
-
while line = lines.shift
|
15
|
-
break if line.empty?
|
16
|
-
if line =~ /^(.+):\s?(.*)$/
|
17
|
-
$headers ||= {}
|
18
|
-
$headers[$1] = $2
|
19
|
-
else
|
20
|
-
lines.unshift(line)
|
21
|
-
break
|
22
|
-
end
|
23
|
-
end
|
24
|
-
$content = lines.join
|
25
|
-
end
|
26
14
|
|
27
15
|
opts = OptionParser.new do |opts|
|
28
16
|
opts.banner = "Usage: bulkmail <options>"
|
@@ -30,42 +18,22 @@ opts = OptionParser.new do |opts|
|
|
30
18
|
opts.separator ""
|
31
19
|
opts.separator "Options:"
|
32
20
|
|
33
|
-
opts.on("-h", "--
|
34
|
-
|
35
|
-
end
|
36
|
-
|
37
|
-
opts.on("-u", "--user username", "User name (for authentication)") do |v|
|
38
|
-
BulkMail.server_settings[:user] = v
|
39
|
-
end
|
40
|
-
|
41
|
-
opts.on("-p", "--password password", "Password (for authentication)") do |v|
|
42
|
-
BulkMail.server_settings[:password] = v
|
43
|
-
end
|
44
|
-
|
45
|
-
opts.on("-a", "--authentication type", "Authentication type") do |v|
|
46
|
-
BulkMail.server_settings[:auth] = v
|
21
|
+
opts.on("-h", "--helo HOSTNAME", "HELO host name.") do |v|
|
22
|
+
$helo = v
|
47
23
|
end
|
48
24
|
|
49
25
|
opts.on("-r", "--recipients filename", "Recipients file name") do |v|
|
50
|
-
$recipients = IO.readlines(v).map {|l| l =~ /(.*)(\r\n|\n)
|
26
|
+
$recipients = IO.readlines(v).map {|l| l =~ /(.*)(\r\n|\n)$/ ? $1 : l}.compact
|
51
27
|
end
|
52
28
|
|
53
29
|
opts.on("-c", "--content filename", "Content file name") do |v|
|
54
|
-
|
55
|
-
end
|
56
|
-
|
57
|
-
opts.on("-s", "--subject subject", "Subject line") do |v|
|
58
|
-
$headers[:subject] = v
|
30
|
+
$content = IO.read(v)
|
59
31
|
end
|
60
32
|
|
61
33
|
opts.on("-f", "--from from", "From address") do |v|
|
62
|
-
$
|
34
|
+
$from = v
|
63
35
|
end
|
64
36
|
|
65
|
-
opts.on("-m", "--mime type", "MIME type") do |v|
|
66
|
-
$headers[:content_type] = v
|
67
|
-
end
|
68
|
-
|
69
37
|
# No argument, shows at tail. This will print an options summary.
|
70
38
|
# Try it and see!
|
71
39
|
opts.on_tail("-?", "--help", "Show this message") do
|
@@ -98,5 +66,12 @@ end
|
|
98
66
|
trap('INT') {exit}
|
99
67
|
|
100
68
|
puts "Please hold on..."
|
101
|
-
|
102
|
-
BulkMail.
|
69
|
+
|
70
|
+
sender = BulkMail::Sender.new({
|
71
|
+
:list => $recipients,
|
72
|
+
:from => $from,
|
73
|
+
:message => $content,
|
74
|
+
:helo => $helo
|
75
|
+
|
76
|
+
})
|
77
|
+
sender.start
|
data/doc/rdoc/created.rid
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
Mon Nov 06 23:13:08 +0200 2006
|
data/lib/bulkmail.rb
CHANGED
@@ -1,110 +1,4 @@
|
|
1
|
-
require '
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
# Returns the symbol as a header string
|
6
|
-
def to_header
|
7
|
-
to_s.split('_').map{|w| w.capitalize}.join('-')
|
8
|
-
end
|
9
|
-
end
|
10
|
-
|
11
|
-
class String
|
12
|
-
# Returns self
|
13
|
-
def to_header
|
14
|
-
self
|
15
|
-
end
|
16
|
-
|
17
|
-
# Returns the address part of a recipient
|
18
|
-
def email_addr
|
19
|
-
s = strip
|
20
|
-
return nil if s.empty?
|
21
|
-
s =~ /<(.*)>/ ? $1 : s
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
class Array
|
26
|
-
# Splits the array into smaller arrays of fixed size
|
27
|
-
def split(batch)
|
28
|
-
result = []
|
29
|
-
idx = 0
|
30
|
-
while idx < size
|
31
|
-
result << slice(idx, batch)
|
32
|
-
idx += batch
|
33
|
-
end
|
34
|
-
result
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
class BulkMail
|
39
|
-
@@server_settings = {
|
40
|
-
:host => 'localhost',
|
41
|
-
:port => 25,
|
42
|
-
:helo_domain => nil,
|
43
|
-
:user => nil,
|
44
|
-
:password => nil,
|
45
|
-
:auth => :plain
|
46
|
-
}
|
47
|
-
|
48
|
-
# Returns the server_settings hash
|
49
|
-
def self.server_settings
|
50
|
-
@@server_settings
|
51
|
-
end
|
52
|
-
|
53
|
-
LINE_BREAK = "\r\n"
|
54
|
-
HEADER_FMT = "%s: %s\r\n".freeze
|
55
|
-
|
56
|
-
# Returns the message headers formatted
|
57
|
-
def self.format_headers(headers)
|
58
|
-
headers.inject('') do |m, kv|
|
59
|
-
m << (HEADER_FMT % [kv[0].to_header, kv[1]])
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
# Starts an SMTP session and yields
|
64
|
-
def self.smtp
|
65
|
-
Net::SMTP.start(
|
66
|
-
@@server_settings[:host],
|
67
|
-
@@server_settings[:port],
|
68
|
-
@@server_settings[:helo_domain] || @@server_settings[:host],
|
69
|
-
@@server_settings[:user],
|
70
|
-
@@server_settings[:password],
|
71
|
-
@@server_settings[:auth]) do |smtp|
|
72
|
-
yield smtp
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
# Sends an email
|
77
|
-
def self.send_message(smtp, from, to, body, headers = nil)
|
78
|
-
if headers && !headers.empty?
|
79
|
-
msg = format_headers(headers)
|
80
|
-
else
|
81
|
-
msg = LINE_BREAK
|
82
|
-
end
|
83
|
-
msg << LINE_BREAK
|
84
|
-
msg << body
|
85
|
-
smtp.send_message msg, from, to
|
86
|
-
end
|
87
|
-
|
88
|
-
# Sends bulk mail to a large number of recipients.
|
89
|
-
def self.send_bulk(sender, recipients, body, headers)
|
90
|
-
failed = []
|
91
|
-
succeeded = []
|
92
|
-
headers[:from] = headers[:to] = sender
|
93
|
-
recipients = recipients.map {|r| r.email_addr}.compact
|
94
|
-
recipients.split(10).each do |list|
|
95
|
-
begin
|
96
|
-
smtp {|s| send_message(s, sender.email_addr, list, body, headers)}
|
97
|
-
succeeded.concat list
|
98
|
-
puts "Sent message to #{list.join(', ')}"
|
99
|
-
rescue => e
|
100
|
-
failed.concat list
|
101
|
-
puts "Failed to send to #{list.join(', ')}: #{e.message}\r\n#{e.backtrace.first}"
|
102
|
-
end
|
103
|
-
end
|
104
|
-
# log failed addresses
|
105
|
-
unless failed.empty?
|
106
|
-
File.open('failed.bulkmail', 'w') {|f| failed.each {|l|f.puts l}}
|
107
|
-
end
|
108
|
-
puts "Message to sent to #{succeeded.size} of #{recipients.size} recipients"
|
109
|
-
end
|
110
|
-
end
|
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')
|
@@ -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
|
+
|