contactology 0.0.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.
Files changed (86) hide show
  1. data/.gitignore +5 -0
  2. data/.infinity_test +16 -0
  3. data/.rspec +3 -0
  4. data/.rvmrc +41 -0
  5. data/.watchr +36 -0
  6. data/Gemfile +6 -0
  7. data/Rakefile +1 -0
  8. data/contactology.gemspec +28 -0
  9. data/lib/contactology.rb +125 -0
  10. data/lib/contactology/api.rb +80 -0
  11. data/lib/contactology/basic_object.rb +21 -0
  12. data/lib/contactology/campaign.rb +127 -0
  13. data/lib/contactology/campaign/preview.rb +11 -0
  14. data/lib/contactology/campaigns.rb +2 -0
  15. data/lib/contactology/campaigns/standard.rb +80 -0
  16. data/lib/contactology/campaigns/transactional.rb +58 -0
  17. data/lib/contactology/configuration.rb +42 -0
  18. data/lib/contactology/contact.rb +193 -0
  19. data/lib/contactology/errors.rb +4 -0
  20. data/lib/contactology/issue.rb +24 -0
  21. data/lib/contactology/issues.rb +18 -0
  22. data/lib/contactology/list.rb +192 -0
  23. data/lib/contactology/list_proxy.rb +25 -0
  24. data/lib/contactology/parser.rb +5 -0
  25. data/lib/contactology/send_result.rb +35 -0
  26. data/lib/contactology/stash.rb +29 -0
  27. data/lib/contactology/transactional_message.rb +38 -0
  28. data/lib/contactology/version.rb +3 -0
  29. data/spec/factories/campaigns.rb +18 -0
  30. data/spec/factories/contacts.rb +3 -0
  31. data/spec/factories/issues.rb +9 -0
  32. data/spec/factories/lists.rb +3 -0
  33. data/spec/factories/transactional_messages.rb +5 -0
  34. data/spec/fixtures/net/campaign/destroy.yml +246 -0
  35. data/spec/fixtures/net/campaign/find/failure.yml +36 -0
  36. data/spec/fixtures/net/campaign/find/success.yml +176 -0
  37. data/spec/fixtures/net/campaign/find_by_name/failure.yml +36 -0
  38. data/spec/fixtures/net/campaign/find_by_name/success.yml +211 -0
  39. data/spec/fixtures/net/campaign/preview.yml +106 -0
  40. data/spec/fixtures/net/campaigns/standard/create/failure.yml +106 -0
  41. data/spec/fixtures/net/campaigns/standard/create/invalid.yml +141 -0
  42. data/spec/fixtures/net/campaigns/standard/create/success.yml +176 -0
  43. data/spec/fixtures/net/campaigns/standard/send_campaign/failure.yml +316 -0
  44. data/spec/fixtures/net/campaigns/standard/send_campaign/success.yml +316 -0
  45. data/spec/fixtures/net/campaigns/transactional/create/failure.yml +36 -0
  46. data/spec/fixtures/net/campaigns/transactional/create/success.yml +71 -0
  47. data/spec/fixtures/net/contact/active.yml +106 -0
  48. data/spec/fixtures/net/contact/change_email/success.yml +176 -0
  49. data/spec/fixtures/net/contact/change_email/unknown.yml +36 -0
  50. data/spec/fixtures/net/contact/create.yml +106 -0
  51. data/spec/fixtures/net/contact/destroy.yml +141 -0
  52. data/spec/fixtures/net/contact/find/active.yml +106 -0
  53. data/spec/fixtures/net/contact/find/suppressed.yml +141 -0
  54. data/spec/fixtures/net/contact/find/unknown.yml +36 -0
  55. data/spec/fixtures/net/contact/lists/empty.yml +106 -0
  56. data/spec/fixtures/net/contact/lists/full.yml +246 -0
  57. data/spec/fixtures/net/contact/lists/unknown.yml +71 -0
  58. data/spec/fixtures/net/contact/suppress.yml +141 -0
  59. data/spec/fixtures/net/list/all.yml +141 -0
  60. data/spec/fixtures/net/list/create.yml +106 -0
  61. data/spec/fixtures/net/list/destroy.yml +176 -0
  62. data/spec/fixtures/net/list/find/success.yml +141 -0
  63. data/spec/fixtures/net/list/find/unknown.yml +36 -0
  64. data/spec/fixtures/net/list/import/success.yml +351 -0
  65. data/spec/fixtures/net/list/subscribe/success.yml +211 -0
  66. data/spec/fixtures/net/list/unsubscribe/success.yml +246 -0
  67. data/spec/fixtures/net/transactional_message/send_message/failure.yml +176 -0
  68. data/spec/fixtures/net/transactional_message/send_message/success.yml +176 -0
  69. data/spec/models/contactology/api_spec.rb +51 -0
  70. data/spec/models/contactology/campaign_spec.rb +75 -0
  71. data/spec/models/contactology/campaigns/standard_spec.rb +84 -0
  72. data/spec/models/contactology/campaigns/transactional_spec.rb +28 -0
  73. data/spec/models/contactology/configuration_spec.rb +29 -0
  74. data/spec/models/contactology/contact_spec.rb +175 -0
  75. data/spec/models/contactology/issues_spec.rb +34 -0
  76. data/spec/models/contactology/list_spec.rb +125 -0
  77. data/spec/models/contactology/send_result_spec.rb +65 -0
  78. data/spec/models/contactology/stash_spec.rb +65 -0
  79. data/spec/models/contactology/transactional_message_spec.rb +65 -0
  80. data/spec/models/contactology_spec.rb +67 -0
  81. data/spec/requests/contacts_spec.rb +4 -0
  82. data/spec/spec_helper.rb +15 -0
  83. data/spec/support/contactology.rb +34 -0
  84. data/spec/support/factory_girl.rb +19 -0
  85. data/spec/support/vcr.rb +11 -0
  86. metadata +282 -0
