contactology 0.0.1

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