postageapp 1.0.24 → 1.2.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 (54) hide show
  1. checksums.yaml +7 -0
  2. data/.travis.yml +13 -12
  3. data/Gemfile +6 -1
  4. data/LICENSE +1 -1
  5. data/README.md +122 -70
  6. data/Rakefile +19 -4
  7. data/generators/postageapp/postageapp_generator.rb +5 -7
  8. data/lib/generators/postageapp/postageapp_generator.rb +15 -9
  9. data/lib/postageapp.rb +42 -36
  10. data/lib/postageapp/configuration.rb +34 -21
  11. data/lib/postageapp/failed_request.rb +60 -36
  12. data/lib/postageapp/logger.rb +6 -7
  13. data/lib/postageapp/mail.rb +3 -0
  14. data/lib/postageapp/mail/arguments.rb +75 -0
  15. data/lib/postageapp/mail/delivery_method.rb +32 -0
  16. data/lib/postageapp/mail/extensions.rb +21 -0
  17. data/lib/postageapp/mailer.rb +72 -20
  18. data/lib/postageapp/mailer/mailer_2.rb +65 -22
  19. data/lib/postageapp/mailer/mailer_3.rb +45 -28
  20. data/lib/postageapp/mailer/mailer_4.rb +72 -40
  21. data/lib/postageapp/rails/rails.rb +17 -7
  22. data/lib/postageapp/rails/railtie.rb +22 -7
  23. data/lib/postageapp/request.rb +67 -43
  24. data/lib/postageapp/response.rb +11 -8
  25. data/lib/postageapp/utils.rb +11 -3
  26. data/lib/postageapp/version.rb +2 -2
  27. data/postageapp.gemspec +13 -11
  28. data/rails/init.rb +1 -1
  29. data/test/configuration_test.rb +35 -38
  30. data/test/failed_request_test.rb +33 -18
  31. data/test/gemfiles/Gemfile.rails-2.3.x +4 -1
  32. data/test/gemfiles/Gemfile.rails-3.0.x +4 -1
  33. data/test/gemfiles/Gemfile.rails-3.1.x +4 -1
  34. data/test/gemfiles/Gemfile.rails-3.2.x +4 -1
  35. data/test/gemfiles/Gemfile.rails-4.0.x +4 -1
  36. data/test/gemfiles/Gemfile.rails-4.1.x +12 -0
  37. data/test/gemfiles/Gemfile.rails-4.2.x +12 -0
  38. data/test/gemfiles/Gemfile.ruby +11 -0
  39. data/test/helper.rb +52 -33
  40. data/test/live_test.rb +11 -8
  41. data/test/mail_delivery_method_test.rb +161 -0
  42. data/test/mailer/action_mailer_2/notifier.rb +37 -28
  43. data/test/mailer/action_mailer_3/notifier.rb +28 -22
  44. data/test/mailer_2_test.rb +20 -16
  45. data/test/mailer_3_test.rb +16 -22
  46. data/test/mailer_4_test.rb +28 -28
  47. data/test/mailer_helper_methods_test.rb +17 -14
  48. data/test/postageapp_test.rb +8 -9
  49. data/test/rails_initialization_test.rb +14 -14
  50. data/test/request_test.rb +35 -35
  51. data/test/response_test.rb +20 -19
  52. data/test/travis_test.rb +168 -0
  53. data/test/with_environment.rb +27 -0
  54. metadata +23 -17
@@ -13,48 +13,54 @@ require 'postageapp/logger'
13
13
  require 'postageapp/request'
14
14
  require 'postageapp/failed_request'
15
15
  require 'postageapp/response'
16
- require 'postageapp/rails/railtie' if defined?(Rails::Railtie)
16
+ require 'postageapp/mail'
17
+ require 'postageapp/mail/delivery_method'
18
+
19
+ require 'postageapp/rails/railtie' if (defined?(Rails::Railtie))
17
20
 
18
21
  module PostageApp
19
-
20
22
  class Error < StandardError ; end
