mandrill_queue 0.1.1

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