outbox 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -2,6 +2,7 @@
2
2
  *.rbc
3
3
  .bundle
4
4
  .config
5
+ .ruby-version
5
6
  .yardoc
6
7
  Gemfile.lock
7
8
  InstalledFiles
data/README.md CHANGED
@@ -1,7 +1,10 @@
1
1
  Outbox
2
2
  ======
3
3
 
4
- Outbox is a Rails Engine and generic interface for sending notifications using a variety of delivery methods, with built-in support for the most popular SaaS solutions.
4
+ [![Gem Version](https://badge.fury.io/rb/outbox.png)](http://badge.fury.io/rb/outbox)
5
+
6
+ Outbox is a factory for creating notifications in a variety of protocols, including: email, SMS, and push notifications. Each protocol is built as a generic interface where the actual delivery method or service can be configured independently of the message itself.
7
+
5
8
 
6
9
  Installation
7
10
  ------------
@@ -24,10 +27,88 @@ Or install it yourself as:
24
27
  $ gem install outbox
25
28
  ```
26
29
 
30
+ Support
31
+ -------
32
+
33
+ This gem is still in early development with plans to support email, SMS, and push notificaitons. As protocols and services are added, this support table will be updated:
34
+
35
+ ### Email
36
+
37
+ | Service | Alias | Client |
38
+ |-------------------------------------------|---------|------------|
39
+ | [Mail gem](https://github.com/mikel/mail) | `:mail` | MailClient |
40
+
41
+ ### SMS
42
+
43
+ TODO…
44
+
45
+ ### Push
46
+
47
+ TODO…
48
+
27
49
  Usage
28
50
  -----
29
51
 
30
- TODO: Write usage instructions here
52
+ Outbox is inspired by [Mail's](https://github.com/mikel/mail) syntax for creating emails.
53
+
54
+ ### Making a Message
55
+
56
+ An Outbox message is actually a factory for creating many different types of messages with the same **topic**. For example: a **topic** could be an event reminder in a calendar application. You want to send out essentially the same content (the reminder) as an email, SMS, and/or push notifications depending on user preferences:
57
+
58
+ ``` ruby
59
+ message = Outbox::Message.new do
60
+ email do
61
+ from 'noreply@myapp.com'
62
+ subject 'You have an upcoming event!'
63
+ end
64
+
65
+ sms do
66
+ from '+15557654321'
67
+ end
68
+
69
+ ios_push do
70
+ badget '+1'
71
+ sound 'default'
72
+ end
73
+
74
+ body "Don't forget, you have an upcoming event on 8/15/2013."
75
+ end
76
+
77
+ # This will deliver the message to User's given contact points.
78
+ message.deliver email: 'user@gmail.com', sms: '+15551234567', ios_push: 'FE66489F304DC75B8D6E8200DFF8A456E8DAEACEC428B427E9518741C92C6660'
79
+ ```
80
+
81
+ ### Making an email
82
+
83
+ Making just an email is done just how you would using the [Mail gem](https://github.com/mikel/mail), so look there for in-depth examples. Here's a simple one to get you started:
84
+
85
+ ``` ruby
86
+ email = Outbox::Messages::Email.new do
87
+ to 'user@gmail.com'
88
+ from 'noreply@myapp.com'
89
+ subject 'You have an upcoming event!'
90
+
91
+ text_part do
92
+ body "Don't forget, you have an upcoming event on 8/15/2013."
93
+ end
94
+
95
+ html_part do
96
+ body "<h1>Event Reminder</h1>..."
97
+ end
98
+ end
99
+
100
+ # Configure the client. If you use the MailClient, you can specify
101
+ # the actual delivery method:
102
+ email.client :mail, delivery_method: :smtp, smtp_settings: {}
103
+
104
+ # And deliver using the specified client
105
+ email.deliver
106
+ ```
107
+
108
+ Configuration
109
+ -------------
110
+
111
+ TODO...
31
112
 
32
113
  Contributing
33
114
  ------------
@@ -1,4 +1,21 @@
1
- require 'outbox/version'
2
-
3
1
  module Outbox
2
+ require 'outbox/errors'
3
+ require 'outbox/version'
4
+
5
+ autoload 'Accessor', 'outbox/accessor'
6
+ autoload 'Message', 'outbox/message'
7
+ autoload 'MessageClients', 'outbox/message_clients'
8
+ autoload 'MessageFields', 'outbox/message_fields'
9
+ autoload 'MessageTypes', 'outbox/message_types'
10
+
11
+ module Clients
12
+ autoload 'Base', 'outbox/clients/base'
13
+ autoload 'MailClient', 'outbox/clients/mail_client'
14
+ autoload 'TestClient', 'outbox/clients/test_client'
15
+ end
16
+
17
+ module Messages
18
+ autoload 'Base', 'outbox/messages/base'
19
+ autoload 'Email', 'outbox/messages/email'
20
+ end
4
21
  end
@@ -0,0 +1,71 @@
1
+ module Outbox
2
+ # Accessor is a simple object for wrapping access to either a hash's keys
3
+ # or an object's properties. You can arbitrarily get/set either. Note that
4
+ # with hashes, the keys are symbolized and a new hash is created - so if you
5
+ # set properties you'll need to get the resulting hash from the #object
6
+ # method.
7
+ #
8
+ # Example:
9
+ #
10
+ # hash = { :a => 1, 'b' => 2 }
11
+ # hash_accessor = Outbox::Accessor.new(hash)
12
+ # hash_accessor[:a] #=> 1
13
+ # hash_accessor[:b] #=> 2
14
+ # hash_accessor[:c] #=> nil
15
+ # hash_accessor[:c] = 3
16
+ # hash_accessor.object[:c] #=> 3
17
+ # hash_accessor.object #=> { a: 1, b: 2, c: 3 }
18
+ #
19
+ # object = OpenStruct.new
20
+ # object.a = 1
21
+ # object.b = 2
22
+ # object_accessor = Outbox::Accessor.new(object)
23
+ # object_accessor[:a] #=> 1
24
+ # object_accessor[:b] #=> 2
25
+ # object_accessor[:c] #=> nil
26
+ # object_accessor[:c] = 3
27
+ # object_accessor.object.c #=> 3
28
+ class Accessor
29
+ attr_reader :object
30
+
31
+ def initialize(object)
32
+ if object.instance_of? Hash
33
+ @object = convert_keys(object)
34
+ else
35
+ @object = object
36
+ end
37
+ end
38
+
39
+ def []=(key, value)
40
+ setter = "#{key}="
41
+ if @object.respond_to?(setter)
42
+ @object.public_send(setter, value)
43
+ elsif @object.respond_to? :[]=
44
+ @object[convert_key(key)] = value
45
+ end
46
+ end
47
+
48
+ def [](key)
49
+ key = convert_key(key)
50
+ if @object.respond_to?(key)
51
+ @object.public_send(key)
52
+ elsif @object.respond_to? :[]
53
+ @object[key]
54
+ end
55
+ end
56
+
57
+ protected
58
+
59
+ def convert_keys(hash)
60
+ result = {}
61
+ hash.each_key do |key|
62
+ result[convert_key(key)] = hash[key]
63
+ end
64
+ result
65
+ end
66
+
67
+ def convert_key(key)
68
+ key.to_sym rescue key
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,39 @@
1
+ module Outbox
2
+ module Clients
3
+ class Base
4
+ attr_reader :settings
5
+
6
+ # Sets default settings for the client.
7
+ #
8
+ # MailClient.defaults delivery_method: :sendmail
9
+ # client = MailClient.new
10
+ # client.settings[:delivery_method] #=> :sendmail
11
+ def self.defaults(defaults = nil)
12
+ @defaults ||= {}
13
+
14
+ if defaults.nil?
15
+ @defaults
16
+ else
17
+ @defaults.merge!(defaults)
18
+ end
19
+ end
20
+
21
+ # Creates a new client instance. Settings can be configured per
22
+ # instance by passing in a hash.
23
+ #
24
+ # client = MailClient.new delivery_method: :sendmail
25
+ # client.settings[:delivery_method] #=> :sendmail
26
+ def initialize(settings = nil)
27
+ @settings = self.class.defaults.dup
28
+ @settings.merge! settings if settings
29
+ end
30
+
31
+ # Delivers the given message.
32
+ #
33
+ # Subclasses must provide an implementation of this method.
34
+ def deliver(message)
35
+ raise NotImplementedError, 'Subclasses must implement a deliver method'
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,40 @@
1
+ module Outbox
2
+ module Clients
3
+ class MailClient < Base
4
+ defaults delivery_method: :smtp
5
+
6
+ # Returns the configured delivery method.
7
+ def delivery_method
8
+ settings[:delivery_method]
9
+ end
10
+
11
+ # Returns the configured delivery method settings. This will also check
12
+ # the Rails-style #{delivery_method}_settings key as well.
13
+ #
14
+ # client = Outbox::Clients::MailClient.new delivery_method: :sendmail, delivery_method_settings: {
15
+ # location: '/usr/bin/sendmail'
16
+ # }
17
+ # client.delivery_method_settings #=> { location: '/usr/bin/sendmail' }
18
+ #
19
+ # client = Outbox::Clients::MailClient.new delivery_method: :sendmail, sendmail_settings: {
20
+ # location: '/usr/bin/sendmail'
21
+ # }
22
+ # client.delivery_method_settings #=> { location: '/usr/bin/sendmail' }
23
+ def delivery_method_settings
24
+ settings[:delivery_method_settings] || settings[:"#{delivery_method}_settings"]
25
+ end
26
+
27
+ def deliver(email)
28
+ message = create_message_from_email(email)
29
+ message.delivery_method(delivery_method, delivery_method_settings)
30
+ message.deliver
31
+ end
32
+
33
+ private
34
+
35
+ def create_message_from_email(email)
36
+ email.message_object.dup
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,20 @@
1
+ module Outbox
2
+ module Clients
3
+ # The TestClient is a bare bones client that does nothing. It is useful
4
+ # when you are testing.
5
+ #
6
+ # It also provides a template of the minimum methods required to make
7
+ # a custom client.
8
+ class TestClient < Base
9
+ # Provides a store of all the message sent with the TestClient so you
10
+ # can check them.
11
+ def self.deliveries
12
+ @@deliveries ||= []
13
+ end
14
+
15
+ def deliver(message)
16
+ self.class.deliveries << message
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,5 @@
1
+ module Outbox
2
+ # Raised when a message is missing data for a required field.
3
+ class MissingRequiredFieldError < StandardError
4
+ end
5
+ end
@@ -0,0 +1,47 @@
1
+ module Outbox
2
+ class Message
3
+ include MessageTypes
4
+
5
+ register_message_type :email, Outbox::Messages::Email
6
+
7
+ # Make a new message. Every message can be created using a hash,
8
+ # block, or direct assignment.
9
+ #
10
+ # message = Message.new do
11
+ # email do
12
+ # subject 'Subject'
13
+ # end
14
+ # end
15
+ # message = Message.new email: { subject: 'Subject' }
16
+ # message = Message.new
17
+ # message.email = Email.new subject: 'Subject'
18
+ def initialize(message_type_values = nil, &block)
19
+ if block_given?
20
+ instance_eval(&block)
21
+ else
22
+ assign_message_type_values(message_type_values) unless message_type_values.nil?
23
+ end
24
+ end
25
+
26
+ # Delivers all of the messages to the given 'audience'. An 'audience' object
27
+ # can be a hash or an object that responds to the current message types. Only
28
+ # the message types specified in the 'audience' object will be sent to.
29
+ #
30
+ # message.deliver email: 'hello@example.com', sms: '+15555555555'
31
+ # audience = OpenStruct.new
32
+ # audience.email = 'hello@example.com'
33
+ # audience.sms = '+15555555555'
34
+ # message.deliver(audience)
35
+ def deliver(audience)
36
+ audience = Outbox::Accessor.new(audience)
37
+
38
+ self.class.message_types.each_key do |message_type|
39
+ message = self.public_send(message_type)
40
+ next if message.nil?
41
+
42
+ recipient = audience[message_type]
43
+ message.deliver(recipient) if recipient
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,70 @@
1
+ module Outbox
2
+ module MessageClients
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ # Returns the default client for the message type.
9
+ #
10
+ # Email.default_client #=> #<Outbox::Clients::Mail>
11
+ #
12
+ # Also allows you to set the default client using an alias, with optoins.
13
+ #
14
+ # Email.default_client :test, option: 'foo'
15
+ # Email.default_client #=> #<Outbox::Clients::TestClient>
16
+ def default_client(client = nil, options = nil)
17
+ if client.nil?
18
+ @default_client
19
+ else
20
+ @default_client = get_client(client, options)
21
+ end
22
+ end
23
+
24
+ # Registers a client class with an alias.
25
+ #
26
+ # Email.register_client_alias :mandrill, MandrillClient
27
+ # Email.default_client :mandrill, mandrill_option: 'foo'
28
+ def register_client_alias(name, client)
29
+ registered_client_aliases[name.to_sym] = client
30
+ end
31
+
32
+ # Returns a hash of client aliases, where the key is the alias and
33
+ # the value is client class.
34
+ def registered_client_aliases
35
+ @registered_client_aliases ||= { test: Outbox::Clients::TestClient }
36
+ end
37
+
38
+ protected
39
+
40
+ def get_client(client, options = nil)
41
+ case client
42
+ when Symbol, String
43
+ client = registered_client_aliases[client.to_sym]
44
+ end
45
+
46
+ if client.instance_of?(Class)
47
+ client.new(options)
48
+ else
49
+ client
50
+ end
51
+ end
52
+ end
53
+
54
+ # Returns the message's client.
55
+ #
56
+ # message.client #=> #<Outbox::Clients::Mail>
57
+ #
58
+ # Also allows you set the instance's client using an alias, with options.
59
+ #
60
+ # message.client :test, option: 'foo'
61
+ # message.client #=> #<Outbox::Clients::TestClient>
62
+ def client(client = nil, options = nil)
63
+ if client.nil?
64
+ @client
65
+ else
66
+ @client = self.class.send(:get_client, client, options)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,209 @@
1
+ module Outbox
2
+ module MessageFields
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ # Sets default values for defined fields.
9
+ #
10
+ # Email.defaults from: 'bob@example.com'
11
+ # message = Email.new
12
+ # message.from #=> 'bob@example.com'
13
+ def defaults(defaults = nil)
14
+ @defaults ||= {}
15
+
16
+ if defaults.nil?
17
+ @defaults
18
+ else
19
+ @defaults.merge!(defaults)
20
+ end
21
+ end
22
+
23
+ # Returns the defined fields for this message type.
24
+ #
25
+ # class SomeMessageType < Outbox::Messages::Base
26
+ # field :to
27
+ # field :from
28
+ # end
29
+ #
30
+ # SomeMessageType.fields #=> [:to, :from]
31
+ #
32
+ # Also allows you to define multiple fields at once.
33
+ #
34
+ # class SomeMessageType < Outbox::Messages::Base
35
+ # fields :to, :from, required: true
36
+ # end
37
+ #
38
+ # message = SomeMessageType.new do
39
+ # to 'Bob'
40
+ # from 'John'
41
+ # end
42
+ # message.to #=> 'Bob'
43
+ # message.from #=> 'John'
44
+ # message.from = nil
45
+ # message.validate_fields #=> raises Outbox::MissingRequiredFieldError
46
+ def fields(*names)
47
+ if names.empty?
48
+ @fields ||= []
49
+ else
50
+ options = names.last.is_a?(Hash) ? names.pop : {}
51
+ names.flatten.each do |name|
52
+ field(name, options)
53
+ end
54
+ end
55
+ end
56
+
57
+ # Defines a 'field' which is a point of data for this type of data.
58
+ # Optionally you can set it to be required, or wether or not you want
59
+ # accessors defined for you. If you define your own accessors, make
60
+ # sure the reader also accepts a value that can be set, so it'll work
61
+ # with the block definition.
62
+ #
63
+ # class SomeMessageType < Outbox::Messages::base
64
+ # field :to, required: true
65
+ # field :body, accessor: false
66
+ #
67
+ # def body(value = nil)
68
+ # value ? self.body = value : @body
69
+ # end
70
+ #
71
+ # def body=(value)
72
+ # @body = parse_body(value)
73
+ # end
74
+ # end
75
+ #
76
+ # message = SomeMessageType.new do
77
+ # to 'Bob'
78
+ # end
79
+ # message.to #=> 'Bob'
80
+ # message.to = 'John'
81
+ # message.to #=> 'John'
82
+ # message.to = nil
83
+ # message.validate_fields #=> raises Outbox::MissingRequiredFieldError
84
+ def field(name, options = {})
85
+ name = name.to_sym
86
+ options = Outbox::Accessor.new(options)
87
+
88
+ fields.push(name)
89
+ required_fields.push(name) if options[:required]
90
+
91
+ unless options[:accessor] == false
92
+ define_field_reader(name) unless options[:reader] == false
93
+ define_field_writer(name) unless options[:writer] == false
94
+ end
95
+ end
96
+
97
+ # Returns an array of the required fields for a message type.
98
+ #
99
+ # class SomeMessageType < Outbox::Messages::Base
100
+ # field :to, required: true
101
+ # fields :from, :subject
102
+ # end
103
+ # SomeMessageType.required_fields #=> [:to]
104
+ #
105
+ # Also can be used an alias for defining fields that are required.
106
+ #
107
+ # class SomeMessageType < Outbox::Messages::Base
108
+ # required_fields :to, :from
109
+ # end
110
+ # SomeMessageType.required_fields #=> [:to, :from]
111
+ def required_fields(*names)
112
+ if names.empty?
113
+ @required_fields ||= []
114
+ else
115
+ options = names.last.is_a?(Hash) ? names.pop : {}
116
+ options[:required] = true
117
+ names << options
118
+ fields(*names)
119
+ end
120
+ end
121
+ alias :required_field :required_fields
122
+
123
+ protected
124
+
125
+ def define_field_reader(name)
126
+ define_method(name) do |value = nil|
127
+ if value.nil?
128
+ @fields[name]
129
+ else
130
+ @fields[name] = value
131
+ end
132
+ end
133
+ end
134
+
135
+ def define_field_writer(name)
136
+ define_method("#{name}=") do |value|
137
+ @fields[name] = value
138
+ end
139
+ end
140
+ end
141
+
142
+ # Read an arbitrary field.
143
+ #
144
+ # Example:
145
+ #
146
+ # message['foo'] = '1234'
147
+ # message['foo'] #=> '1234'
148
+ def [](name)
149
+ @fields[name.to_sym]
150
+ end
151
+
152
+ # Add an arbitray field.
153
+ #
154
+ # Example:
155
+ #
156
+ # message['foo'] = '1234'
157
+ # message['foo'] #=> '1234'
158
+ def []=(name, value)
159
+ @fields[name.to_sym] = value
160
+ end
161
+
162
+ # Returns a hash of the defined fields.
163
+ #
164
+ # class SomeMessageType < Outbox::Messages::Base
165
+ # fields :to, :from
166
+ # end
167
+ # message = SomeMessageType.new to: 'Bob'
168
+ # message.from 'John'
169
+ # message.fields #=> { to: 'Bob', from: 'John' }
170
+ #
171
+ # Also allows you to set fields if you pass in a hash.
172
+ #
173
+ # message.fields to: 'Bob', from: 'Sally'
174
+ # message.fields #=> { to: 'Bob', from: 'Sally' }
175
+ def fields(new_fields = nil)
176
+ if new_fields.nil?
177
+ fields = {}
178
+ self.class.fields.each do |field|
179
+ fields[field] = self.public_send(field)
180
+ end
181
+ fields
182
+ else
183
+ self.fields = new_fields
184
+ end
185
+ end
186
+
187
+ # Assigns the values of the given hash.
188
+ #
189
+ # message.to = 'Bob'
190
+ # message.fields = { from: 'Sally' }
191
+ # message.fields #=> { to: 'Bob', from: 'Sally' }
192
+ def fields=(new_fields)
193
+ new_fields.each do |field, value|
194
+ self.public_send(field, value) if self.respond_to?(field)
195
+ end
196
+ end
197
+
198
+ # Checks the current values of the fields and raises errors for any
199
+ # validation issues.
200
+ def validate_fields
201
+ self.class.required_fields.each do |field|
202
+ value = self.public_send(field)
203
+ if value.nil? || value.respond_to?(:empty?) && value.empty?
204
+ raise Outbox::MissingRequiredFieldError.new("Missing required field: #{field}")
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end