@@ -0,0 +1,11 @@
1
+ require 'contactology/stash'
2
+
3
+ module Contactology
4
+ class Campaign
5
+ class Preview < Contactology::Stash
6
+ property :html
7
+ property :text
8
+ property :links
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,2 @@
1
+ module Contactology::Campaigns
2
+ end
@@ -0,0 +1,80 @@
1
+ require 'contactology/campaign'
2
+ require 'contactology/campaigns'
3
+
4
+ class Contactology::Campaigns::Standard < Contactology::Campaign
5
+ property :content, :required => true
6
+ property :name, :from => :campaignName, :required => true
7
+ property :recipients, :required => true
8
+ property :sender_email, :from => :senderEmail, :required => true
9
+ property :sender_name, :from => :senderName, :required => true
10
+ property :subject, :required => true
11
+
12
+
13
+ def []=(property, value)
14
+ if property.to_s == 'recipients'
15
+ super 'recipients', normalize_recipients(value)
16
+ else
17
+ super
18
+ end
19
+ end
20
+
21
+ def save(options = {})
22
+ self.class.query('Campaign_Create_Standard', options.merge({
23
+ 'recipients' => recipients,
24
+ 'campaignName' => name,
25
+ 'subject' => subject,
26
+ 'senderEmail' => sender_email,
27
+ 'senderName' => sender_name,
28
+ 'content' => content,
29
+ 'optionalParameters' => {
30
+ 'authenticate' => authenticate,
31
+ 'replyToEmail' => reply_to_email,
32
+ 'replyToName' => reply_to_name,
33
+ 'trackReplies' => track_replies,
34
+ 'recipientName' => recipient_name,
35
+ 'showInArchive' => show_in_archive,
36
+ 'viewInBrowser' => view_in_browser,
37
+ 'trackOpens' => track_opens,
38
+ 'trackClickThruHTML' => track_click_thru_html,
39
+ 'trackClickThruText' => track_click_thru_text,
40
+ 'googleAnalyticsName' => google_analytics_name,
41
+ 'clickTaleName' => click_tale_name,
42
+ 'clickTaleCustomFields' => click_tale_custom_fields,
43
+ 'automaticTweet' => automatic_tweet
44
+ },
45
+ :on_error => false,
46
+ :on_timeout => false,
47
+ :on_success => Proc.new { |response| self.id = response; self }
48
+ }))
49
+ end
50
+
51
+ ##
52
+ # Public: Sends the campaign.
53
+ #
54
+ # Returns an empty collection when successful.
55
+ # Returns a collection of issues when unsuccessful.
56
+ #
57
+ def send_campaign(options = {})
58
+ self.class.query('Campaign_Send', options.merge({
59
+ 'campaignId' => id,
60
+ :on_error => Proc.new { |response| process_send_campaign_result response },
61
+ :on_timeout => process_send_campaign_result({'success' => false, 'issues' => [{'text' => 'Connection timeout'}]}),
62
+ :on_success => Proc.new { |response| process_send_campaign_result response }
63
+ }))
64
+ end
65
+
66
+
67
+ private
68
+
69
+
70
+ def normalize_recipients(input)
71
+ input = [input].flatten.compact.uniq
72
+ input = input.select { |i| i.kind_of?(Contactology::List) }.collect { |list| list.id }
73
+ input = {'list' => input.size == 1 ? input.first : input}
74
+ input
75
+ end
76
+
77
+ def process_send_campaign_result(data)
78
+ Contactology::SendResult.new(data)
79
+ end
80
+ end
@@ -0,0 +1,58 @@
1
+ require 'contactology/campaign'
2
+ require 'contactology/campaigns'
3
+
4
+ class Contactology::Campaigns::Transactional < Contactology::Campaign
5
+ property :test_contact, :from => :testContact
6
+ property :test_replacements, :from => :testReplacements
7
+
8
+
9
+ def self.create(attributes, options = {})
10
+ new(attributes).save(options)
11
+ end
12
+
13
+
14
+ ##
15
+ # Public: Stores the campaign information onto Contactology.
16
+ #
17
+ # Returns a Transactional instance with the campaign ID when successful.
18
+ # Returns a Contactology::SendResult instance with issues on failure.
19
+ #
20
+ def save(options = {})
21
+ self.class.query('Campaign_Create_Transactional', options.merge({
22
+ 'campaignName' => name,
23
+ 'content' => content,
24
+ 'senderEmail' => sender_email,
25
+ 'senderName' => sender_name,
26
+ 'subject' => subject,
27
+ 'testContact' => test_contact,
28
+ 'testReplacements' => test_replacements,
29
+ 'optionalParameters' => {
30
+ 'authenticate' => authenticate,
31
+ 'automaticTweet' => automatic_tweet,
32
+ 'clickTaleCustomFields' => click_tale_custom_fields,
33
+ 'clickTaleName' => click_tale_name,
34
+ 'googleAnalyticsName' => google_analytics_name,
35
+ 'recipientName' => recipient_name,
36
+ 'replyToEmail' => reply_to_email,
37
+ 'replyToName' => reply_to_name,
38
+ 'showInArchive' => show_in_archive,
39
+ 'trackClickThruHTML' => track_click_thru_html,
40
+ 'trackClickThruText' => track_click_thru_text,
41
+ 'trackOpens' => track_opens,
42
+ 'trackReplies' => track_replies,
43
+ 'viewInBrowser' => view_in_browser
44
+ },
45
+ :on_error => Proc.new { |response| process_send_campaign_result response },
46
+ :on_timeout => process_send_campaign_result('success' => false, 'issues' => [{'text' => 'Connection error'}]),
47
+ :on_success => Proc.new { |response| self.id = response; self }
48
+ }))
49
+ end
50
+
51
+
52
+ private
53
+
54
+
55
+ def process_send_campaign_result(data)
56
+ Contactology::SendResult.new(data)
57
+ end
58
+ end
@@ -0,0 +1,42 @@
1
+ ##
2
+ # Holds configuration objects used by the library. The Contactology module
3
+ # holds reference to a default Configuration, which will be used when no
4
+ # explicit configurations are given to a particular query.
5
+ #
6
+ class Contactology::Configuration
7
+ ##
8
+ # Public: Set the API endpoint to be used.
9
+ #
10
+ # endpoint - The String to use for the API endpoint.
11
+ #
12
+ # Returns nothing.
13
+ #
14
+ attr_writer :endpoint
15
+
16
+ ##
17
+ # Public: Get the API key used for queries.
18
+ #
19
+ # Returns the String of the key.
20
+ #
21
+ attr_reader :key
22
+ #
23
+ ##
24
+ # Public: Set the API key used for queries.
25
+ #
26
+ # key - The String to use for the API key.
27
+ #
28
+ # Returns nothing.
29
+ #
30
+ attr_writer :key
31
+
32
+ ##
33
+ # Public: Get the API endpoint used by the configuration. Unless explicitly
34
+ # set, the endpoint will default to the official production endpoint at
35
+ # "https://api.emailcampaigns.net/2/REST/".
36
+ #
37
+ # Returns the String for the API endpoint.
38
+ #
39
+ def endpoint
40
+ @endpoint ||= 'https://api.emailcampaigns.net/2/REST/'
41
+ end
42
+ end
@@ -0,0 +1,193 @@
1
+ require 'contactology/stash'
2
+ require 'contactology/api'
3
+
4
+ module Contactology
5
+ ##
6
+ # Represents a Contact on Contactology. Contacts always must have an email
7
+ # address and then may optionally carry other custom fields.
8
+ #
9
+ class Contact < Contactology::Stash
10
+ extend API
11
+
12
+ property :id, :from => :contactId
13
+ property :email, :required => true
14
+ property :status
15
+ property :source
16
+ property :custom_fields, :from => :customFields
17
+
18
+
19
+ ##
20
+ # Public: Create a new contact. The only required attribute is an :email
21
+ # address.
22
+ #
23
+ # Examples
24
+ #
25
+ # Contactology::Contact.create(:email => 'joe@example.local')
26
+ # # => #<Contactology::Contact:0x000... @email="joe@example.com" ...>
27
+ #
28
+ # Returns a Contactology::Contact instance when successful.
29
+ # Returns false when unsuccessful or a network error occurs.
30
+ #
31
+ def self.create(attributes, options = {})
32
+ contact = new(attributes)
33
+ contact.save(options) ? contact : false
34
+ end
35
+
36
+ ##
37
+ # Public: Lookup a contact's information by email address.
38
+ #
39
+ # Examples
40
+ #
41
+ # Contactology::Contact.find('joe@example.local')
42
+ # # => #<Contactology::Contact:0x000... @email="joe@example.local" ...>
43
+ #
44
+ # Returns a Contactology::Contact instance when a match is found. Otherise,
45
+ # returns nil.
46
+ #
47
+ def self.find(email, options = {})
48
+ query('Contact_Get', options.merge({
49
+ 'email' => email,
50
+ 'optionalParameters' => {'getAllCustomFields' => true},
51
+ :on_timeout => nil,
52
+ :on_error => nil,
53
+ :on_success => Proc.new { |response|
54
+ Contact.new(response.values.first) if response.respond_to?(:values)
55
+ }
56
+ }))
57
+ end
58
+
59
+
60
+ ##
61
+ # Public: Indicates whether or not the contact is active. Active contacts
62
+ # may receive mailings from your campaigns.
63
+ #
64
+ # Returns true if active.
65
+ # Returns false if non-active.
66
+ #
67
+ def active?
68
+ status == 'active'
69
+ end
70
+
71
+ ##
72
+ # Public: Indicates whether or not the contact has a bounced address. This
73
+ # means that mail delivery has failed in a way that Contactology is no
74
+ # longer sending mailings to this contact.
75
+ #
76
+ # Returns true if bounced.
77
+ # Returns false if non-bounced.
78
+ #
79
+ def bounced?
80
+ status == 'bounced'
81
+ end
82
+
83
+ ##
84
+ # Public: Changes the contact's email address to the new address given.
85
+ #
86
+ # Examples
87
+ #
88
+ # contact = Contactology::Contact.find('joe@example.local')
89
+ # # => #<Contactology::Contact:0x000... @email="joe@example.local" ...>
90
+ # contact.change_email('jim@example.local')
91
+ # # => true
92
+ # contact.email
93
+ # # => 'jim@example.local'
94
+ #
95
+ # Returns true when successful.
96
+ # Returns false when unsuccessful or for a network failure.
97
+ #
98
+ def change_email(new_email, options = {})
99
+ self.class.query('Contact_Change_Email', options.merge({
100
+ 'email' => email,
101
+ 'newEmail' => new_email,
102
+ :on_timeout => false,
103
+ :on_error => false,
104
+ :on_success => Proc.new { |response| self.email = new_email; true }
105
+ }))
106
+ end
107
+
108
+ def deleted?
109
+ status == 'deleted'
110
+ end
111
+
112
+ ##
113
+ # Public: Removes the contact from Contactology and from your account.
114
+ #
115
+ # Examples
116
+ #
117
+ # contact = Contactology::Contact.find('joe@example.local')
118
+ # # => #<Contactology::Contact:0x000... @email="joe@example.local" ...>
119
+ # contact.destroy
120
+ # # => true
121
+ #
122
+ # Returns true when successful.
123
+ # Returns false when unsuccessful or for a network failure.
124
+ #
125
+ def destroy(options = {})
126
+ self.class.query('Contact_Delete', options.merge({
127
+ :email => email,
128
+ :on_timeout => false,
129
+ :on_error => false,
130
+ :on_success => Proc.new { |r| self.status = 'deleted'; true }
131
+ }))
132
+ end
133
+
134
+ def lists(options = {})
135
+ self.class.query('Contact_Get_Subscriptions', options.merge({
136
+ 'email' => email,
137
+ :on_timeout => [],
138
+ :on_error => [],
139
+ :on_success => Proc.new { |response|
140
+ response.collect { |listid| ListProxy.new(listid) }
141
+ }
142
+ }))
143
+ end
144
+
145
+ def save(options = {})
146
+ self.class.query('Contact_Add', {
147
+ 'email' => email,
148
+ 'customFields' => custom_fields,
149
+ 'optionalParameters' => {'updateCustomFields' => true},
150
+ :on_timeout => false,
151
+ :on_error => false,
152
+ :on_success => Proc.new { |response| self.status = 'active'; self }
153
+ })
154
+ end
155
+
156
+ def save!(options = {})
157
+ save(options) || raise(InvalidObjectError)
158
+ end
159
+
160
+ ##
161
+ # Public: Suppresses the contact, removing them from receiving campaign
162
+ # mailings.
163
+ #
164
+ # Returns true when successful.
165
+ # Returns false when unsuccessful.
166
+ #
167
+ def suppress(options = {})
168
+ response = self.class.query('Contact_Suppress', options.merge({
169
+ :email => email,
170
+ :on_timeout => false,
171
+ :on_error => false,
172
+ :on_success => Proc.new { |response| response }
173
+ }))
174
+
175
+ if response
176
+ self.status = 'suppressed'
177
+ end
178
+
179
+ response
180
+ end
181
+
182
+ ##
183
+ # Public: Indicates whether or not the contact is suppressed. Suppressed
184
+ # contacts may not receive mailings from your campaigns.
185
+ #
186
+ # Returns true if suppressed.
187
+ # Returns false if non-suppressed.
188
+ #
189
+ def suppressed?
190
+ status == 'suppressed'
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,4 @@
1
+ module Contactology
2
+ Error = Class.new(StandardError)
3
+ InvalidObjectError = Class.new(Error)
4
+ end
@@ -0,0 +1,24 @@
1
+ module Contactology
2
+ class Issue
3
+ attr_reader :type
4
+ attr_reader :text
5
+ attr_reader :message
6
+ attr_reader :context
7
+ attr_reader :col
8
+ attr_reader :deduction
9
+
10
+ def initialize(details)
11
+ details = Hash.new unless details.kind_of?(Hash)
12
+ @type = details['type']
13
+ @text = details['text']
14
+ @message = details['message']
15
+ @context = details['context']
16
+ @col = details['col']
17
+ @deduction = details['deduction']
18
+ end
19
+
20
+ def to_s
21
+ "%s: %s, %d point deduction" % [type, text || message, deduction]
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,18 @@
1
+ require 'contactology/issue'
2
+
3
+ module Contactology
4
+ class Issues < Array
5
+ attr_reader :score
6
+
7
+ def initialize(data = nil)
8
+ data = Hash.new unless data.kind_of?(Hash)
9
+ @score = data['score'] || 0
10
+ (data['issues'] || []).each { |i| self << i }
11
+ end
12
+
13
+
14
+ def <<(o)
15
+ super(Issue.new(o))
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,192 @@
1
+ require 'contactology/stash'
2
+ require 'contactology/api'
3
+
4
+ module Contactology
5
+ ##
6
+ # Represents a subscription List in Contactology. Lists are a convenient way
7
+ # to organize groups of Contacts in order to send large numbers of contacts
8
+ # a Campaign, easily.
9
+ #
10
+ class List < Contactology::Stash
11
+ extend API
12
+
13
+ property :id, :from => :listId
14
+ property :name, :required => true
15
+ property :description
16
+ property :type
17
+ property :opt_in, :from => :optIn
18
+
19
+
20
+ ##
21
+ # Public: Returns a collection of all active lists on your account.
22
+ #
23
+ # Returns a collection of Contactology::List instances.
24
+ #
25
+ def self.all(options = {})
26
+ query('List_Get_Active_Lists', options.merge({
27
+ :on_timeout => [],
28
+ :on_error => [],
29
+ :on_success => Proc.new { |response| response.values.collect { |list| List.new(list) }}
30
+ }))
31
+ end
32
+
33
+ ##
34
+ # Public: Creates a new, public list on Contactology. The new list's :name
35
+ # is the only required attribute.
36
+ #
37
+ # Returns a Contactology::List instance when successful.
38
+ # Returns false when unsuccessful or a network failure occurs.
39
+ #
40
+ def self.create(attributes, options = {})
41
+ raise ArgumentError, 'Expected an :name attribute' unless attributes.has_key?(:name)
42
+ new(attributes).save(options)
43
+ end
44
+
45
+ ##
46
+ # Public: Loads a Contactology list by ID.
47
+ #
48
+ # Returns a Contactology::List instance on success.
49
+ # Returns nil for an unknown ID or network error.
50
+ #
51
+ def self.find(id, options = {})
52
+ query('List_Get_Info', options.merge({
53
+ 'listId' => id,
54
+ :on_timeout => nil,
55
+ :on_error => nil,
56
+ :on_success => Proc.new { |response| new(response) if response.kind_of?(Hash) }
57
+ }))
58
+ end
59
+
60
+
61
+ ##
62
+ # Public: Removes a list from Contactology.
63
+ #
64
+ # Returns true when successful.
65
+ # Returns false when unsuccessful.
66
+ #
67
+ def destroy(options = {})
68
+ self.class.query('List_Delete', options.merge({
69
+ 'listId' => id,
70
+ :on_timeout => false,
71
+ :on_error => false,
72
+ :on_success => Proc.new { |response| response }
73
+ }))
74
+ end
75
+
76
+ ##
77
+ # Public: Imports contacts into a list using a prescribed contact
78
+ # collection format.
79
+ #
80
+ # Examples
81
+ #
82
+ # list = Contactology::List.create :name => 'import-test'
83
+ # # => #<Contactology::List:0x000... @name="import-test" ...>
84
+ # list.import([{'email' => 'import@example.local', 'first_name' => 'Imp', 'last_name' => 'Orted'}, {...}])
85
+ # # => true
86
+ #
87
+ # Returns true if all contacts imported successfully.
88
+ # Returns false if any contact import failed or a network error occurred.
89
+ #
90
+ def import(contacts, options = {})
91
+ self.class.query('List_Import_Contacts', options.merge({
92
+ 'listId' => id,
93
+ 'source' => options[:source] || 'Manual Entry',
94
+ 'contacts' => contacts,
95
+ 'optionalParameters' => {
96
+ 'activateDeleted' => true,
97
+ 'updateCustomFields' => true
98
+ },
99
+ :on_timeout => false,
100
+ :on_error => false,
101
+ :on_success => Proc.new { |response|
102
+ response.kind_of?(Hash) && response['success'] == contacts.size
103
+ }
104
+ }))
105
+ end
106
+
107
+ def internal?
108
+ type == 'internal'
109
+ end
110
+
111
+ def opt_in?
112
+ opt_in
113
+ end
114
+
115
+ def public?
116
+ type == 'public'
117
+ end
118
+
119
+ def private?
120
+ type == 'private'
121
+ end
122
+
123
+ def save(options = {})
124
+ self.class.query('List_Add_Public', options.merge({
125
+ 'listId' => id,
126
+ 'name' => name,
127
+ 'description' => description,
128
+ 'optionalParameters' => {
129
+ 'optIn' => opt_in
130
+ },
131
+ :on_timeout => false,
132
+ :on_error => false,
133
+ :on_success => Proc.new { |response|
134
+ data = self.class.find(response)
135
+ self.id = data.id
136
+ self.description = data.description
137
+ self.name = data.name
138
+ self.type = data.type
139
+ self.opt_in = data.opt_in
140
+ self
141
+ }
142
+ }))
143
+ end
144
+
145
+ def save!(options = {})
146
+ save(options) || raise(InvalidObjectError)
147
+ end
148
+
149
+ ##
150
+ # Public: Adds an email address to the list.
151
+ #
152
+ # Examples
153
+ #
154
+ # list = Contactology::List.find 1
155
+ # # => #<Contactology::List:0x000... @id="1" ...>
156
+ # list.subscribe 'new@example.local'
157
+ # # => true
158
+ #
159
+ # Returns true when the address is successfully added.
160
+ # Returns false when the subscription fails or a network error occurs.
161
+ #
162
+ def subscribe(email, options = {})
163
+ self.class.query('List_Subscribe', options.merge({
164
+ 'listId' => id,
165
+ 'email' => email.respond_to?(:email) ? email.email : email,
166
+ :on_timeout => false,
167
+ :on_error => false,
168
+ :on_success => Proc.new { |response| response }
169
+ }))
170
+ end
171
+
172
+ def test?
173
+ type == 'test'
174
+ end
175
+
176
+ ##
177
+ # Public: Unsubscribes an email address from the Contactology::List.
178
+ #
179
+ # Returns true when the address is removed.
180
+ # Returns false when the removal fails or a network error occurs.
181
+ #
182
+ def unsubscribe(email, options = {})
183
+ self.class.query('List_Unsubscribe', options.merge({
184
+ 'listId' => id,
185
+ 'email' => email,
186
+ :on_timeout => false,
187
+ :on_error => false,
188
+ :on_success => Proc.new { |response| response }
189
+ }))
190
+ end
191
+ end
192
+ end