mhodgson-ar_mailer 1.4.5

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,85 @@
1
+ = 1.4.4
2
+
3
+ * Exit init.d script with message if no mailers defined.
4
+
5
+ = 1.4.3
6
+
7
+ * Bugs fixed
8
+ * Replaced mistaken call to log when removing pid file artifact for
9
+ non-running daemon
10
+
11
+ = 1.4.2
12
+
13
+ * New Features
14
+ * Added Ruby based linux init.d script for handling daemon startup using yaml
15
+ config file. See files share/linux/ar_sendmail and ar_sendmail.conf
16
+ * Bugs fixed
17
+ * Proper handling for relative and absolute paths for the pid file
18
+ * Removed hoe dependency since we need the explicit gemspec file for github and
19
+ not deploying to RubyForge its not as useful.
20
+ * Moved old BSD rc.d script to share/bsd folder
21
+ * Updated README with github gem install, docs and init script info
22
+
23
+ = 1.4.1
24
+
25
+ * Bugs fixed
26
+ * Daemon failed on startup fixed with expanding full path of pid file
27
+
28
+ = 1.4.0
29
+
30
+ * Forked gem and published on GitHub (gem sources -a http://gems.github.com)
31
+ * New Features
32
+ * Added pid file creation on daemonize with command line option to specify pid filename [Dylan Egan]
33
+
34
+ = 1.3.1
35
+
36
+ * Fix bug #12530, gmail causes SSL errors. Submitted by Kyle Maxwell
37
+ and Alex Ostleitner.
38
+ * Try ActionMailer::Base::server_settings then ::smtp_settings. Fixes
39
+ bug #12516. Submitted by Alex Ostleitner.
40
+
41
+ = 1.3.0
42
+
43
+ * New Features
44
+ * Added automatic mail queue cleanup.
45
+ * MAY CAUSE LOSS OF DATA. If you haven't run ar_sendmail within
46
+ the expiry time, set it to 0.
47
+ * Bugs fixed
48
+ * Authentication errors are now handled by retrying once.
49
+
50
+ = 1.2.0
51
+
52
+ * Bugs fixed
53
+ * Handle SMTPServerBusy by backing off @delay seconds then re-queueing
54
+ * Allow email delivery class to be set in ARMailer.
55
+ * ar_sendmail --mailq works with --table-name now.
56
+ * Miscellaneous Updates
57
+ * Added documentation to require 'action_mailer/ar_mailer' in
58
+ instructions.
59
+ * Moved to ZSS p4 repository
60
+ * Supports TLS now. Requested by Dave Thomas. smtp_tls.rb from Kyle
61
+ Maxwell & etc.
62
+
63
+ = 1.1.0
64
+
65
+ * Features
66
+ * Added --chdir to set rails directory
67
+ * Added --environment to set RAILS_ENV
68
+ * Exits cleanly on TERM or INT signals
69
+ * Added FreeBSD rc.d script
70
+ * Exceptions during SMTP sending are now logged
71
+ * No longer waits if sending email took too long
72
+ * Bugs fixed
73
+ * Fixed last send attempt in --mailq
74
+ * Better SMTP error handling
75
+ * Messages are removed from the queue on 5xx errors
76
+ * Added Net::SMTP.reset to avoid needing to recreate the connection
77
+
78
+ = 1.0.1
79
+
80
+ * Bugs fixed
81
+ * From and to of email destination were swapped
82
+
83
+ = 1.0.0
84
+
85
+ * Birthday
data/LICENSE.txt ADDED
@@ -0,0 +1,28 @@
1
+ Original code copyright 2006, 2007, Eric Hodel, The Robot Co-op. All
2
+ rights reserved. Some code under other license, see individual files
3
+ for details.
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions
7
+ are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright
10
+ notice, this list of conditions and the following disclaimer.
11
+ 2. Redistributions in binary form must reproduce the above copyright
12
+ notice, this list of conditions and the following disclaimer in the
13
+ documentation and/or other materials provided with the distribution.
14
+ 3. Neither the names of the authors nor the names of their contributors
15
+ may be used to endorse or promote products derived from this software
16
+ without specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS
19
+ OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21
+ ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE
22
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
23
+ OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
24
+ OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
25
+ BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
26
+ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
27
+ OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
28
+ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/Manifest.txt ADDED
@@ -0,0 +1,15 @@
1
+ History.txt
2
+ LICENSE.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ bin/ar_sendmail
7
+ lib/action_mailer/ar_mailer.rb
8
+ lib/action_mailer/ar_sendmail.rb
9
+ lib/smtp_tls.rb
10
+ share/bsd/ar_sendmail
11
+ share/linux/ar_sendmail
12
+ share/linux/ar_sendmail.conf
13
+ test/action_mailer.rb
14
+ test/test_armailer.rb
15
+ test/test_arsendmail.rb
data/README.txt ADDED
@@ -0,0 +1,55 @@
1
+ = ar_mailer
2
+
3
+ A two-phase delivery agent for ActionMailer
4
+
5
+ Rubyforge Project:
6
+
7
+ http://rubyforge.org/projects/seattlerb
8
+
9
+ Documentation:
10
+
11
+ http://seattlerb.org/ar_mailer
12
+
13
+ and for forked additions
14
+
15
+ http://github.com/mhodgson/ar_mailer/wikis
16
+
17
+ Bugs:
18
+
19
+ http://rubyforge.org/tracker/?func=add&group_id=1513&atid=5921
20
+
21
+ == About
22
+
23
+ Even delivering email to the local machine may take too long when you have to
24
+ send hundreds of messages. ar_mailer allows you to store messages into the
25
+ database for later delivery by a separate process, ar_sendmail.
26
+
27
+ == Installing ar_mailer (forked)
28
+
29
+ Install the gem from GitHub gems server:
30
+
31
+ First, if you haven't already
32
+
33
+ $ sudo gem sources -a http://gems.github.com
34
+
35
+ Then
36
+
37
+ $ sudo gem install adzap-ar_mailer
38
+
39
+ See ActionMailer::ARMailer for instructions on converting to ARMailer.
40
+
41
+ See ar_sendmail -h for options to ar_sendmail.
42
+
43
+ NOTE: You may need to delete an smtp_tls.rb file if you have one lying
44
+ around. ar_mailer supplies it own.
45
+
46
+ === init.d/rc.d scripts
47
+
48
+ For Linux both script and demo config files are in share/linux.
49
+ See ar_sendmail.conf for setting up your config. Copy the ar_sendmail file
50
+ to /etc/init.d/ and make it executable. Then for Debian based distros run
51
+ 'sudo update-rc.d ar_sendmail defaults' and it should work. Make sure you have
52
+ the config file /etc/ar_sendmail.conf in place before starting.
53
+
54
+ For FreeBSD or NetBSD script is share/bsd/ar_sendmail. This is old and does not
55
+ support the config file unless someone wants to submit a patch.
data/Rakefile ADDED
@@ -0,0 +1,60 @@
1
+ require 'rubygems'
2
+ require 'rake/gempackagetask'
3
+ require 'rake/testtask'
4
+ require 'rake/rdoctask'
5
+
6
+ $:.unshift(File.expand_path(File.dirname(__FILE__) + '/lib'))
7
+
8
+ require './lib/action_mailer/ar_sendmail'
9
+
10
+ ar_mailer_gemspec = Gem::Specification.new do |s|
11
+ s.name = %q{ar_mailer}
12
+ s.version = ActionMailer::ARSendmail::VERSION
13
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
14
+ s.authors = ["Eric Hodel"]
15
+ s.date = %q{2008-07-04}
16
+ s.default_executable = %q{ar_sendmail}
17
+ s.description = %q{Even delivering email to the local machine may take too long when you have to send hundreds of messages. ar_mailer allows you to store messages into the database for later delivery by a separate process, ar_sendmail.}
18
+ s.email = %q{drbrain@segment7.net}
19
+ s.executables = ["ar_sendmail"]
20
+ s.extra_rdoc_files = ["History.txt", "LICENSE.txt", "Manifest.txt", "README.txt"]
21
+ s.files = ["History.txt", "LICENSE.txt", "Manifest.txt", "README.txt", "Rakefile", "bin/ar_sendmail", "lib/action_mailer/ar_mailer.rb", "lib/action_mailer/ar_sendmail.rb", "lib/smtp_tls.rb", "share/bsd/ar_sendmail", "share/linux/ar_sendmail", "share/linux/ar_sendmail.conf", "test/action_mailer.rb", "test/test_armailer.rb", "test/test_arsendmail.rb"]
22
+ s.has_rdoc = true
23
+ s.homepage = %q{http://seattlerb.org/ar_mailer}
24
+ s.rdoc_options = ["--main", "README.txt"]
25
+ s.require_paths = ["lib"]
26
+ s.rubyforge_project = %q{seattlerb}
27
+ s.rubygems_version = %q{1.2.0}
28
+ s.summary = %q{A two-phase delivery agent for ActionMailer}
29
+ s.test_files = ["test/test_armailer.rb", "test/test_arsendmail.rb"]
30
+ end
31
+
32
+ Rake::GemPackageTask.new(ar_mailer_gemspec) do |pkg|
33
+ pkg.gem_spec = ar_mailer_gemspec
34
+ end
35
+
36
+ namespace :gem do
37
+ namespace :spec do
38
+ desc "Update ar_mailer.gemspec"
39
+ task :generate do
40
+ File.open("ar_mailer.gemspec", "w") do |f|
41
+ f.puts(ar_mailer_gemspec.to_ruby)
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ desc "Build packages and install"
48
+ task :install => :package do
49
+ sh %{sudo gem install --local pkg/ar_mailer-#{ActionMailer::ARSendmail::VERSION}}
50
+ end
51
+
52
+ desc 'Default: run unit tests.'
53
+ task :default => :test
54
+
55
+ desc 'Test the ar_mailer gem.'
56
+ Rake::TestTask.new(:test) do |t|
57
+ t.libs << 'lib' << 'test'
58
+ t.pattern = 'test/**/test_*.rb'
59
+ t.verbose = true
60
+ end
data/bin/ar_sendmail ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'action_mailer/ar_sendmail'
4
+
5
+ ActionMailer::ARSendmail.run
6
+
@@ -0,0 +1,98 @@
1
+ require 'action_mailer'
2
+
3
+ ##
4
+ # Adds sending email through an ActiveRecord table as a delivery method for
5
+ # ActionMailer.
6
+ #
7
+ # == Converting to ActionMailer::ARMailer
8
+ #
9
+ # Go to your Rails project:
10
+ #
11
+ # $ cd your_rails_project
12
+ #
13
+ # Create a new migration:
14
+ #
15
+ # $ ar_sendmail --create-migration
16
+ #
17
+ # You'll need to redirect this into a file. If you want a different name
18
+ # provide the --table-name option.
19
+ #
20
+ # Create a new model:
21
+ #
22
+ # $ ar_sendmail --create-model
23
+ #
24
+ # You'll need to redirect this into a file. If you want a different name
25
+ # provide the --table-name option.
26
+ #
27
+ # Change your email classes to inherit from ActionMailer::ARMailer instead of
28
+ # ActionMailer::Base:
29
+ #
30
+ # --- app/model/emailer.rb.orig 2006-08-10 13:16:33.000000000 -0700
31
+ # +++ app/model/emailer.rb 2006-08-10 13:16:43.000000000 -0700
32
+ # @@ -1,4 +1,4 @@
33
+ # -class Emailer < ActionMailer::Base
34
+ # +class Emailer < ActionMailer::ARMailer
35
+ #
36
+ # def comment_notification(comment)
37
+ # from comment.author.email
38
+ #
39
+ # You'll need to be sure to set the From address for your emails. Something
40
+ # like:
41
+ #
42
+ # def list_send(recipient)
43
+ # from 'no_reply@example.com'
44
+ # # ...
45
+ #
46
+ # Edit config/environment.rb and require ar_mailer.rb:
47
+ #
48
+ # require 'action_mailer/ar_mailer'
49
+ #
50
+ # Edit config/environments/production.rb and set the delivery agent:
51
+ #
52
+ # $ grep delivery_method config/environments/production.rb
53
+ # ActionMailer::Base.delivery_method = :activerecord
54
+ #
55
+ # Run ar_sendmail:
56
+ #
57
+ # $ ar_sendmail
58
+ #
59
+ # You can also run it from cron with -o, or as a daemon with -d.
60
+ #
61
+ # See <tt>ar_sendmail -h</tt> for full details.
62
+ #
63
+ # == Alternate Mail Storage
64
+ #
65
+ # If you want to set the ActiveRecord model that emails will be stored in,
66
+ # see ActionMailer::ARMailer::email_class=
67
+
68
+ class ActionMailer::ARMailer < ActionMailer::Base
69
+
70
+ @@email_class = Email
71
+
72
+ ##
73
+ # Current email class for deliveries.
74
+
75
+ def self.email_class
76
+ @@email_class
77
+ end
78
+
79
+ ##
80
+ # Sets the email class for deliveries.
81
+
82
+ def self.email_class=(klass)
83
+ @@email_class = klass
84
+ end
85
+
86
+ ##
87
+ # Adds +mail+ to the Email table. Only the first From address for +mail+ is
88
+ # used.
89
+
90
+ def perform_delivery_activerecord(mail)
91
+ mail.destinations.each do |destination|
92
+ @@email_class.create :mail => mail.encoded, :to => destination,
93
+ :from => mail.from.first, :message_id => mail.message_id
94
+ end
95
+ end
96
+
97
+ end
98
+
@@ -0,0 +1,648 @@
1
+ require 'optparse'
2
+ require 'net/smtp'
3
+ require 'net/imap'
4
+ require 'smtp_tls'
5
+ require 'rubygems'
6
+
7
+ class Object # :nodoc:
8
+ unless respond_to? :path2class then
9
+ def self.path2class(path)
10
+ path.split(/::/).inject self do |k,n| k.const_get n end
11
+ end
12
+ end
13
+ end
14
+
15
+ ##
16
+ # Hack in RSET
17
+
18
+ module Net # :nodoc:
19
+ class SMTP # :nodoc:
20
+
21
+ unless instance_methods.include? 'reset' then
22
+ ##
23
+ # Resets the SMTP connection.
24
+
25
+ def reset
26
+ getok 'RSET'
27
+ end
28
+ end
29
+
30
+ end
31
+ end
32
+
33
+ module ActionMailer; end # :nodoc:
34
+
35
+ ##
36
+ # ActionMailer::ARSendmail delivers email from the email table to the
37
+ # SMTP server configured in your application's config/environment.rb.
38
+ # ar_sendmail does not work with sendmail delivery.
39
+ #
40
+ # ar_mailer can deliver to SMTP with TLS using smtp_tls.rb borrowed from Kyle
41
+ # Maxwell's action_mailer_optional_tls plugin. Simply set the :tls option in
42
+ # ActionMailer::Base's smtp_settings to true to enable TLS.
43
+ #
44
+ # See ar_sendmail -h for the full list of supported options.
45
+ #
46
+ # The interesting options are:
47
+ # * --daemon
48
+ # * --mailq
49
+ # * --create-migration
50
+ # * --create-model
51
+ # * --table-name
52
+
53
+ class ActionMailer::ARSendmail
54
+
55
+ ##
56
+ # The version of ActionMailer::ARSendmail you are running.
57
+
58
+ VERSION = '1.4.3'
59
+
60
+ ##
61
+ # Maximum number of times authentication will be consecutively retried
62
+
63
+ MAX_AUTH_FAILURES = 2
64
+
65
+ ##
66
+ # Email delivery attempts per run
67
+
68
+ attr_accessor :batch_size
69
+
70
+ ##
71
+ # Seconds to delay between runs
72
+
73
+ attr_accessor :delay
74
+
75
+ ##
76
+ # Maximum age of emails in seconds before they are removed from the queue.
77
+
78
+ attr_accessor :max_age
79
+
80
+ ##
81
+ # Be verbose
82
+
83
+ attr_accessor :verbose
84
+
85
+ ##
86
+ # ActiveRecord class that holds emails
87
+
88
+ attr_reader :email_class
89
+
90
+ ##
91
+ # True if only one delivery attempt will be made per call to run
92
+
93
+ attr_reader :once
94
+
95
+ ##
96
+ # Times authentication has failed
97
+
98
+ attr_accessor :failed_auth_count
99
+
100
+ @@pid_file = nil
101
+
102
+ def self.remove_pid_file
103
+ if @@pid_file
104
+ require 'shell'
105
+ sh = Shell.new
106
+ sh.rm @@pid_file
107
+ end
108
+ end
109
+
110
+ ##
111
+ # Creates a new migration using +table_name+ and prints it on stdout.
112
+
113
+ def self.create_migration(table_name)
114
+ require 'active_support'
115
+ puts <<-EOF
116
+ class Add#{table_name.classify} < ActiveRecord::Migration
117
+ def self.up
118
+ create_table :#{table_name.tableize} do |t|
119
+ t.column :from, :string
120
+ t.column :to, :string
121
+ t.column :last_send_attempt, :integer, :default => 0
122
+ t.column :mail, :text
123
+ t.column :created_on, :datetime
124
+ t.column :sent, :boolean, :default => false
125
+ t.column :status, :string
126
+ t.column :message_id, :string
127
+ end
128
+ end
129
+
130
+ def self.down
131
+ drop_table :#{table_name.tableize}
132
+ end
133
+ end
134
+ EOF
135
+ end
136
+
137
+ ##
138
+ # Creates a new model using +table_name+ and prints it on stdout.
139
+
140
+ def self.create_model(table_name)
141
+ require 'active_support'
142
+ puts <<-EOF
143
+ class #{table_name.classify} < ActiveRecord::Base
144
+ end
145
+ EOF
146
+ end
147
+
148
+ ##
149
+ # Prints a list of unsent emails and the last delivery attempt, if any.
150
+ #
151
+ # If ActiveRecord::Timestamp is not being used the arrival time will not be
152
+ # known. See http://api.rubyonrails.org/classes/ActiveRecord/Timestamp.html
153
+ # to learn how to enable ActiveRecord::Timestamp.
154
+
155
+ def self.mailq(table_name)
156
+ klass = table_name.split('::').inject(Object) { |k,n| k.const_get n }
157
+ emails = klass.find :all
158
+
159
+ if emails.empty? then
160
+ puts "Mail queue is empty"
161
+ return
162
+ end
163
+
164
+ total_size = 0
165
+
166
+ puts "-Queue ID- --Size-- ----Arrival Time---- -Sender/Recipient-------"
167
+ emails.each do |email|
168
+ size = email.mail.length
169
+ total_size += size
170
+
171
+ create_timestamp = email.created_on rescue
172
+ email.created_at rescue
173
+ Time.at(email.created_date) rescue # for Robot Co-op
174
+ nil
175
+
176
+ created = if create_timestamp.nil? then
177
+ ' Unknown'
178
+ else
179
+ create_timestamp.strftime '%a %b %d %H:%M:%S'
180
+ end
181
+
182
+ puts "%10d %8d %s %s" % [email.id, size, created, email.from]
183
+ if email.last_send_attempt > 0 then
184
+ puts "Last send attempt: #{Time.at email.last_send_attempt}"
185
+ end
186
+ puts " #{email.to}"
187
+ puts
188
+ end
189
+
190
+ puts "-- #{total_size/1024} Kbytes in #{emails.length} Requests."
191
+ end
192
+
193
+ ##
194
+ # Processes command line options in +args+
195
+
196
+ def self.process_args(args)
197
+ name = File.basename $0
198
+
199
+ options = {}
200
+ options[:Chdir] = '.'
201
+ options[:Daemon] = false
202
+ options[:Delay] = 60
203
+ options[:MaxAge] = 86400 * 7
204
+ options[:Once] = false
205
+ options[:RailsEnv] = ENV['RAILS_ENV']
206
+ options[:TableName] = 'Email'\
207
+ options[:Imap] = ''
208
+ options[:Port] = 993
209
+ options[:Login] = ''
210
+ options[:Password] = ''
211
+ options[:Pidfile] = options[:Chdir] + '/log/ar_sendmail.pid'
212
+
213
+ opts = OptionParser.new do |opts|
214
+ opts.banner = "Usage: #{name} [options]"
215
+ opts.separator ''
216
+
217
+ opts.separator "#{name} scans the email table for new messages and sends them to the"
218
+ opts.separator "website's configured SMTP host."
219
+ opts.separator ''
220
+ opts.separator "#{name} must be run from a Rails application's root."
221
+
222
+ opts.separator ''
223
+ opts.separator 'Sendmail options:'
224
+
225
+ opts.on("-b", "--batch-size BATCH_SIZE",
226
+ "Maximum number of emails to send per delay",
227
+ "Default: Deliver all available emails", Integer) do |batch_size|
228
+ options[:BatchSize] = batch_size
229
+ end
230
+
231
+ opts.on( "--delay DELAY",
232
+ "Delay between checks for new mail",
233
+ "in the database",
234
+ "Default: #{options[:Delay]}", Integer) do |delay|
235
+ options[:Delay] = delay
236
+ end
237
+
238
+ opts.on( "--max-age MAX_AGE",
239
+ "Maxmimum age for an email. After this",
240
+ "it will be removed from the queue.",
241
+ "Set to 0 to disable queue cleanup.",
242
+ "Default: #{options[:MaxAge]} seconds", Integer) do |max_age|
243
+ options[:MaxAge] = max_age
244
+ end
245
+
246
+ opts.on("-o", "--once",
247
+ "Only check for new mail and deliver once",
248
+ "Default: #{options[:Once]}") do |once|
249
+ options[:Once] = once
250
+ end
251
+
252
+ opts.on("-d", "--daemonize",
253
+ "Run as a daemon process",
254
+ "Default: #{options[:Daemon]}") do |daemon|
255
+ options[:Daemon] = true
256
+ end
257
+
258
+ opts.on("-p", "--pidfile PIDFILE",
259
+ "Set the pidfile location",
260
+ "Default: #{options[:Chdir]}#{options[:Pidfile]}", String) do |pidfile|
261
+ options[:Pidfile] = pidfile
262
+ end
263
+
264
+ opts.on( "--mailq",
265
+ "Display a list of emails waiting to be sent") do |mailq|
266
+ options[:MailQ] = true
267
+ end
268
+
269
+ opts.separator ''
270
+ opts.separator 'Setup Options:'
271
+
272
+ opts.on( "--create-migration",
273
+ "Prints a migration to add an Email table",
274
+ "to stdout") do |create|
275
+ options[:Migrate] = true
276
+ end
277
+
278
+ opts.on( "--create-model",
279
+ "Prints a model for an Email ActiveRecord",
280
+ "object to stdout") do |create|
281
+ options[:Model] = true
282
+ end
283
+
284
+ opts.separator ''
285
+ opts.separator 'Generic Options:'
286
+
287
+ opts.on("-c", "--chdir PATH",
288
+ "Use PATH for the application path",
289
+ "Default: #{options[:Chdir]}") do |path|
290
+ usage opts, "#{path} is not a directory" unless File.directory? path
291
+ usage opts, "#{path} is not readable" unless File.readable? path
292
+ options[:Chdir] = path
293
+ end
294
+
295
+ opts.on("-e", "--environment RAILS_ENV",
296
+ "Set the RAILS_ENV constant",
297
+ "Default: #{options[:RailsEnv]}") do |env|
298
+ options[:RailsEnv] = env
299
+ end
300
+
301
+ opts.on("-t", "--table-name TABLE_NAME",
302
+ "Name of table holding emails",
303
+ "Used for both sendmail and",
304
+ "migration creation",
305
+ "Default: #{options[:TableName]}") do |name|
306
+ options[:TableName] = name
307
+ end
308
+
309
+ opts.on("-v", "--[no-]verbose",
310
+ "Be verbose",
311
+ "Default: #{options[:Verbose]}") do |verbose|
312
+ options[:Verbose] = verbose
313
+ end
314
+
315
+ opts.on("-i", "--imap",
316
+ "Imap server used to check for bounces",
317
+ "Default: none") do |name|
318
+ options[:Imap] = name
319
+ end
320
+
321
+ opts.on("-l", "--login",
322
+ "login name to check for bounces",
323
+ "Default: none") do |name|
324
+ options[:Login] = name
325
+ end
326
+
327
+ opts.on( "--password",
328
+ "password name to check for bounces",
329
+ "Default: none") do |name|
330
+ options[:Password] = name
331
+ end
332
+
333
+ opts.on( "--port",
334
+ "port to check for bounces",
335
+ "Default: #{options[:Port]}") do |name|
336
+ options[:Port] = name
337
+ end
338
+
339
+ opts.on("-h", "--help",
340
+ "You're looking at it") do
341
+ usage opts
342
+ end
343
+
344
+ opts.separator ''
345
+ end
346
+
347
+ opts.parse! args
348
+
349
+ return options if options.include? :Migrate or options.include? :Model
350
+
351
+ ENV['RAILS_ENV'] = options[:RailsEnv]
352
+
353
+ Dir.chdir options[:Chdir] do
354
+ begin
355
+ require 'config/environment'
356
+ rescue LoadError
357
+ usage opts, <<-EOF
358
+ #{name} must be run from a Rails application's root to deliver email.
359
+ #{Dir.pwd} does not appear to be a Rails application root.
360
+ EOF
361
+ end
362
+ end
363
+
364
+ return options
365
+ end
366
+
367
+ ##
368
+ # Processes +args+ and runs as appropriate
369
+
370
+ def self.run(args = ARGV)
371
+ options = process_args args
372
+
373
+ if options.include? :Migrate then
374
+ create_migration options[:TableName]
375
+ exit
376
+ elsif options.include? :Model then
377
+ create_model options[:TableName]
378
+ exit
379
+ elsif options.include? :MailQ then
380
+ mailq options[:TableName]
381
+ exit
382
+ end
383
+
384
+ if options[:Daemon] then
385
+ require 'webrick/server'
386
+ @@pid_file = File.expand_path(options[:Pidfile], options[:Chdir])
387
+ if File.exists? @@pid_file
388
+ # check to see if process is actually running
389
+ pid = ''
390
+ File.open(@@pid_file, 'r') {|f| pid = f.read.chomp }
391
+ if system("ps -p #{pid} | grep #{pid}") # returns true if process is running, o.w. false
392
+ $stderr.puts "Warning: The pid file #{@@pid_file} exists and ar_sendmail is running. Shutting down."
393
+ exit
394
+ else
395
+ # not running, so remove existing pid file and continue
396
+ self.remove_pid_file
397
+ $stderr.puts "ar_sendmail is not running. Removing existing pid file and starting up..."
398
+ end
399
+ end
400
+ WEBrick::Daemon.start
401
+ File.open(@@pid_file, 'w') {|f| f.write("#{Process.pid}\n")}
402
+ end
403
+
404
+ new(options).run
405
+
406
+ rescue SystemExit
407
+ raise
408
+ rescue SignalException
409
+ exit
410
+ rescue Exception => e
411
+ $stderr.puts "Unhandled exception #{e.message}(#{e.class}):"
412
+ $stderr.puts "\t#{e.backtrace.join "\n\t"}"
413
+ exit 1
414
+ end
415
+
416
+ ##
417
+ # Prints a usage message to $stderr using +opts+ and exits
418
+
419
+ def self.usage(opts, message = nil)
420
+ if message then
421
+ $stderr.puts message
422
+ $stderr.puts
423
+ end
424
+
425
+ $stderr.puts opts
426
+ exit 1
427
+ end
428
+
429
+ ##
430
+ # Creates a new ARSendmail.
431
+ #
432
+ # Valid options are:
433
+ # <tt>:BatchSize</tt>:: Maximum number of emails to send per delay
434
+ # <tt>:Delay</tt>:: Delay between deliver attempts
435
+ # <tt>:TableName</tt>:: Table name that stores the emails
436
+ # <tt>:Once</tt>:: Only attempt to deliver emails once when run is called
437
+ # <tt>:Verbose</tt>:: Be verbose.
438
+
439
+ def initialize(options = {})
440
+ options[:Delay] ||= 60
441
+ options[:TableName] ||= 'Email'
442
+ options[:MaxAge] ||= 86400 * 7
443
+
444
+ @batch_size = options[:BatchSize]
445
+ @delay = options[:Delay]
446
+ @email_class = Object.path2class options[:TableName]\
447
+ @once = options[:Once]
448
+ @verbose = options[:Verbose]
449
+ @max_age = options[:MaxAge]
450
+ @imap = options[:Imap]
451
+ @port = options[:Port]
452
+ @login = options[:Login]
453
+ @password = options[:Password]
454
+
455
+ @failed_auth_count = 0
456
+ end
457
+
458
+ ##
459
+ # Removes emails that have lived in the queue for too long. If max_age is
460
+ # set to 0, no emails will be removed.
461
+
462
+ def cleanup
463
+ return if @max_age == 0
464
+ timeout = Time.now - @max_age
465
+ conditions = ["last_send_attempt > 0 and created_on < ? AND status <> 'Failure'", timeout]
466
+ mail = @email_class.destroy_all conditions
467
+
468
+ log "expired #{mail.length} emails from the queue"
469
+ end
470
+
471
+ ##
472
+ # Delivers +emails+ to ActionMailer's SMTP server and destroys them.
473
+
474
+ def deliver(emails)
475
+ user = smtp_settings[:user] || smtp_settings[:user_name]
476
+ Net::SMTP.start smtp_settings[:address], smtp_settings[:port],
477
+ smtp_settings[:domain], user,
478
+ smtp_settings[:password],
479
+ smtp_settings[:authentication],
480
+ smtp_settings[:tls] do |smtp|
481
+ @failed_auth_count = 0
482
+ until emails.empty? do
483
+ email = emails.shift
484
+ begin
485
+ res = smtp.send_message email.mail, email.from, email.to
486
+ email.update_attribute(:sent, true)
487
+ log "sent email %011d from %s to %s: %p" %
488
+ [email.id, email.from, email.to, res]
489
+ rescue Net::SMTPFatalError => e
490
+ log "5xx error sending email %d, removing from queue: %p(%s):\n\t%s" %
491
+ [email.id, e.message, e.class, e.backtrace.join("\n\t")]
492
+ email.destroy
493
+ smtp.reset
494
+ rescue Net::SMTPServerBusy => e
495
+ log "server too busy, sleeping #{@delay} seconds"
496
+ sleep delay
497
+ return
498
+ rescue Net::SMTPUnknownError, Net::SMTPSyntaxError, TimeoutError => e
499
+ email.last_send_attempt = Time.now.to_i
500
+ email.save rescue nil
501
+ log "error sending email %d: %p(%s):\n\t%s" %
502
+ [email.id, e.message, e.class, e.backtrace.join("\n\t")]
503
+ smtp.reset
504
+ end
505
+ end
506
+ end
507
+ rescue Net::SMTPAuthenticationError => e
508
+ @failed_auth_count += 1
509
+ if @failed_auth_count >= MAX_AUTH_FAILURES then
510
+ log "authentication error, giving up: #{e.message}"
511
+ raise e
512
+ else
513
+ log "authentication error, retrying: #{e.message}"
514
+ end
515
+ sleep delay
516
+ rescue Net::SMTPServerBusy, SystemCallError, OpenSSL::SSL::SSLError
517
+ # ignore SMTPServerBusy/EPIPE/ECONNRESET from Net::SMTP.start's ensure
518
+ end
519
+
520
+ def check_bounces
521
+ imap = Net::IMAP.new(@imap, @port, true)
522
+ imap.login(@login, @password)
523
+ imap.select('INBOX')
524
+ imap.search(['ALL']).each do |message_id|
525
+ msg = imap.fetch(message_id,'RFC822')[0].attr['RFC822']
526
+
527
+ receive(msg)
528
+ #Mark message as deleted and it will be removed from storage when user session closed
529
+ imap.store(message_id, "+FLAGS", [:Deleted])
530
+ end
531
+ # tell server to permanently remove all messages flagged as :Deleted
532
+ imap.expunge()
533
+ imap.logout()
534
+ imap.disconnect()
535
+ end
536
+
537
+ def receive(email)
538
+ bounce = BouncedDelivery.from_email(email)
539
+ msg = @email_class.find_by_message_id(bounce.original_message_id)
540
+ msg.update_attribute(:status, bounce.status)
541
+ end
542
+ ##
543
+ # Prepares ar_sendmail for exiting
544
+
545
+ def do_exit
546
+ log "caught signal, shutting down"
547
+ self.class.remove_pid_file
548
+ exit
549
+ end
550
+
551
+ ##
552
+ # Returns emails in email_class that haven't had a delivery attempt in the
553
+ # last 300 seconds.
554
+
555
+ def find_emails
556
+ options = { :conditions => ['last_send_attempt < ? AND sent IS FALSE', Time.now.to_i - 300] }
557
+ options[:limit] = batch_size unless batch_size.nil?
558
+ mail = @email_class.find :all, options
559
+
560
+ log "found #{mail.length} emails to send"
561
+ mail
562
+ end
563
+
564
+ ##
565
+ # Installs signal handlers to gracefully exit.
566
+
567
+ def install_signal_handlers
568
+ trap 'TERM' do do_exit end
569
+ trap 'INT' do do_exit end
570
+ end
571
+
572
+ ##
573
+ # Logs +message+ if verbose
574
+
575
+ def log(message)
576
+ $stderr.puts message if @verbose
577
+ ActionMailer::Base.logger.info "ar_sendmail: #{message}"
578
+ end
579
+
580
+ ##
581
+ # Scans for emails and delivers them every delay seconds. Only returns if
582
+ # once is true.
583
+
584
+ def run
585
+ install_signal_handlers
586
+
587
+ loop do
588
+ now = Time.now
589
+ begin
590
+ cleanup
591
+ deliver find_emails
592
+ check_bounces
593
+ rescue ActiveRecord::Transactions::TransactionError
594
+ end
595
+ break if @once
596
+ sleep @delay if now + @delay > Time.now
597
+ end
598
+ end
599
+
600
+ ##
601
+ # Proxy to ActionMailer::Base::smtp_settings. See
602
+ # http://api.rubyonrails.org/classes/ActionMailer/Base.html
603
+ # for instructions on how to configure ActionMailer's SMTP server.
604
+ #
605
+ # Falls back to ::server_settings if ::smtp_settings doesn't exist for
606
+ # backwards compatibility.
607
+
608
+ def smtp_settings
609
+ ActionMailer::Base.smtp_settings rescue ActionMailer::Base.server_settings
610
+ end
611
+
612
+ end
613
+
614
+ class BouncedDelivery
615
+
616
+ attr_accessor :status_info, :original_message_id
617
+
618
+ def self.from_email(email)
619
+ returning(bounce = self.new) do
620
+ status_part = email.parts.detect do |part|
621
+ part.content_type == "message/delivery-status"
622
+ end
623
+ statuses = status_part.body.split(/\n/)
624
+ bounce.status_info = statuses.inject({}) do |hash, line|
625
+ key, value = line.split(/:/)
626
+ hash[key] = value.strip rescue nil
627
+ hash
628
+ end
629
+ original_message_part = email.parts.detect do |part|
630
+ part.content_type == "message/rfc822"
631
+ end
632
+ parsed_msg = TMail::Mail.parse(original_message_part.body)
633
+ bounce.original_message_id = parsed_msg.message_id
634
+ end
635
+ end
636
+
637
+ def status
638
+ case status_info['Status']
639
+ when /^5/
640
+ 'Failure'
641
+ when /^4/
642
+ 'Temporary Failure'
643
+ when /^2/
644
+ 'Success'
645
+ end
646
+ end
647
+ end
648
+