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 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
- = about bulkmail
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. I wrote it to be as simple as possible, and as a basic alternative to commercial bulk-mailers. The free ones suck.
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
- [--host, -h] smtp hostname (if none is specified localhost is presumed)
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 can also include headers in the content file, provided they are followed by a blank line. For example:
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.send_bulk(sender, recipients, content, headers)
32
-
33
- Where:
34
- [sender] is the sender address. This address will also be used as in the form and to headers.
35
- [recipients] is an array of email addresses
36
- [content] is the message content
37
- [headers] is an array containing message headers.
38
-
39
- You can change server settings by using BulkMail.server_settings:
40
-
41
- BulkMail.server_settings[:host] = 'mail.example.com'
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.2"
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/*.rb']
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
- $sender = nil
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", "--host HOSTNAME", "SMPT host name. Default is localhost.") do |v|
34
- BulkMail.server_settings[:host] = v
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)$/; $1}
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
- parse_file(v)
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
- $sender = v
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
- $sender ||= $headers['From']
102
- BulkMail.send_bulk($sender, $recipients, $content, $headers)
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
- Fri Oct 27 11:38:40 +0200 2006
1
+ Mon Nov 06 23:13:08 +0200 2006
data/lib/bulkmail.rb CHANGED
@@ -1,110 +1,4 @@
1
- require 'net/smtp'
2
-
3
- # Symbol extensions
4
- class Symbol
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
+