postageapp 0.0.0 → 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 (49) hide show
  1. data/.gitignore +3 -0
  2. data/LICENSE +1 -1
  3. data/README.md +176 -0
  4. data/Rakefile +14 -12
  5. data/VERSION +1 -1
  6. data/generators/postageapp/postageapp_generator.rb +34 -0
  7. data/generators/postageapp/templates/initializer.rb +3 -0
  8. data/generators/postageapp/templates/postageapp_tasks.rake +78 -0
  9. data/lib/generators/postageapp/postageapp_generator.rb +27 -0
  10. data/lib/postageapp.rb +56 -0
  11. data/lib/postageapp/configuration.rb +91 -0
  12. data/lib/postageapp/failed_request.rb +64 -0
  13. data/lib/postageapp/logger.rb +16 -0
  14. data/lib/postageapp/mailer.rb +48 -0
  15. data/lib/postageapp/mailer/mailer_2.rb +92 -0
  16. data/lib/postageapp/mailer/mailer_3.rb +192 -0
  17. data/lib/postageapp/rails.rb +14 -0
  18. data/lib/postageapp/request.rb +87 -0
  19. data/lib/postageapp/response.rb +39 -0
  20. data/lib/postageapp/utils.rb +30 -0
  21. data/lib/postageapp/version.rb +7 -0
  22. data/postageapp.gemspec +61 -7
  23. data/test/configuration_test.rb +66 -0
  24. data/test/failed_request_test.rb +79 -0
  25. data/test/helper.rb +66 -0
  26. data/test/mailer/action_mailer_2/notifier.rb +64 -0
  27. data/test/mailer/action_mailer_2/notifier/with_body_and_attachment.erb +1 -0
  28. data/test/mailer/action_mailer_2/notifier/with_html_and_text_views.text.html.erb +1 -0
  29. data/test/mailer/action_mailer_2/notifier/with_html_and_text_views.text.plain.erb +1 -0
  30. data/test/mailer/action_mailer_2/notifier/with_simple_view.erb +1 -0
  31. data/test/mailer/action_mailer_2/notifier/with_text_only_view.text.plain.erb +1 -0
  32. data/test/mailer/action_mailer_3/notifier.rb +98 -0
  33. data/test/mailer/action_mailer_3/notifier/with_html_and_text_views.html.erb +1 -0
  34. data/test/mailer/action_mailer_3/notifier/with_html_and_text_views.text.erb +1 -0
  35. data/test/mailer/action_mailer_3/notifier/with_old_api.html.erb +1 -0
  36. data/test/mailer/action_mailer_3/notifier/with_old_api.text.erb +1 -0
  37. data/test/mailer/action_mailer_3/notifier/with_simple_view.erb +1 -0
  38. data/test/mailer/action_mailer_3/notifier/with_text_only_view.text.erb +1 -0
  39. data/test/mailer_2_test.rb +87 -0
  40. data/test/mailer_3_test.rb +104 -0
  41. data/test/mailer_helper_methods_test.rb +24 -0
  42. data/test/postageapp_test.rb +18 -0
  43. data/test/rails_initialization_test.rb +29 -0
  44. data/test/request_integration_test.rb +78 -0
  45. data/test/request_test.rb +81 -0
  46. data/test/response_test.rb +40 -0
  47. metadata +84 -9
  48. data/README.rdoc +0 -17
  49. data/test/test_postageapp.rb +0 -7
