mandrill_queue 0.1.1

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.
data/Gemfile ADDED
@@ -0,0 +1,19 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :test do
6
+ gem 'rspec', require: false
7
+ gem 'guard-rspec', require: false
8
+
9
+ gem 'rb-inotify', :require => false
10
+ gem 'rb-fsevent', :require => false
11
+ gem 'rb-fchange', :require => false
12
+
13
+ gem 'ruby_gntp' if RUBY_PLATFORM =~ /darwin/i
14
+ gem 'libnotify' if RUBY_PLATFORM =~ /linux/i
15
+ end
16
+
17
+ # group :development, :test do
18
+ # gem 'debugger', platform: :ruby
19
+ # end
data/Guardfile ADDED
@@ -0,0 +1,5 @@
1
+ guard :rspec, cmd: 'rspec --order rand:$RANDOM' do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
4
+ watch('spec/spec_helper.rb') { "spec" }
5
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Stan
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,200 @@
1
+ ==========================
2
+ MandrillQueue::Mailer
3
+ ==========================
4
+ [![Build Status](https://travis-ci.org/fixate/mandrill_queue.png)](https://travis-ci.org/fixate/mandrill_queue)
5
+
6
+ DSL for sending mailers through Mailchimps Mandrill API. This gem enqueues the
7
+ message on a background worker (`Resque` only for now, but I want to refactor
8
+ so that it doesnt matter).
9
+
10
+ The DSL is modelled on the JSON api [here](https://mandrillapp.com/api/docs/messages.ruby.html#method=send-template).
11
+
12
+ ## The DSL
13
+
14
+ ```ruby
15
+ # app/mailers/my_mailer.rb
16
+ class MyMailer < MandrillQueue::Mailer
17
+ # Template names are inferred from the class_name (without Mailer) + the method
18
+ # name. Spaces are `sluggified`. If you want to override the prefixes use:
19
+ # template_prefix 'my-project'
20
+ # now templates will be 'my-project' + sluggified method
21
+ #
22
+ # template_prefix '' means no prefix will be used
23
+
24
+ # Set defaults for all methods here.
25
+ # The full DSL is available here so feel free to include `merge_vars`,
26
+ # `preserve_recipients`, `to` etc.
27
+ # Settings here have a lower precedence than method settings.
28
+ #
29
+ defaults do
30
+ # Setting the default template will disable implicit template names
31
+ # template 'master_template'
32
+ message do
33
+ from_email 'no-reply@mysite.com'
34
+ end
35
+
36
+ content do ... end
37
+ end
38
+
39
+ def welcome_dave
40
+ template 'welcome'
41
+
42
+ message do
43
+ to 'dave@amazabal.ls', 'Dave' # Name optional
44
+ cc 'davesmom@yahoo.com'
45
+ bcc 'daves-sis@gmail.com'
46
+
47
+ to [{email: 'another@person.com', name: 'Another'}, ...]
48
+ end
49
+
50
+ # Template content
51
+ # e.g. <div mc:edit="my_tag"></div>
52
+ content do
53
+ my_tag '<p>Content!</p>'
54
+ end
55
+ end
56
+
57
+ def welcome_many(users)
58
+ message do
59
+ # If the given parameter is an array of objects or hashes
60
+ # that respond_to?/has_key? `email` then we're good to go.
61
+ # Same goes for `name`. Second and third parameters override this
62
+ # e.g. to users, :work_email, :fullname
63
+ to users
64
+
65
+ # You can also do your own mapping (to, cc and bcc have the same DSL):
66
+ cc users do |user|
67
+ email user.work_email
68
+ name "#{user.firstname} #{user.lastname}"
69
+ end
70
+ end
71
+ end
72
+
73
+ def message_with_merge_vars(vars) # Template slug: my-message-with-merge-vars
74
+ message do
75
+ to 'some@email.com'
76
+
77
+ global_merge_vars do ... end
78
+ global_merge_vars {vars: 'everywhere'}
79
+
80
+ # Substitute *|MERGE_VARS|* in your template for given recipients
81
+ merge_vars 'some@email.com' do
82
+ key 'value'
83
+ whatever 'you want'
84
+ this_will 'only apply to some@email.com'
85
+ end
86
+ # If an array of objects/hashes contains an email method or key
87
+ # that will be used as the recipient and the rest as normal vars.
88
+ merge_vars vars #, :other_email_field
89
+
90
+ track_clicks false
91
+ end
92
+
93
+ # Use send_at/send_in (no difference) to tell Mandrill to delay sending
94
+ send_in 2.days.from_now
95
+ end
96
+
97
+ def html_message(html)
98
+ message do
99
+ # (omitted)
100
+ html "<html><body>#{html}</html></body>"
101
+ end
102
+ end
103
+
104
+ # Meanwhile in another file...
105
+ # maybe your controller...
106
+ # Just like ActionMailer (note the class method calls are handed to instance methods)
107
+ MyMailer.welcome_many(users).deliver
108
+ ```
109
+
110
+ ## Installation
111
+
112
+ You probably already know this bit:
113
+
114
+ gem 'resque' # Support for Sidekiq and writing custom adapters coming soon...
115
+ gem 'mandrill-queue'
116
+
117
+ but didn't know this (but it's optional):
118
+
119
+ rails g mandrill_queue:initializer
120
+
121
+ Global configuration options are documented in the initializer
122
+ but heres a taster:
123
+
124
+ ```ruby
125
+ MandrillQueue.configure do |config|
126
+ config.queue = :hipster_queue
127
+ # ...
128
+ end
129
+ ```
130
+
131
+ ## Setting up the worker
132
+
133
+ Run it with a rake task like so:
134
+
135
+ rake resque:work QUEUES=mailer
136
+
137
+ TODO: I still need to check that everything is OK when running the worker in Rails
138
+ since I run mine outside Rails as a lightweight worker using:
139
+
140
+ rake resque:work -r ./worker.rb QUEUES=mailer
141
+
142
+
143
+ ## Devise mailer integration
144
+
145
+ Since Mandrill_Queue quacks like ActionMailer where it counts, getting your Devise
146
+ mailers on Mandrill infrastructure is pretty easy. Here is my implementation:
147
+
148
+ ```ruby
149
+ class DeviseMailer < MandrillResque::Mailer
150
+ defaults do
151
+ message do
152
+ from_email Devise.mailer_sender
153
+ track_clicks false
154
+ track_opens false
155
+ view_content_link false
156
+ end
157
+ end
158
+
159
+ # Setup a template with the slug: devise-confirmation-instructions
160
+ def confirmation_instructions(record, token, opts = {})
161
+ confirm_url = user_confirmation_url(record, confirmation_token: token)
162
+ devise_mail(record, {name: record.fullname, confirmation_url: confirm_url})
163
+ end
164
+
165
+ # Slug: devise-reset-password-instructions
166
+ def reset_password_instructions(record, token, opts = {})
167
+ reset_url = edit_user_password_url(record, reset_password_token: token)
168
+ devise_mail(record, {name: record.fullname, reset_url: reset_url})
169
+ end
170
+
171
+ # Slug: devise-unlock-instructions
172
+ def unlock_instructions(record, token, opts = {})
173
+ unlock_url = user_unlock_url(record, unlock_token: token)
174
+ devise_mail(record, {name: record.fullname, unlock_url: unlock_url})
175
+ end
176
+
177
+ protected
178
+ def devise_mail(record, global_vars = {})
179
+ message do
180
+ to record, :email, :fullname
181
+
182
+ global_merge_vars global_vars
183
+ end
184
+ end
185
+ end
186
+ ```
187
+
188
+ ## TODO
189
+
190
+ 1. Refactor so that it can work with `Sidekiq` or a custom adapter - coming soon...
191
+ 2. Allow synchonous sending.
192
+ 2. Render ActionView views to mailers.
193
+
194
+ ## Contributing
195
+
196
+ 1. Fork it
197
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
198
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
199
+ 4. Push to the branch (`git push origin my-new-feature`)
200
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/tasks"
@@ -0,0 +1,103 @@
1
+ module MandrillQueue
2
+ class ArrayMetadata
3
+ class Var
4
+ Variables::DSL.include_as(self, :vars)
5
+
6
+ def initialize(recipient = nil, &block)
7
+ @_recipient = recipient
8
+ dsl(&block) if block_given?
9
+ end
10
+
11
+ def recipient=(value)
12
+ @_recipient = value
13
+ end
14
+
15
+ def recipient(value = nil)
16
+ @_recipient = value unless value.nil?
17
+ @_recipient
18
+ end
19
+
20
+ def to_hash(options = {})
21
+ hash = {}
22
+ hash[:rcpt] = recipient if options[:include_nils] || !recipient.nil?
23
+ hash[:vars] = vars.to_key_value_array(options) if options[:include_nils] || !@_vars.nil?
24
+ hash
25
+ end
26
+
27
+ def set!(hash)
28
+ @_recipient = hash[:rcpt]
29
+ @_vars = nil
30
+ vars.set!(hash[:vars]) unless hash[:vars].nil?
31
+ self
32
+ end
33
+
34
+ def dsl(&block)
35
+ vars.dsl(&block)
36
+ end
37
+
38
+ def validate(errors)
39
+ errors.push([:merge_vars, "Recipient cannot be empty for merge vars."]) if recipient.blank?
40
+ end
41
+ end
42
+
43
+ module DSL
44
+ def merge_vars(recipient = nil, &block)
45
+ @_merge_vars ||= MergeVars.new
46
+ @_merge_vars.dsl(recipient, &block) if !recipient.nil? || block_given?
47
+ block_given? ? self : @_merge_vars
48
+ end
49
+ end
50
+
51
+ def initialize
52
+ @_merge_vars = []
53
+ end
54
+
55
+ def add(*args, &block)
56
+ @_merge_vars << Var.new(*args, &block)
57
+ end
58
+
59
+ alias_method :dsl, :add
60
+
61
+ def to_a(options = {})
62
+ @_merge_vars.map do |v|
63
+ v.to_hash(options)
64
+ end
65
+ end
66
+
67
+ def first
68
+ @_merge_vars.first
69
+ end
70
+
71
+ def last
72
+ @_merge_vars.last
73
+ end
74
+
75
+ def [](index)
76
+ @_merge_vars[index]
77
+ end
78
+
79
+ def count
80
+ @_merge_vars.count
81
+ end
82
+
83
+ def merge_vars
84
+ @_merge_vars
85
+ end
86
+
87
+ def validate(errors)
88
+ @_merge_vars.each do |v|
89
+ v.validate(errors)
90
+ end
91
+ end
92
+
93
+ def set!(list)
94
+ @_merge_vars = list.map do |obj|
95
+ Var.new.set!(obj.symbolize_keys)
96
+ end
97
+
98
+ self
99
+ end
100
+ end
101
+
102
+ end
103
+ end
@@ -0,0 +1,42 @@
1
+ module MandrillQueue
2
+ class Configuration
3
+ ACCESSORS = [:message_defaults, :resque, :default_worker_class,
4
+ :default_queue, :api_key, :logger]
5
+ attr_accessor(*ACCESSORS)
6
+
7
+ def initialize(defaults = {}, &block)
8
+ set(defaults)
9
+ instance_eval(&block) if block_given?
10
+ end
11
+
12
+ def []=(key, value)
13
+ send("#{key}=", value)
14
+ end
15
+
16
+ def [](key)
17
+ send(key)
18
+ end
19
+
20
+ def reset
21
+ ACCESSORS.each do |key|
22
+ send("#{key}=", nil)
23
+ end
24
+
25
+ yield self if block_given?
26
+ end
27
+
28
+ def each_key(&block)
29
+ ACCESSORS.each(&block)
30
+ end
31
+
32
+ def set(hash)
33
+ each_key do |k, v|
34
+ send("#{k}=", hash[k])
35
+ end
36
+ end
37
+
38
+ def self.accessors
39
+ ACCESSORS
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,15 @@
1
+ require 'active_support/core_ext/string'
2
+ require 'active_support/core_ext/hash'
3
+ require 'active_support/core_ext/array'
4
+
5
+ class String
6
+ def sluggify
7
+ value = self
8
+ value.gsub!(/[']+/, '')
9
+ value.gsub!(/\W+/, ' ')
10
+ value.strip!
11
+ value.downcase!
12
+ value.gsub!(/[^A-Za-z0-9]/, '-')
13
+ value
14
+ end
15
+ end
@@ -0,0 +1,24 @@
1
+ module MandrillQueue
2
+ class Error < ::StandardError; end
3
+
4
+ class MandrillValidationError < Error
5
+ def initialize(errors)
6
+ @_errors = errors
7
+ super(message)
8
+ end
9
+
10
+ def message
11
+ <<-TXT
12
+ Validation Errors:
13
+ #{@_errors.inject(''){ |s, (name, e)| s += "\n- [#{name}]: #{e}" }}
14
+ TXT
15
+ end
16
+ end
17
+
18
+ class MessageError < Error; end
19
+
20
+ class VariableError < Error; end
21
+ class VariableNotSetError < VariableError; end
22
+
23
+ class RecipientDataError < Error; end
24
+ end
@@ -0,0 +1,51 @@
1
+ require 'pp'
2
+ require 'stringio'
3
+
4
+ module MandrillQueue
5
+ module Logging
6
+ def logging
7
+ @_logger ||= MandrillQueue.configuration.logger || begin
8
+ MandrillQueue.resque.constants.include?(:Logging) ? MandrillQueue.resque::Logging : nil
9
+ end
10
+ end
11
+
12
+ def pretty(obj)
13
+ s = StringIO.new
14
+ PP.pp(obj, s)
15
+ s.rewind
16
+ s.read
17
+ end
18
+
19
+ def result_formatter(r)
20
+ <<-TXT
21
+ ID: #{r['_id']}
22
+ EMAIL: #{r['email']}
23
+ STATUS: #{r['status']}
24
+ ---
25
+ TXT
26
+ end
27
+
28
+ def log_results(result)
29
+ errors = []
30
+ formatted = result.map do |r|
31
+ unless ['sent', 'queued'].include?(r['status'])
32
+ errors << result_formatter(r)
33
+ end
34
+
35
+ result_formatter(r)
36
+ end
37
+
38
+ logging.debug <<-TXT.tr("\t", '')
39
+ \n*******************************************
40
+ #{formatted.join("\n")}
41
+ *******************************************
42
+ TXT
43
+
44
+ if errors.empty?
45
+ logging.info("#{result.count} message(s) successfully sent.")
46
+ else
47
+ logging.error("The following messages were not sent:\n#{errors.join("\n")}")
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,184 @@
1
+ require 'mandrill_queue'
2
+ require 'mandrill_queue/errors'
3
+ require 'mandrill_queue/message'
4
+ require 'mandrill_queue/variables'
5
+
6
+ module MandrillQueue
7
+ class Mailer
8
+ class << self
9
+ def respond_to?(method, include_private = false)
10
+ super || action_methods.include?(method)
11
+ end
12
+
13
+ def action_methods
14
+ klass = self
15
+ klass = klass.superclass until klass == MandrillQueue::Mailer
16
+ return [] if self == klass
17
+ self.public_instance_methods(true) -
18
+ klass.public_instance_methods(true)
19
+ end
20
+
21
+ def method_missing(method, *args)
22
+ if respond_to?(method)
23
+ mailer = new(defaults)
24
+ mailer.send(method, *args)
25
+ mailer.template(template_from_method(method)) if mailer.template.blank? &&
26
+ !mailer.message.content_message?
27
+ mailer
28
+ else
29
+ super
30
+ end
31
+ end
32
+
33
+ def configuration
34
+ MandrillQueue.configuration
35
+ end
36
+
37
+ def defaults(&block)
38
+ return @_defaults ||= {} unless block_given?
39
+
40
+ mailer = new
41
+ @_in_defaults = true
42
+ mailer.instance_eval(&block)
43
+ @_defaults = mailer.to_hash
44
+ ensure
45
+ @_in_defaults = false
46
+ end
47
+
48
+ def defaults=(hash)
49
+ @_defaults = hash
50
+ end
51
+
52
+ def message_defaults
53
+ md = configuration.message_defaults
54
+ md.merge!(defaults[:message]) unless @_in_defaults || defaults[:message].nil?
55
+ md
56
+ end
57
+
58
+ def template_prefix(*args)
59
+ @template_prefix = args.first unless args.count == 0
60
+ if @template_prefix.nil?
61
+ "#{self.name.chomp('Mailer').sluggify}-"
62
+ else
63
+ @template_prefix
64
+ end
65
+ end
66
+
67
+ def all_templates
68
+ action_methods.map do |method|
69
+ template_from_method(method)
70
+ end
71
+ end
72
+
73
+ private
74
+ def template_from_method(method)
75
+ template = defaults[:template].blank? ? method.to_s.sluggify : defaults[:template]
76
+ template_prefix + template
77
+ end
78
+ end # End Singleton
79
+
80
+ ACCESSORS = [:template, :send_at]
81
+
82
+ def initialize(values = nil)
83
+ set!(values) unless values.nil?
84
+ end
85
+
86
+ def reset!
87
+ ACCESSORS.each do |key|
88
+ instance_variable_set("@#{key}", nil)
89
+ end
90
+ @_message = nil
91
+ @_content = nil
92
+ self
93
+ end
94
+
95
+ def message(&block)
96
+ @_message ||= Message::Internal.new(self.class.message_defaults)
97
+ @_message.dsl(&block) if block_given?
98
+ block_given? ? self : @_message
99
+ end
100
+
101
+ # Define setters
102
+ ACCESSORS.each do |key|
103
+ define_method key do |value = nil|
104
+ var = "@#{key}".to_sym
105
+ if value.nil?
106
+ instance_variable_get(var)
107
+ else
108
+ instance_variable_set(var, value)
109
+ self
110
+ end
111
+ end
112
+ end
113
+
114
+ alias :send_in :send_at
115
+
116
+ def worker_class
117
+ self.class.configuration.default_worker_class || ::MandrillQueue::Worker
118
+ end
119
+
120
+ def queue
121
+ @_queue ||= \
122
+
123
+ if instance_variable_defined?(:@queue)
124
+ instance_variable_get(:@queue)
125
+ elsif worker_class.instance_variable_defined?(:@queue)
126
+ worker_class.instance_variable_get(:@queue)
127
+ elsif worker_class.respond_to?(:queue)
128
+ worker_class.queue
129
+ else
130
+ self.class.configuration.default_queue || :mailer
131
+ end
132
+ end
133
+
134
+ def deliver
135
+ validate!
136
+ MandrillQueue.resque.enqueue_to(queue, worker_class, to_hash)
137
+ end
138
+
139
+ def to_hash(options = {})
140
+ hash = {}
141
+ ACCESSORS.each do |key|
142
+ value = instance_variable_get("@#{key}".to_sym)
143
+ hash[key] = value if options[:include_nil] || !value.nil?
144
+ end
145
+
146
+ hash[:message] = message.to_hash(options) rescue nil if !@_message.nil? || options[:include_nils]
147
+ hash[:content] = content.to_key_value_array(options) rescue nil if !@_content.nil? || options[:include_nils]
148
+ hash
149
+ end
150
+
151
+ def set!(hash)
152
+ hash.symbolize_keys!
153
+ ACCESSORS.each do |key|
154
+ instance_variable_set("@#{key}", hash[key])
155
+ end
156
+
157
+ message.set!(hash[:message]) unless hash[:message].nil?
158
+ content.set!(hash[:content]) unless hash[:content].nil?
159
+ self
160
+ end
161
+
162
+ alias_method :dsl, :instance_eval
163
+
164
+ def use_defaults!
165
+ set!(self.class.defaults) unless self.class.defaults.nil?
166
+ self
167
+ end
168
+
169
+ def validate!
170
+ errors = []
171
+ message.validate(errors) unless @_message.nil?
172
+
173
+ raise MandrillValidationError.new(errors) unless errors.empty?
174
+ self
175
+ end
176
+
177
+ # Include variable DSL at end of class
178
+ Variables::DSL.include_as(self, :content)
179
+ end
180
+ end
181
+
182
+ if defined?(ActiveSupport)
183
+ ActiveSupport.run_load_hooks(:mandrill_queue, MandrillQueue::Mailer)
184
+ end
@@ -0,0 +1,25 @@
1
+ require 'mandrill'
2
+ require 'mandrill_queue'
3
+
4
+ module MandrillQueue
5
+ module MandrillApi
6
+ class Error < ::StandardError; end
7
+
8
+ def configuration
9
+ MandrillQueue.configuration
10
+ end
11
+
12
+ def mandrill
13
+ @_api ||= begin
14
+ if configuration.api_key.nil?
15
+ raise MandrillQueue::Api::Error, <<-ERR
16
+ An Api key has not been configured. Please configure on as follows in an initializer:
17
+ MandrillQueue.configure do { |c| c.api_key = 'xxxxxxxxxxxxxx' }
18
+ ERR
19
+ end
20
+
21
+ Mandrill::API.new(configuration.api_key)
22
+ end
23
+ end
24
+ end
25
+ end