ar_mailer_revised 0.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +16 -0
  3. data/README.md +54 -70
  4. data/ar_mailer_revised.gemspec +7 -2
  5. data/bin/ar_sendmail +2 -7
  6. data/lib/action_mailer/ar_mailer.rb +45 -18
  7. data/lib/ar_mailer_revised.rb +3 -2
  8. data/lib/ar_mailer_revised/email_scaffold.rb +17 -11
  9. data/lib/ar_mailer_revised/helpers/command_line.rb +198 -0
  10. data/lib/ar_mailer_revised/helpers/general.rb +81 -0
  11. data/lib/ar_mailer_revised/mailman.rb +263 -0
  12. data/lib/ar_mailer_revised/version.rb +1 -1
  13. data/lib/generators/ar_mailer_revised/install_generator.rb +37 -0
  14. data/{generators → lib/generators}/ar_mailer_revised/templates/migration.rb +0 -0
  15. data/{generators → lib/generators}/ar_mailer_revised/templates/model.rb +1 -0
  16. data/test/dummy/README.rdoc +28 -0
  17. data/test/dummy/Rakefile +6 -0
  18. data/test/dummy/app/assets/images/.keep +0 -0
  19. data/test/dummy/app/assets/javascripts/application.js +13 -0
  20. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  21. data/test/dummy/app/controllers/application_controller.rb +5 -0
  22. data/test/dummy/app/controllers/concerns/.keep +0 -0
  23. data/test/dummy/app/helpers/application_helper.rb +2 -0
  24. data/test/dummy/app/mailers/.keep +0 -0
  25. data/test/dummy/app/mailers/test_mailer.rb +32 -0
  26. data/test/dummy/app/models/.keep +0 -0
  27. data/test/dummy/app/models/concerns/.keep +0 -0
  28. data/test/dummy/app/models/email.rb +31 -0
  29. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  30. data/test/dummy/bin/bundle +3 -0
  31. data/test/dummy/bin/rails +4 -0
  32. data/test/dummy/bin/rake +4 -0
  33. data/test/dummy/config.ru +4 -0
  34. data/test/dummy/config/application.rb +23 -0
  35. data/test/dummy/config/boot.rb +5 -0
  36. data/test/dummy/config/database.yml +25 -0
  37. data/test/dummy/config/environment.rb +5 -0
  38. data/test/dummy/config/environments/development.rb +37 -0
  39. data/test/dummy/config/environments/production.rb +83 -0
  40. data/test/dummy/config/environments/test.rb +34 -0
  41. data/{generators/ar_mailer_revised/templates/initializer.rb → test/dummy/config/initializers/ar_mailer_revised.rb} +2 -2
  42. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  43. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  44. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  45. data/test/dummy/config/initializers/inflections.rb +16 -0
  46. data/test/dummy/config/initializers/mime_types.rb +4 -0
  47. data/test/dummy/config/initializers/session_store.rb +3 -0
  48. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  49. data/test/dummy/config/locales/en.yml +23 -0
  50. data/test/dummy/config/routes.rb +56 -0
  51. data/test/dummy/config/secrets.yml +22 -0
  52. data/test/dummy/db/development.sqlite3 +0 -0
  53. data/test/dummy/db/migrate/20140518140150_create_emails.rb +35 -0
  54. data/test/dummy/db/schema.rb +28 -0
  55. data/test/dummy/db/test.sqlite3 +0 -0
  56. data/test/dummy/lib/assets/.keep +0 -0
  57. data/test/dummy/log/.keep +0 -0
  58. data/test/dummy/log/development.log +119 -0
  59. data/test/dummy/public/404.html +67 -0
  60. data/test/dummy/public/422.html +67 -0
  61. data/test/dummy/public/500.html +66 -0
  62. data/test/dummy/public/favicon.ico +0 -0
  63. data/test/dummy/test/mailers/previews/test_mailer_preview.rb +4 -0
  64. data/test/dummy/test/mailers/test_mailer_test.rb +54 -0
  65. data/test/generators/install_generator_test.rb +14 -0
  66. metadata +184 -12
  67. data/generators/ar_mailer_revised/ar_mailer_revised_generator.rb +0 -23