21
23
 
24
+ # Call this method to modify your configuration
25
+ # Example:
26
+ # PostageApp.configure do |config|
27
+ # config.api_key = '1234567890abcdef'
28
+ # config.recipient_override = 'test@test.test' if Rails.env.staging?
29
+ # end
30
+ #
31
+ # If you do not want/need to initialize the gem in this way, you can use the environment
32
+ # variable POSTAGEAPP_API_KEY to set up your key.
33
+
34
+ def self.configure
35
+ yield(configuration)
36
+ end
37
+
38
+ # Accessor for the PostageApp::Configuration object
39
+ # Example use:
40
+ # PostageApp.configuration.api_key = '1234567890abcdef'
41
+ def self.configuration
42
+ @configuration ||= Configuration.new
43
+ end
44
+
22
45
  class << self
23
-
24
- # Call this method to modify your configuration
25
- # Example:
26
- # PostageApp.configure do |config|
27
- # config.api_key = '1234567890abcdef'
28
- # config.recipient_override = 'test@test.test' if Rails.env.staging?
29
- # end
30
- #
31
- # If you do not want/need to initialize the gem in this way, you can use the environment
32
- # variable POSTAGEAPP_API_KEY to set up your key.
33
-
34
- def configure
35
- yield configuration
36
- end
37
-
38
- # Accessor for the PostageApp::Configuration object
39
- # Example use:
40
- # PostageApp.configuration.api_key = '1234567890abcdef'
41
- def configuration
42
- @configuration ||= Configuration.new
43
- end
44
46
  alias :config :configuration
45
-
46
- # Logger for the plugin
47
- def logger
48
- @logger ||= begin
49
- configuration.logger || PostageApp::Logger.new(
50
- if configuration.project_root
51
- FileUtils.mkdir_p(File.join(File.expand_path(configuration.project_root), 'log'))
52
- File.join(configuration.project_root, "log/postageapp_#{configuration.environment}.log")
53
- else
54
- STDOUT
55
- end
56
- )
57
- end
47
+ end
48
+
49
+ # Logger for the plugin
50
+ def self.logger
51
+ @logger ||= begin
52
+ configuration.logger || PostageApp::Logger.new(
53
+ if (configuration.project_root)
54
+ FileUtils.mkdir_p(File.join(File.expand_path(configuration.project_root), 'log'))
55
+ File.join(configuration.project_root, "log/postageapp_#{configuration.environment}.log")
56
+ else
57
+ STDOUT
58
+ end
59
+ )
58
60
  end
59
61
  end
60
62
  end
63
+
64
+ require 'postageapp/mail/extensions'
65
+
66
+ PostageApp::Mail::Extensions.install!
@@ -1,5 +1,4 @@
1
1
  class PostageApp::Configuration
2
-
3
2
  # +true+ for https, +false+ for http connections (default: is +true+)
4
3
  attr_accessor :secure
5
4
 
@@ -12,7 +11,7 @@ class PostageApp::Configuration
12
11
 
13
12
  # The port on which PostageApp service runs (default: 443 for secure, 80 for
14
13
  # insecure connections)
15
- attr_accessor :port
14
+ attr_writer :port
16
15
 
17
16
  # The hostname of the proxy server (if using a proxy)
18
17
  attr_accessor :proxy_host
@@ -54,43 +53,57 @@ class PostageApp::Configuration
54
53
  attr_accessor :logger
55
54
 
56
55
  def initialize
57
- @secure = true
58
- @host = 'api.postageapp.com'
59
- @http_open_timeout = 5
60
- @http_read_timeout = 10
56
+ @secure = true
57
+ @host = 'api.postageapp.com'
58
+ @http_open_timeout = 5
59
+ @http_read_timeout = 10
61
60
  @requests_to_resend = %w( send_message )
62
- @framework = 'undefined framework'
63
- @environment = 'production'
61
+ @framework = 'undefined framework'
62
+ @environment = 'production'
64
63
  end
