ar_mailer_revised 0.1 → 1.0.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.
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 .