@@ -0,0 +1,81 @@
1
+ #
2
+ # This module contains helper functionality for the Mailman class
3
+ # that handles the actual email sending as a batch process.
4
+ #
5
+ # @author Stefan Exner <stex@sterex.de>
6
+ #
7
+
8
+ require 'log4r'
9
+
10
+ module ArMailerRevised
11
+ module Helpers
12
+ module General
13
+ #
14
+ # Generates a logger object using Log4r
15
+ # The output file is determined by +#log_file+
16
+ #
17
+ # If the custom log file path is set to +stdout+ or +stderr+,
18
+ # these are used instead of a log file.
19
+ #
20
+ # @return [Log4r::Logger] the file output logger
21
+ #
22
+ def logger
23
+ unless @logger
24
+ @logger = Log4r::Logger.new 'ar_mailer'
25
+
26
+ if %w[stdout stderr].include?(@options[:log_file])
27
+ outputter = Log4r::Outputter.send(@options[:log_file])
28
+ else
29
+ outputter = Log4r::FileOutputter.new('ar_mailer_log', :filename => log_file)
30
+ end
31
+
32
+ outputter.formatter = Log4r::PatternFormatter.new(:pattern => '[%5l - %c] %d :: %m')
33
+ @logger.outputters = outputter
34
+
35
+ @logger.level = log_level
36
+ end
37
+ @logger
38
+ end
39
+
40
+ #
41
+ # Determines the correct log file location
42
+ # It defaults to the current environment's log file
43
+ # @todo Check if that interferes with Rails' logging process
44
+ #
45
+ # @return [String] Path to the logfile
46
+ #
47
+ def log_file
48
+ @log_file ||= @options[:log_file] ? File.expand_path(@options[:log_file]) : File.join(Rails.root, 'log', "#{rails_environment}.log")
49
+ end
50
+
51
+ #
52
+ # Determines the correct log level from the given script arguments
53
+ # Defaults to +INFO+
54
+ #
55
+ # @return [Int] a log level from +Log4r+
56
+ #
57
+ def log_level
58
+ @log_level ||= "Log4r::#{@options[:log_level].upcase}".constantize
59
+ end
60
+
61
+ #
62
+ # @return [String] the currently active rails environment
63
+ #
64
+ def rails_environment
65
+ ENV['RAILS_ENV']
66
+ end
67
+
68
+ #
69
+ # Checks if the given environment is currently active
70
+ # Works like Rails.env.env?
71
+ #
72
+ # @param [String, Symbol] env
73
+ # The environment name
74
+ #
75
+ # @return [Bool] +true+ if the current environment matches the given
76
+ def rails_environment?(env)
77
+ rails_environment.to_s == env.to_s
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,263 @@
1
+ #
2
+ # This class handles the actual email sending.
3
+ # It is called by the +ar_sendmail+ executable in /bin
4
+ # with command line arguments
5
+ #
6
+ # @author Stefan Exner
7
+ #
8
+
9
+ require 'net/smtp'
10
+ require 'ar_mailer_revised/version'
11
+ require 'ar_mailer_revised/helpers/command_line'
12
+ require 'ar_mailer_revised/helpers/general'
13
+
14
+ module ArMailerRevised
15
+ class Mailman
16
+ include ArMailerRevised::Helpers::General
17
+ include ArMailerRevised::Helpers::CommandLine
18
+
19
+ #
20
+ # Simply holds a copy of the options given in from command line
21
+ #
22
+ def initialize(options = {})
23
+ @options = options
24
+ end
25
+
26
+ def run
27
+ logger.debug 'ArMailerRevised initialized with the following options:'
28
+ logger.debug Hirb::Helpers::AutoTable.render @options
29
+
30
+ deliver_emails
31
+ end
32
+
33
+ private
34
+
35
+ #
36
+ # Performs a single email sending for the given batch size
37
+ # Only emails which are ready for sending are actually sent.
38
+ # "Ready for sending" means in this case, that +delivery_time+ is +nil+
39
+ # or set to a time which is <= Time.now
40
+ #
41
+ # Take a look at +EmailScaffold+ for more information
42
+ # about the used scopes
43
+ #
44
+ # @todo: Check if we should delete emails which cause SMTPFatalErrors
45
+ # @todo: Probably add better error handling than simple re-tries
46
+ #
47
+ def deliver_emails
48
+ total_mail_count = ArMailerRevised.email_class.ready_to_deliver.count
49
+ emails = ArMailerRevised.email_class.ready_to_deliver.with_batch_size(@options[:batch_size])
50
+
51
+ if emails.empty?
52
+ logger.info 'No emails to be sent, existing'
53
+ return
54
+ end
55
+
56
+ logger.info "Starting batch sending process, sending #{emails.count} / #{total_mail_count} mails"
57
+
58
+ group_emails_by_settings(emails).each do |settings_hash, grouped_emails|
59
+ setting = OpenStruct.new(settings_hash)
60
+ logger.info "Using setting #{setting.address}:#{setting.port}/#{setting.user_name}"
61
+
62
+ smtp = Net::SMTP.new(setting.address, setting.port)
63
+ smtp.open_timeout = 10
64
+ smtp.read_timeout = 10
65
+ setup_tls(smtp, setting)
66
+
67
+ #Connect to the server and handle possible errors
68
+ begin
69
+ smtp.start(setting.domain, setting.user_name, setting.password, setting.authentication) do
70
+ grouped_emails.each do |email|
71
+ send_email(smtp, email)
72
+ end
73
+ end
74
+ rescue Net::SMTPAuthenticationError => e
75
+ handle_smtp_authentication_error(setting, e, grouped_emails)
76
+ rescue Net::SMTPServerBusy => e
77
+ logger.warn 'Server is busy, trying again next batch.'
78
+ logger.warn 'Complete Error: ' + e.to_s
79
+ rescue Net::OpenTimeout, Net::ReadTimeout => e
80
+ handle_smtp_timeout(setting, e, grouped_emails)
81
+ rescue Net::SMTPSyntaxError, Net::SMTPFatalError, Net::SMTPUnknownError => e
82
+ #TODO: Should we remove the custom SMTP settings here as well?
83
+ logger.warn 'Other SMTP error, trying again next batch.'
84
+ logger.warn 'Complete Error: ' + e.to_s
85
+ rescue Exception => e
86
+ logger.warn 'Other Error, trying again next batch.'
87
+ logger.warn 'Complete Error: ' + e.to_s
88
+ puts e.backtrace
89
+ end
90
+ end
91
+ end
92
+
93
+ #
94
+ # As there may be multiple emails using the same SMTP settings,
95
+ # it would just slow down the sending having to connect to the server
96
+ # multiple times. Therefore, all emails with the same settings
97
+ # are grouped together.
98
+ #
99
+ # @param [Array<Email>] emails
100
+ # Emails to be grouped together
101
+ #
102
+ # @return [Hash<Setting, Email>]
103
+ # Hash mapping SMTP settings to emails.
104
+ # All emails which did not have custom SMTP settings are
105
+ # grouped together under the default SMTP settings.
106
+ #
107
+ def group_emails_by_settings(emails)
108
+ emails.inject({}) do |hash, email|
109
+ setting = ActionMailer::Base.smtp_settings
110
+
111
+ if email.smtp_settings
112
+ setting = email.smtp_settings.clone
113
+ setting[:custom_setting] = true
114
+ end
115
+
116
+ hash[setting] ||= []
117
+ hash[setting] << email
118
+
119
+ hash
120
+ end
121
+ end
122
+
123
+ #
124
+ # Sets the wished TLS / StartTLS options in the
125
+ # given SMTP instance, based on what the user defined
126
+ # in his application's / the email's SMTP settings.
127
+ #
128
+ # Available Settings are (descending importance, meaning that
129
+ # a higher importance setting will override a lower importance setting)
130
+ #
131
+ # 1. +:enable_starttls_auto+ enables STARTTLS if the serves is capable to handle it
132
+ # 2. +:enable_starttls+ forces the usage of STARTTLS, whether the server is capable of it or not
133
+ # 3. +:tls+ forces the usage of TLS (SSL SMTP)
134
+ #
135
+ def setup_tls(smtp, setting)
136
+ if setting.enable_starttls_auto
137
+ logger.debug 'Using STARTTLS, if the server accepts it'
138
+ smtp.enable_starttls_auto
139
+ elsif setting.enable_starttls
140
+ logger.debug 'Forcing STARTTLS'
141
+ smtp.enable_starttls
142
+ elsif setting.tls
143
+ logger.debug 'Forcing TLS'
144
+ smtp.enable_tls
145
+ end
146
+ end
147
+
148
+ #
149
+ # Performs an email sending attempt
150
+ #
151
+ # @param [Net::SMTP] smtp
152
+ # The SMTP connection which already has to be established
153
+ #
154
+ # @param [Email]
155
+ # The email record to be sent.
156
+ #
157
+ # Error handling works as follows:
158
+ #
159
+ # - If the server is busy while sending the email (SMTPServerBusy),
160
+ # the system will leave the email at its old place in the queue and try
161
+ # again next batch as we simply assume that the server failure is just temporarily
162
+ # and the email will not cause the whole email sending to stagnate
163
+ #
164
+ # - If another error occurs, the system will adjust the last_send_attempt
165
+ # in the email record and therefore move it to the end of the queue to
166
+ # ensure that other (working) emails are sent without being held up
167
+ # in the queue by this probably malformed one.
168
+ #
169
+ # Errors are logged with the :warn level.
170
+ #
171
+ def send_email(smtp, email)
172
+ logger.info "Sending Email ##{email.id}"
173
+ smtp.send_message(email.mail, email.from, email.to)
174
+ email.destroy
175
+ rescue Net::SMTPServerBusy => e
176
+ logger.warn 'Server is currently busy, trying again next batch'
177
+ logger.warn 'Complete Error: ' + e.to_s
178
+ rescue Net::SMTPSyntaxError, Net::SMTPFatalError, Net::SMTPUnknownError, Net::ReadTimeout => e
179
+ logger.warn 'Other exception, trying again next batch: ' + e.to_s
180
+ adjust_last_send_attempt!(email)
181
+ end
182
+
183
+ #-----------------------------------------------------------------
184
+ # SMTP connection error handling
185
+ # These errors happen directly when connecting to the SMTP server
186
+ #-----------------------------------------------------------------
187
+
188
+ #
189
+ # Handles Net::OpenTimeout and Net::ReadTimeout occurring
190
+ # while connecting to an SMTP server.
191
+ #
192
+ # If the setting was a custom SMTP setting, it will be removed from
193
+ # all given emails - but only if it failed before.
194
+ # With this, each email setting gets 2 tries.
195
+ #
196
+ # @param [OpenStruct] setting
197
+ # The used SMTP settings
198
+ #
199
+ # @param [Exception] exception
200
+ # The exception thrown
201
+ #
202
+ # @param [Array<Email>] emails
203
+ # All emails to be delivered using this system (in the current batch)
204
+ #
205
+ def handle_smtp_timeout(setting, exception, emails)
206
+ logger.warn "SMTP connection timeout while connecting to '#{setting.address}:#{setting.port}'"
207
+ logger.warn 'Complete Error: ' + exception.to_s
208
+
209
+ if setting.custom_setting
210
+ emails.each do |email|
211
+ if email.previously_attempted?
212
+ logger.warn 'Setting default SMTP settings for all affected emails, they will be sent next batch.'
213
+ remove_custom_smtp_settings!(email)
214
+ else
215
+ adjust_last_send_attempt!(email)
216
+ end
217
+ end
218
+ end
219
+ end
220
+
221
+ #
222
+ # Handles authentication errors occuring while connecting to an SMTP server.
223
+ # @see #handle_smtp_timeout
224
+ #
225
+ # The main difference is, that custom SMTP settings will be deleted directly
226
+ # as it isn't very likely that time will solve the error.
227
+ #
228
+ def handle_smtp_authentication_error(setting, exception, emails)
229
+ logger.warn "SMTP authentication error while connecting to '#{setting.host}:#{setting.port}'"
230
+ logger.warn 'Complete Error: ' + exception.to_s
231
+
232
+ if setting.custom_setting
233
+ logger.warn 'Setting default SMTP settings for all affected emails, they will be sent next batch.'
234
+
235
+ if setting.custom_setting
236
+ emails.each { |email| remove_custom_smtp_settings!(email) }
237
+ end
238
+ else
239
+ logger.error "Your application's base setting ('#{setting.host}:#{setting.port}') produced an authentication error!"
240
+ end
241
+ end
242
+
243
+ #
244
+ # Adjusts the last send attempt timestamp in the given
245
+ # email to the current time.
246
+ #
247
+ def adjust_last_send_attempt!(email)
248
+ logger.info "Setting last send attempt for email ##{email.id} (was: #{email.last_send_attempt})"
249
+ email.last_send_attempt = Time.now.to_i
250
+ email.save(:validate => false)
251
+ end
252
+
253
+ #
254
+ # Removes the custom smtp settings from a given email record
255
+ # and saves it without validations
256
+ #
257
+ def remove_custom_smtp_settings!(email)
258
+ logger.info "Removing custom SMTP settings (#{email.smtp_settings[:address]}:#{email.smtp_settings[:port]}) for email ##{email.id}"
259
+ email.smtp_settings = nil
260
+ email.save(:validate => false)
261
+ end
262
+ end
263
+ end
@@ -1,3 +1,3 @@
1
1
  module ArMailerRevised
