postageapp 0.0.0 → 1.0.0

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