65
64
 
66
65
  alias_method :secure?, :secure
67
66
 
67
+ # Assign which API key is used to make API calls. Can also be specified
68
+ # using the `POSTAGEAPP_API_KEY` environment variable.
68
69
  def api_key=(key)
69
70
  @api_key = key
70
71
  end
71
72
 
73
+ # Returns the API key used to make API calls. Can be specified as the
74
+ # `POSTAGEAPP_API_KEY` environment variable.
72
75
  def api_key
73
- @api_key || ENV['POSTAGEAPP_API_KEY']
76
+ @api_key ||= ENV['POSTAGEAPP_API_KEY']
74
77
  end
75
78
 
79
+ # Returns the HTTP protocol used to make API calls
76
80
  def protocol
77
- @protocol || if secure?
78
- 'https'
79
- else
80
- 'http'
81
- end
81
+ @protocol ||= (secure? ? 'https' : 'http')
82
82
  end
83
83
 
84
+ # Returns the port used to make API calls
84
85
  def port
85
- @port || if secure?
86
- 443
87
- else
88
- 80
89
- end
86
+ @port ||= (secure? ? 443 : 80)
90
87
  end
91
88
 
89
+ # Returns the endpoint URL to make API calls
92
90
  def url
93
91
  "#{self.protocol}://#{self.host}:#{self.port}"
94
92
  end
95
-
96
- end
93
+
94
+ # Returns a properly config
95
+ def http
96
+ http = Net::HTTP::Proxy(
97
+ self.proxy_host,
98
+ self.proxy_port,
99
+ self.proxy_user,
100
+ self.proxy_pass
101
+ ).new(self.host, self.port)
102
+
103
+ http.read_timeout = self.http_read_timeout
104
+ http.open_timeout = self.http_open_timeout
105
+ http.use_ssl = self.secure?
106
+
107
+ http
108
+ end
109
+ end
@@ -1,75 +1,99 @@
1
1
  module PostageApp::FailedRequest
2
-
3
2
  # Stores request object into a file for future re-send
4
3
  # returns true if stored, false if not (due to undefined project path)
5
4
  def self.store(request)
6
- return false if !store_path || !PostageApp.configuration.requests_to_resend.member?(request.method.to_s)
5
+ return false unless (self.store_path)
6
+ return false unless (PostageApp.configuration.requests_to_resend.member?(request.method.to_s))
7
7
 
8
- open(file_path(request.uid), 'wb') do |f|
9
- f.write(Marshal.dump(request))
10
- end unless File.exists?(file_path(request.uid))
8
+ unless (File.exists?(file_path(request.uid)))
9
+ open(file_path(request.uid), 'wb') do |f|
10
+ f.write(Marshal.dump(request))
11
+ end
12
+ end
11
13
 
12
- PostageApp.logger.info "STORING FAILED REQUEST [#{request.uid}]"
14
+ PostageApp.logger.info("STORING FAILED REQUEST [#{request.uid}]")
13
15
 
14
16
  true
15
17
  end
18
+
19
+ def self.force_delete!(path)
20
+ File.delete(path)
21
+
22
+ rescue
23
+ nil
24
+ end
16
25
 
17
26
  # Attempting to resend failed requests
18
27
  def self.resend_all
19
- return false if !store_path
28
+ return false unless (self.store_path)
20
29
 
21
30
  Dir.foreach(store_path) do |filename|
22
- next if !filename.match /^\w{40}$/
31
+ next unless (filename.match(/^\w{40}$/))
23
32
 
24
33
  request = initialize_request(filename)
25
34
 
26
- receipt_response = PostageApp::Request.new(:get_message_receipt, :uid => filename).send(true)
27
- if receipt_response.fail?
35
+ receipt_response = PostageApp::Request.new(
36
+ :get_message_receipt,
37
+ :uid => filename
38
+ ).send(true)
39
+
40
+ if (receipt_response.fail?)
28
41
  return