2
- VERSION = '0.1'
2
+ VERSION = '1.0.0'
3
3
  end
@@ -0,0 +1,37 @@
1
+ module ArMailerRevised
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ include Rails::Generators::Migration
5
+
6
+ source_root File.expand_path('../templates', __FILE__)
7
+
8
+ argument :model_name, :type => :string, :default => "Email"
9
+
10
+ def self.next_migration_number(path)
11
+ if @prev_migration_nr
12
+ @prev_migration_nr += 1
13
+ else
14
+ @prev_migration_nr = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
15
+ end
16
+ @prev_migration_nr.to_s
17
+ end
18
+
19
+ desc 'Installs everything necessary'
20
+ def create_install
21
+ template 'model.rb', "app/models/#{model_name.classify.underscore}.rb"
22
+ migration_template 'migration.rb', "db/migrate/create_#{model_name.classify.underscore.pluralize}.rb"
23
+
24
+ initializer 'ar_mailer_revised.rb', <<INIT
25
+ ArMailerRevised.configuration do |config|
26
+
27
+ #The model your application is using for email sending.
28
+ #If you created it using the ArMailerRevised generator, the below
29
+ #model name should already be correct.
30
+ config.email_class = #{model_name}
31
+
32
+ end
33
+ INIT
34
+ end
35
+ end
36
+ end
37
+ end
@@ -21,6 +21,7 @@
21
21
  # Serialized Hash storing custom SMTP settings just for this email.
22
22
  # If this value is +nil+, the system will use the default SMTP settings set up in the application
23
23
  #
24
+
24
25
  class <%= model_name.classify %> < ActiveRecord::Base
25
26
  #Helper methods and named scopes provided by ArMailerRevised
26
27
  include ArMailerRevised::EmailScaffold
@@ -0,0 +1,28 @@
1
+ == README
2
+
3
+ This README would normally document whatever steps are necessary to get the
4
+ application up and running.
5
+
6
+ Things you may want to cover:
7
+
8
+ * Ruby version
9
+
10
+ * System dependencies
11
+
12
+ * Configuration
13
+
14
+ * Database creation
15
+
16
+ * Database initialization
17
+
18
+ * How to run the test suite
19
+
20
+ * Services (job queues, cache servers, search engines, etc.)
21
+
22
+ * Deployment instructions
23
+
24
+ * ...
25
+
26
+
27
+ Please feel free to use a different markup language if you do not plan to run
28
+ <tt>rake doc:app</tt>.
@@ -0,0 +1,6 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require File.expand_path('../config/application', __FILE__)
5
+
6
+ Rails.application.load_tasks
File without changes
@@ -0,0 +1,13 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file.
9
+ //
10
+ // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require_tree .