postageapp 1.0.24 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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