29
- elsif receipt_response.ok?
30
- PostageApp.logger.info "NOT RESENDING FAILED REQUEST [#{filename}]"
31
- File.delete(file_path(filename)) rescue nil
32
-
33
- elsif receipt_response.not_found?
34
- PostageApp.logger.info "RESENDING FAILED REQUEST [#{filename}]"
42
+ elsif (receipt_response.ok?)
43
+ PostageApp.logger.info("Skipping failed request (already sent) [#{filename}]")
44
+
45
+ force_delete!(file_path(filename))
46
+ elsif (receipt_response.not_found?)
47
+ PostageApp.logger.info("Retrying failed request [#{filename}]")
48
+
35
49
  response = request.send(true)
36
50
 
37
51
  # Not a fail, so we can remove this file, if it was then
38
52
  # there will be another attempt to resend
39
- File.delete(file_path(filename)) rescue nil if !response.fail?
53
+
54
+ unless (response.fail?)
55
+ force_delete!(file_path(filename))
56
+ end
40
57
  else
41
- PostageApp.logger.info "NOT RESENDING FAILED REQUEST [#{filename}], RECEIPT CANNOT BE PROCESSED"
42
- File.delete(file_path(filename)) rescue nil
58
+ PostageApp.logger.info("Skipping failed request (non-replayable request type) [#{filename}]")
59
+
60
+ force_delete!(file_path(filename))
43
61
  end
44
62
  end
63
+
45
64
  return
46
65
  end
47
66
 
48
67
  # Initializing PostageApp::Request object from the file
49
68
  def self.initialize_request(uid)
50
- return false if !store_path
51
-
52
- if File.exists?(file_path(uid))
53
- begin
54
- Marshal.load(File.read(file_path(uid)))
55
- rescue
56
- File.delete(file_path(uid))
57
- return false
58
- end
59
- end
69
+ return false unless (self.store_path)
70
+ return false unless (File.exists?(file_path(uid)))
71
+
72
+ Marshal.load(File.read(file_path(uid)))
73
+
74
+ rescue
75
+ force_delete!(file_path(uid))
76
+
77
+ false
60
78
  end
61
79
 
62
80
  protected
63
-
64
81
  def self.store_path
65
- return if !PostageApp.configuration.project_root
66
- dir = File.join(File.expand_path(PostageApp.configuration.project_root), 'tmp/postageapp_failed_requests')
67
- FileUtils.mkdir_p(dir) unless File.exists?(dir)
68
- return dir
82
+ return unless (PostageApp.configuration.project_root)
83
+
84
+ dir = File.join(
85
+ File.expand_path(PostageApp.configuration.project_root),
86
+ 'tmp/postageapp_failed_requests'
87
+ )
88
+
89
+ unless (File.exists?(dir))
90
+ FileUtils.mkdir_p(dir)
91
+ end
92
+
93
+ dir
69
94
  end
70
95
 
71
96
  def self.file_path(uid)
72
97
  File.join(store_path, uid)
73
98
  end
74
-
75
- end
99
+ end
@@ -1,16 +1,15 @@
1
1
  class PostageApp::Logger < ::Logger
2
-
3
2
  def format_message(severity, datetime, progname, msg)
4
- timestamp = datetime.strftime('%m/%d/%Y %H:%M:%S %Z')
5
- message = case msg
3
+ "[%s] %s\n" % [
4
+ datetime.strftime('%m/%d/%Y %H:%M:%S %Z'),
5
+ case (msg)
6
6
  when PostageApp::Request
7
7
  "REQUEST [#{msg.url}]\n #{msg.arguments_to_send.to_json}"
8
8
  when PostageApp::Response
9
9
  "RESPONSE [#{msg.status} #{msg.uid} #{msg.message}]\n #{msg.data.to_json}"
10
10
  else
11
11
  msg
12
- end
13
- "[#{timestamp}] #{message}\n"
12
+ end
13
+ ]
14
14
  end