@@ -0,0 +1,91 @@
1
+ class PostageApp::Configuration
2
+
3
+ # The API key for your postageapp.com project
4
+ attr_accessor :api_key
5
+
6
+ # +true+ for https, +false+ for http connections (default: is +true+)
7
+ attr_accessor :secure
8
+
9
+ # The protocol used to connect to the service (default: 'https' for secure
10
+ # and 'http' for insecure connections)
11
+ attr_accessor :protocol
12
+
13
+ # The host to connect to (default: 'api.postageapp.com')
14
+ attr_accessor :host
15
+
16
+ # The port on which PostageApp service runs (default: 443 for secure, 80 for
17
+ # insecure connections)
18
+ attr_accessor :port
19
+
20
+ # The hostname of the proxy server (if using a proxy)
21
+ attr_accessor :proxy_host
22
+
23
+ # The port of the proxy server (if using proxy)
24
+ attr_accessor :proxy_port
25
+
26
+ # The username for the proxy server (if using proxy)
27
+ attr_accessor :proxy_user
28
+
29
+ # The password for the proxy server (if using proxy)
30
+ attr_accessor :proxy_pass
31
+
32
+ # The HTTP open timeout in seconds (defaults to 2).
33
+ attr_accessor :http_open_timeout
34
+
35
+ # The HTTP read timeout in seconds (defaults to 5).
36
+ attr_accessor :http_read_timeout
37
+
38
+ # The email address that all send_message method should address
39
+ # all messages while overriding original addresses
40
+ attr_accessor :recipient_override
41
+
42
+ # A list of API method names payloads of which are captured and resent
43
+ # in case of service unavailability
44
+ attr_accessor :requests_to_resend
45
+
46
+ # The file path of the project. This is where logs and failed requests
47
+ # can be stored
48
+ attr_accessor :project_root
49
+
50
+ # The framework PostageApp gem runs in
51
+ attr_accessor :framework
52
+
53
+ # Environment gem is running in
54
+ attr_accessor :environment
55
+
56
+ # The logger used by this gem
57
+ attr_accessor :logger
58
+
59
+ def initialize
60
+ @secure = true
61
+ @host = 'api.postageapp.com'
62
+ @http_open_timeout = 5
63
+ @http_read_timeout = 10
64
+ @requests_to_resend = %w( send_message )
65
+ @framework = 'undefined framework'
66
+ @environment = 'production'
67
+ end
68
+
69
+ alias_method :secure?, :secure
70
+
71
+ def protocol
72
+ @protocol || if secure?
73
+ 'https'
74
+ else
75
+ 'http'
76
+ end
77
+ end
78
+
79
+ def port
80
+ @port || if secure?
81
+ 443
82
+ else
83
+ 80
84
+ end
85
+ end
86
+
87
+ def url
88
+ "#{self.protocol}://#{self.host}:#{self.port}"
89
+ end
90
+
91
+ end
@@ -0,0 +1,64 @@
1
+ module PostageApp::FailedRequest
2
+
3
+ # Stores request object into a file for future re-send
4
+ # returns true if stored, false if not (due to undefined project path)
5
+ def self.store(request)
6
+ return false if !store_path || !PostageApp.configuration.requests_to_resend.member?(request.method.to_s)
7
+
8
+ open(file_path(request.uid), 'w') do |f|
9
+ f.write(Marshal.dump(request))
10
+ end unless File.exists?(file_path(request.uid))
11
+
12
+ PostageApp.logger.info "STORING FAILED REQUEST [#{request.uid}]"
13
+
14
+ true
15
+ end
16
+
17
+ # Attempting to resend failed requests
18
+ def self.resend_all
19
+ return false if !store_path
20
+
21
+ Dir.foreach(store_path) do |filename|
22
+ next if !filename.match /^\w{40}$/
23
+
24
+ request = initialize_request(filename)
25
+
26
+ receipt_response = PostageApp::Request.new(:get_message_receipt, :uid => filename).send(true)
27
+ if receipt_response.ok?
28
+ PostageApp.logger.info "NOT RESENDING FAILED REQUEST [#{filename}]"
29
+ File.delete(file_path(filename))
30
+
31
+ elsif receipt_response.not_found?
32
+ PostageApp.logger.info "RESENDING FAILED REQUEST [#{filename}]"
33
+ response = request.send(true)
34
+
35
+ # Not a fail, so we can remove this file, if it was then
36
+ # there will be another attempt to resend
37
+ File.delete(file_path(filename)) if !response.fail?
38
+ end
39
+ end
40
+
41
+ return
42
+ end
43
+
44
+ # Initializing PostageApp::Request object from the file
45
+ def self.initialize_request(uid)
46
+ return false if !store_path
47
+
48
+ Marshal.load(File.read(file_path(uid))) if File.exists?(file_path(uid))
49
+ end
50
+
51
+ protected
52
+
53
+ def self.store_path
54
+ return if !PostageApp.configuration.project_root
55
+ dir = File.join(File.expand_path(PostageApp.configuration.project_root), 'tmp/postageapp_failed_requests')
56
+ FileUtils.mkdir_p(dir) unless File.exists?(dir)
57
+ return dir
58
+ end
59
+
60
+ def self.file_path(uid)
61
+ File.join(store_path, uid)
62
+ end
63
+
64
+ end
@@ -0,0 +1,16 @@
1
+ class PostageApp::Logger < ::Logger
2
+
3
+ def format_message(severity, datetime, progname, msg)
4
+ timestamp = datetime.strftime('%m/%d/%Y %H:%M:%S %Z')
5
+ message = case msg
6
+ when PostageApp::Request
7
+ "REQUEST [#{msg.url}]\n #{msg.arguments_to_send.to_json}"
8
+ when PostageApp::Response
9
+ "RESPONSE [#{msg.status} #{msg.uid} #{msg.message}]\n #{msg.data.to_json}"
10
+ else
11
+ msg
12
+ end
13
+ "[#{timestamp}] #{message}\n"
14
+ end
15
+
16
+ end
@@ -0,0 +1,48 @@
1
+ require 'action_mailer'
2
+ require 'action_mailer/version'
3
+
4
+ # Loading PostageApp::Mailer class depending on what action_mailer is
5
+ # currently installed on the system. Assuming we're dealing only with
6
+ # ones that come with Rails 2 and 3
7
+ if ActionMailer::VERSION::MAJOR >= 3
8
+ require File.expand_path('../mailer/mailer_3', __FILE__)
9
+ else
10
+ require File.expand_path('../mailer/mailer_2', __FILE__)
11
+ end
12
+
13
+ # General helper methods for Request object to act more like TMail::Mail
14
+ # of Mail for testing
15
+ class PostageApp::Request
16
+
17
+ # Getter and setter for headers. You can specify headers in the following
18
+ # formats:
19
+ # headers['Custom-Header'] = 'Custom Value'
20
+ # headers 'Custom-Header-1' => 'Custom Value 1',
21
+ # 'Custom-Header-2' => 'Custom Value 2'
22
+ def headers(value = nil)
23
+ self.arguments['headers'] ||= { }
24
+ if value && value.is_a?(Hash)
25
+ value.each do |k, v|
26
+ self.arguments['headers'][k.to_s] = v.to_s
27
+ end
28
+ end
29
+ self.arguments['headers']
30
+ end
31
+
32
+ def to
33
+ self.arguments_to_send.dig('arguments', 'recipients')
34
+ end
35
+
36
+ def from
37
+ self.arguments_to_send.dig('arguments', 'headers', 'from')
38
+ end
39
+
40
+ def subject
41
+ self.arguments_to_send.dig('arguments', 'headers', 'subject')
42
+ end
43
+
44
+ def body
45
+ self.arguments_to_send.dig('arguments', 'content')
46
+ end
47
+
48
+ end
@@ -0,0 +1,92 @@
1
+ # Postage::Mailer allows you to use/re-use existing mailers set up using
2
+ # ActionMailer. The only catch is to change inheritance from ActionMailer::Base
3
+ # to PostageApp::Mailer. Also don't forget to require 'postageapp/mailer'
4
+ #
5
+ # Here's an example of a valid PostageApp::Mailer class
6
+ #
7
+ # require 'postageapp/mailer'
8
+ #
9
+ # class Notifier < PostageApp::Mailer
10
+ # def signup_notification(recipient)
11
+ # recipients recipient.email_address
12
+ # from 'system@example.com'
13
+ # subject 'New Account Information'
14
+ # end
15
+ # end
16
+ #
17
+ # Postage::Mailer introduces a few mailer methods specific to Postage:
18
+ #
19
+ # * postage_template - template name that is defined in your PostageApp project
20
+ # * postage_variables - extra variables you want to send along with the message
21
+ #
22
+ # Sending email
23
+ #
24
+ # Notifier.deliver_signup_notification(user) # attempts to deliver to PostageApp (depending on env)
25
+ # request = Notifier.create_signup_notification(user) # creates PostageApp::Request object
26
+ #
27
+ class PostageApp::Mailer < ActionMailer::Base
28
+
29
+ # Using :test as a delivery method if set somewhere else
30
+ self.delivery_method = :postage unless (self.delivery_method == :test)
31
+
32
+ adv_attr_accessor :postage_template
33
+ adv_attr_accessor :postage_variables
34
+
35
+ def perform_delivery_postage(mail)
36
+ mail.send
37
+ end
38
+
39
+ def deliver!(mail = @mail)
40
+ raise 'PostageApp::Request object not present, cannot deliver' unless mail
41
+ __send__("perform_delivery_#{delivery_method}", mail) if perform_deliveries
42
+ end
43
+
44
+ # Creating a Postage::Request object unlike TMail one in ActionMailer::Base
45
+ def create_mail
46
+ params = { }
47
+ params['recipients'] = self.recipients unless self.recipients.blank?
48
+
49
+ params['headers'] = { }
50
+ params['headers']['subject'] = self.subject unless self.subject.blank?
51
+ params['headers']['from'] = self.from unless self.from.blank?
52
+ params['headers'].merge!(self.headers) unless self.headers.blank?
53
+
54
+ params['content'] = { }
55
+ params['attachments'] = { }
56
+
57
+ if @parts.empty?
58
+ params['content'][self.content_type] = self.body unless self.body.blank?
59
+ else
60
+ self.parts.each do |part|
61
+ case part.content_disposition
62
+ when 'inline'
63
+ part.content_type = 'text/plain' if part.content_type.blank? && String === part.body
64
+ params['content'][part.content_type] = part.body
65
+ when 'attachment'
66
+ params['attachments'][part.filename] = {
67
+ 'content_type' => part.content_type,
68
+ 'content' => Base64.encode64(part.body)
69
+ }
70
+ end
71
+ end
72
+ end
73
+
74
+ params['template'] = self.postage_template unless self.postage_template.blank?
75
+ params['variables'] = self.postage_variables unless self.postage_variables.blank?
76
+
77
+ params.delete('headers') if params['headers'].blank?
78
+ params.delete('content') if params['content'].blank?
79
+ params.delete('attachments') if params['attachments'].blank?
80
+
81
+ @mail = PostageApp::Request.new(:send_message, params)
82
+ end
83
+
84
+ # Not insisting rendering a view if it's not there. PostageApp gem can send blank content
85
+ # provided that the template is defined.
86
+ def render(opts)
87
+ super(opts)
88
+ rescue ActionView::MissingTemplate
89
+ # do nothing
90
+ end
91
+
92
+ end
@@ -0,0 +1,192 @@
1
+ # Postage::Mailer allows you to use/re-use existing mailers set up using
2
+ # ActionMailer. The only catch is to change inheritance from ActionMailer::Base
3
+ # to PostageApp::Mailer. Also don't forget to require 'postageapp/mailer'
4
+ #
5
+ # Here's an example of a valid PostageApp::Mailer class
6
+ #
7
+ # require 'postageapp/mailer'
8
+ #
9
+ # class Notifier < PostageApp::Mailer
10
+ # def signup_notification(recipient)
11
+ # mail(
12
+ # :to => recipient.email,
13
+ # :from => 'sender@test.test',
14
+ # :subject => 'Test Message'
15
+ # )
16
+ # end
17
+ # end
18
+ #
19
+ # Postage::Mailer introduces a few mailer methods specific to Postage:
20
+ #
21
+ # * postage_template - template name that is defined in your PostageApp project
22
+ # * postage_variables - extra variables you want to send along with the message
23
+ #
24
+ # Sending email
25
+ #
26
+ # request = Notifier.signup_notification(user) # creates PostageApp::Request object
27
+ # response = request.deliver # attempts to deliver the message and creates a PostageApp::Response
28
+ #
29
+ class PostageApp::Mailer < ActionMailer::Base
30
+
31
+ # Wrapper for creating attachments
32
+ # Attachments sent to PostageApp are in the following format:
33
+ # 'filename.ext' => {
34
+ # 'content_type' => 'content/type',
35
+ # 'content' => 'base64_encoded_content'
36
+ # }
37
+ class Attachments < Hash
38
+
39
+ def initialize(message)
40
+ @_message = message
41
+ message.arguments['attachments'] ||= { }
42
+ end
43
+
44
+ def []=(filename, attachment)
45
+ default_content_type = MIME::Types.type_for(filename).first.content_type rescue ''
46
+ if attachment.is_a?(Hash)
47
+ content_type = attachment[:content_type] || default_content_type
48
+ content = Base64.encode64(attachment[:body])
49
+ else
50
+ content_type = default_content_type
51
+ content = Base64.encode64(attachment)
52
+ end
53
+ @_message.arguments['attachments'][filename] = {
54
+ 'content_type' => content_type,
55
+ 'content' => content
56
+ }
57
+ end
58
+ end
59
+
60
+ # In API call we can specify PostageApp template that will be used
61
+ # to generate content of the message
62
+ attr_accessor :postage_template
63
+
64
+ # Hash of variables that will be used to inject into the content
65
+ attr_accessor :postage_variables
66
+
67
+ # Instead of initializing Mail object, we prepare PostageApp::Request
68
+ def initialize(method_name = nil, *args)
69
+ @_message = PostageApp::Request.new(:send_message)
70
+ process(method_name, *args) if method_name
71
+ end
72
+
73
+ def postage_template(value)
74
+ @_message.arguments['template'] = value
75
+ end
76
+
77
+ def postage_variables(variables = {})
78
+ @_message.arguments['variables'] = variables
79
+ end
80
+
81
+ def attachments
82
+ @_attachments ||= Attachments.new(@_message)
83
+ end
84
+
85
+ # Overriding method that prepares Mail object. This time we'll be
86
+ # contructing PostageApp::Request payload.
87
+ def mail(headers = {}, &block)
88
+ # Guard flag to prevent both the old and the new API from firing
89
+ # Should be removed when old API is removed
90
+ @mail_was_called = true
91
+ m = @_message
92
+
93
+ # At the beginning, do not consider class default for parts order neither content_type
94
+ content_type = headers[:content_type]
95
+ parts_order = headers[:parts_order]
96
+
97
+ # Call all the procs (if any)
98
+ default_values = self.class.default.merge(self.class.default) do |k,v|
99
+ v.respond_to?(:call) ? v.bind(self).call : v
100
+ end
101
+
102
+ # Handle defaults
103
+ headers = headers.reverse_merge(default_values)
104
+ headers[:subject] ||= default_i18n_subject
105
+
106
+ # Set configure delivery behavior
107
+ wrap_delivery_behavior!(headers.delete(:delivery_method))
108
+
109
+ # Assigning recipients
110
+ m.arguments['recipients'] = headers.delete(:to)
111
+
112
+ # Assign all headers except parts_order, content_type and body
113
+ assignable = headers.except(:parts_order, :content_type, :body, :template_name, :template_path)
114
+ m.arguments['headers'] = assignable
115
+
116
+ # Render the templates and blocks
117
+ responses, explicit_order = collect_responses_and_parts_order(headers, &block)
118
+ create_parts_from_responses(m, responses)
119
+
120
+ m
121
+ end
122
+
123
+ # Overriding method to create mesage from the old_api
124
+ def create_mail
125
+ m = @_message
126
+
127
+ m.arguments['headers'] ||= { }
128
+ m.arguments['headers']['from'] = from
129
+ m.arguments['headers']['subject'] = subject
130
+ m.arguments['recipients'] = recipients
131
+
132
+ m
133
+ end
134
+
135
+ # Overriding part assignment from old_api
136
+ # For now only accepting a hash
137
+ def part(params)
138
+ @_message.arguments['content'] ||= { }
139
+ @_message.arguments['content'][params[:content_type]] = params[:body]
140
+ end
141
+
142
+ # Overriding attachment assignment from old_api
143
+ # For now only accepting a hash
144
+ def attachment(params)
145
+ @_message.arguments['attachments'] ||= { }
146
+ @_message.arguments['attachments'][params[:filename]] = {
147
+ 'content_type' => params[:content_type],
148
+ 'content' => Base64.encode64(params[:body].to_s)
149
+ }
150
+ end
151
+
152
+ # Overriding method in old_api
153
+ def create_inline_part(body, mime_type = nil)
154
+ @_message.arguments['content'] ||= { }
155
+ @_message.arguments['content'][mime_type && mime_type.to_s || 'text/plain'] = body
156
+ end
157
+
158
+ protected
159
+
160
+ def create_parts_from_responses(m, responses) #:nodoc:
161
+ content = m.arguments['content'] ||= {}
162
+ responses.each do |part|
163
+ content[part[:content_type]] = part[:body]
164
+ end
165
+ end
166
+
167
+ end
168
+
169
+ # A set of methods that are useful when request needs to behave as Mail
170
+ class PostageApp::Request
171
+
172
+ attr_accessor :delivery_handler,
173
+ :perform_deliveries,
174
+ :raise_delivery_errors
175
+
176
+ # Either doing an actual send, or passing it along to Mail::TestMailer
177
+ # Probably not the best way as we're skipping way too many intermediate methods
178
+ def deliver
179
+ if @delivery_method == Mail::TestMailer
180
+ mailer = @delivery_method.new(nil)
181
+ mailer.deliver!(self)
182
+ else
183
+ self.send
184
+ end
185
+ end
186
+
187
+ # Not 100% on this, but I need to assign this so I can properly handle deliver method
188
+ def delivery_method(method = nil, settings = {})
189
+ @delivery_method = method
190
+ end
191
+
192
+ end