15
-
16
- end
15
+ end
@@ -0,0 +1,3 @@
1
+ class PostageApp::Mail
2
+ require 'postageapp/mail/arguments'
3
+ end
@@ -0,0 +1,75 @@
1
+ require 'set'
2
+
3
+ # This class decomposes a Mail::Message into a PostageApp API call that
4
+ # produces the same result when sent.
5
+
6
+ class PostageApp::Mail::Arguments
7
+ # Certain headers need to be ignored since they are generated internally.
8
+ HEADERS_IGNORED = Set.new(%w[
9
+ Content-Type
10
+ To
11
+ ]).freeze
12
+
13
+ # Creates a new instance with the given Mail::Message binding.
14
+ def initialize(mail)
15
+ @mail = mail
16
+ end
17
+
18
+ # Returns the extracted arguments. If a pre-existing arguments has is
19
+ # supplied, arguments are injected into that.
20
+ def extract(arguments = nil)
21
+ arguments ||= { }
22
+ arguments['content'] ||= { }
23
+ arguments['headers'] ||= { }
24
+
25
+ arguments['recipients'] = @mail.to
26
+
27
+ if (@mail.multipart?)
28
+ @mail.parts.each do |part|
29
+ if (part.content_disposition)
30
+ add_attachment(arguments, part)
31
+ else
32
+ add_part(arguments, part)
33
+ end
34
+ end
35
+ else
36
+ add_part(arguments, @mail)
37
+ end
38
+
39
+ _headers = arguments['headers']
40
+ @mail.header.fields.each do |field|
41
+ next if (HEADERS_IGNORED.include?(field.name))
42
+
43
+ _headers[field.name] = field.value
44
+ end
45
+
46
+ if (@mail.has_attachments?)
47
+ @mail.attachments.each do |attachment|
48
+ add_attachment(arguments, attachment)
49
+ end
50
+ end
51
+
52
+ [ :send_message, arguments ]
53
+ end
54
+
55
+ protected
56
+ def add_part(arguments, part)
57
+ case (part.content_type.to_s.split(/\s*;/).first)
58
+ when 'text/html'
59
+ arguments['content']['text/html'] = part.body.to_s
60
+ when 'text/plain', nil
61
+ arguments['content']['text/plain'] = part.body.to_s
62
+ else
63
+ # Unknown type.
64
+ end
65
+ end
66
+
67
+ def add_attachment(arguments, part)
68
+ arguments['attachments'] ||= { }
69
+
70
+ arguments['attachments'][part.filename] = {
71
+ 'content' => Base64.encode64(part.body.to_s),
72
+ 'content_type' => part.content_type
73
+ }
74
+ end
75
+ end
@@ -0,0 +1,32 @@
1
+ class PostageApp::Mail::DeliveryMethod
2
+ def self.deliveries
3
+ @deliveries ||= [ ]
4
+ end
5
+
6
+ # Creates a new DeliveryMethod instance with the supplied options.
7
+ def initialize(options)
8
+ @options = options.dup
9
+ end
10
+
11
+ # Delivers a given Mail::Message through PostageApp using the configuration
12
+ # specified through Mail defaults or settings applied to ActionMailer.
13
+ def deliver!(mail)
14
+ api_method, arguments = PostageApp::Mail::Arguments.new(mail).extract
15
+
16
+ case (@options[:api_key])
17
+ when false, :test
18
+ # In testing mode, just capture the calls that would have been made so
19
+ # they can be inspected later using the deliveries class method.
20
+ self.class.deliveries << [ api_method, arguments ]
21
+ when nil
22
+ # If the API key is not defined, raise an error providing a hint as to
23
+ # how to set that correctly.
24
+ raise PostageApp::Error,
25
+ "PostageApp API key not defined: Add :api_key to config.action_mailer.postageapp_settings to config/application.rb"
26
+ else
27
+ arguments['api_key'] ||= @options[:api_key]
28
+
29
+ PostageApp::Request.new(api_method, arguments).send
30
+ end
31
+ end
32
+ end