monkeywrench 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/README.mdown +78 -0
  2. data/Rakefile +22 -0
  3. data/lib/monkey_wrench.rb +1 -0
  4. data/lib/monkeywrench.rb +10 -0
  5. data/lib/monkeywrench/array.rb +9 -0
  6. data/lib/monkeywrench/base.rb +97 -0
  7. data/lib/monkeywrench/campaign.rb +0 -0
  8. data/lib/monkeywrench/campaign_aim.rb +0 -0
  9. data/lib/monkeywrench/campaign_stats.rb +0 -0
  10. data/lib/monkeywrench/config.rb +35 -0
  11. data/lib/monkeywrench/error.rb +104 -0
  12. data/lib/monkeywrench/hash.rb +69 -0
  13. data/lib/monkeywrench/helper.rb +0 -0
  14. data/lib/monkeywrench/list.rb +353 -0
  15. data/lib/monkeywrench/member.rb +33 -0
  16. data/lib/monkeywrench/security.rb +0 -0
  17. data/test/fixtures/api_fail.json +1 -0
  18. data/test/fixtures/campaigns_success.json +24 -0
  19. data/test/fixtures/listBatchSubscribe1000_success.json +1 -0
  20. data/test/fixtures/listBatchSubscribe10_success.json +1 -0
  21. data/test/fixtures/listBatchSubscribe4_success.json +1 -0
  22. data/test/fixtures/listBatchSubscribe5_success.json +1 -0
  23. data/test/fixtures/listBatchSubscribe_success.json +1 -0
  24. data/test/fixtures/listBatchSubscribe_with_error_success.json +1 -0
  25. data/test/fixtures/listBatchUnsubscribe_success.json +1 -0
  26. data/test/fixtures/listMemberInfo_fail.json +1 -0
  27. data/test/fixtures/listMemberInfo_success.json +1 -0
  28. data/test/fixtures/listMembers_none_success.json +1 -0
  29. data/test/fixtures/listMembers_success.json +8 -0
  30. data/test/fixtures/listSubscribe_success.json +1 -0
  31. data/test/fixtures/listUnsubscribe_success.json +0 -0
  32. data/test/fixtures/listUpdateMember_success.json +1 -0
  33. data/test/fixtures/listsEmpty_success.json +2 -0
  34. data/test/fixtures/lists_success.json +14 -0
  35. data/test/monkey_wrench/base_test.rb +55 -0
  36. data/test/monkey_wrench/base_test.rbc +1758 -0
  37. data/test/monkey_wrench/campaign_aim_test.rb +0 -0
  38. data/test/monkey_wrench/campaign_aim_test.rbc +40 -0
  39. data/test/monkey_wrench/campaign_stats_test.rb +0 -0
  40. data/test/monkey_wrench/campaign_stats_test.rbc +40 -0
  41. data/test/monkey_wrench/campaign_test.rb +0 -0
  42. data/test/monkey_wrench/campaign_test.rbc +40 -0
  43. data/test/monkey_wrench/hash_test.rb +57 -0
  44. data/test/monkey_wrench/hash_test.rbc +1636 -0
  45. data/test/monkey_wrench/helper_test.rb +0 -0
  46. data/test/monkey_wrench/helper_test.rbc +40 -0
  47. data/test/monkey_wrench/list_test.rb +405 -0
  48. data/test/monkey_wrench/list_test.rbc +10798 -0
  49. data/test/monkey_wrench/security_test.rb +0 -0
  50. data/test/monkey_wrench/security_test.rbc +40 -0
  51. data/test/test_helper.rb +66 -0
  52. data/test/test_helper.rbc +2045 -0
  53. metadata +169 -0
@@ -0,0 +1,78 @@
1
+ # MonkeyWrench
2
+ MonkeyWrench is a rubyesque API for interfacing with [Mailchimp](http://www.mailchimp.com) (or show me some love by registering [via my affiliate link](http://eepurl.com/Ge71)). It makes managing a mailing list, adding/removing subscribers, and setting up autoresponders so easy that even a monkey could do it.
3
+
4
+ The API provided by MonkeyWrench takes an idiomatic ruby approach where possible, so supported parameters and options may differ slightly to what is documented in the official Mailchimp API documentation. Where possible, refer to the MonkeyWrench docs.
5
+
6
+ ## Getting Started
7
+
8
+ ### Installation
9
+
10
+ The easiest way to get started is to install the gem:
11
+
12
+ gem install monkeywrench
13
+
14
+ ### Usage
15
+
16
+ To get started, you need to first connect to the appropriate datacenter with your API key:
17
+
18
+ MonkeyWrench::Config.new(:datacenter => "us1",
19
+ :apikey => "your-api-key-goes-here")
20
+
21
+ From there you've got a rich API for managing Lists and Members. To subscribe a new user to a list simply do the following:
22
+
23
+ list = MonkeyWrench::List.find_by_name("My Example List")
24
+ list.subscribe("foo@bar.com")
25
+
26
+ ## Further Reading
27
+
28
+ For more information, [check the documentation](http://rdoc.info/projects/rubypond/monkeywrench)
29
+
30
+ ## Compatibility
31
+
32
+ Tested on the following versions:
33
+
34
+ * 1.8.7
35
+ * 1.9.2
36
+ * Ruby Enterprise Edition 1.8.7
37
+ * Rubinius 1.1.1
38
+ * JRuby 1.5.5
39
+
40
+ It may work in 1.8.6 too, but a test dependency (WebMock) won't work so tests aren't currently automated against that version.
41
+
42
+ ## Status
43
+
44
+ The library is currently under development, but in production use for many clients. It's still currently using the 1.2 version of the Mailchimp API (Mailchimp haven't made any deprecation announcements yet, and they still support 1.1 so it shouldn't be an issue any time soon). I'm working towards a switch to 1.3.
45
+
46
+ ## Contributing
47
+
48
+ Patches and pull requests are gladly accepted. Please make sure that you include associated tests and documentation and that your commit messages are brief but descriptive.
49
+
50
+ ## Credits & Contributions
51
+
52
+ * [David Heath](https://davidheath.org/)
53
+ * [Maxime Guilbot](https://github.com/maxime)
54
+ * [Keith Marcum](https://github.com/kamarcum)
55
+
56
+ ## License
57
+
58
+ MonkeyWrench is released under the MIT license.
59
+
60
+ Copyright (c) 2010 Glenn Gillen
61
+
62
+ Permission is hereby granted, free of charge, to any person obtaining a copy
63
+ of this software and associated documentation files (the "Software"), to deal
64
+ in the Software without restriction, including without limitation the rights
65
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
66
+ copies of the Software, and to permit persons to whom the Software is
67
+ furnished to do so, subject to the following conditions:
68
+
69
+ The above copyright notice and this permission notice shall be included in
70
+ all copies or substantial portions of the Software.
71
+
72
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
73
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
74
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
75
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
76
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
77
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
78
+ THE SOFTWARE.
@@ -0,0 +1,22 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/gempackagetask'
4
+ require 'rake/rdoctask'
5
+
6
+ spec_data = File.open('monkeywrench.gemspec').read
7
+ spec = nil
8
+ Thread.new do
9
+ spec = eval("#{spec_data}")
10
+ end.join
11
+
12
+ Rake::GemPackageTask.new(spec) do |pkg|
13
+ pkg.need_zip = false
14
+ pkg.need_tar = false
15
+ end
16
+
17
+ require 'rake/testtask'
18
+ Rake::TestTask.new(:test) do |test|
19
+ test.libs << 'lib' << 'test'
20
+ test.pattern = 'test/**/*_test.rb'
21
+ test.verbose = false
22
+ end
@@ -0,0 +1 @@
1
+ require "#{File.dirname(__FILE__)}/monkeywrench"
@@ -0,0 +1,10 @@
1
+ require "httparty"
2
+ base_dir = File.join(File.dirname(__FILE__), "monkeywrench")
3
+ ["base", "config", "hash", "list", "error", "member"].each do |lib|
4
+ require File.join(base_dir, lib)
5
+ end
6
+
7
+ begin
8
+ OpenStruct.class_eval { undef :id, :type }
9
+ rescue NameError
10
+ end
@@ -0,0 +1,9 @@
1
+ require 'cgi'
2
+ module MonkeyWrench
3
+ module Array
4
+ end
5
+ end
6
+
7
+ class Array
8
+ include MonkeyWrench::Array
9
+ end
@@ -0,0 +1,97 @@
1
+ require 'ostruct'
2
+ module MonkeyWrench
3
+ class Base < OpenStruct
4
+ include HTTParty
5
+
6
+ @@apikey = nil
7
+ @@datacenter = nil
8
+ @@dryrun = false
9
+
10
+ class << self
11
+ def default_query_params
12
+ { :output => "json", :apikey=> @@apikey}
13
+ end
14
+
15
+ def base_uri
16
+ "http://#{datacenter}.api.mailchimp.com/1.2/"
17
+ end
18
+
19
+ def default_retry_limit
20
+ 3
21
+ end
22
+
23
+ def get(params, http_options = {})
24
+ if @@dryrun
25
+ puts "GET #{base_uri} #{params.merge(default_query_params).inspect}"
26
+ return {}
27
+ else
28
+ robustly(http_options) do
29
+ response = super(base_uri, http_options.merge(:query => params.merge(default_query_params)))
30
+ handle_errors(response.parsed_response)
31
+ end
32
+ end
33
+ end
34
+
35
+ def post(params, http_options = {})
36
+ if @@dryrun
37
+ puts "POST #{base_uri} #{params.merge(default_query_params).inspect}"
38
+ return {}
39
+ else
40
+ robustly(http_options) do
41
+ post_params = params.dup
42
+ get_params = default_query_params.merge(:method => post_params.delete(:method))
43
+ response = super(base_uri, http_options.merge(:body => post_params, :query => get_params))
44
+ handle_errors(response.parsed_response)
45
+ end
46
+ end
47
+ end
48
+
49
+ def handle_errors(objects)
50
+ return objects unless objects.respond_to?(:has_key?)
51
+
52
+ if objects.has_key?("error")
53
+ objects.replace({ "error" => MonkeyWrench::Error.new(objects['error'], objects['code']) })
54
+ elsif objects.has_key?("errors")
55
+ objects["errors"] = objects["errors"].map do |err|
56
+ message = err.delete('message')
57
+ code = err.delete('code')
58
+ MonkeyWrench::Error.new(message, code, err)
59
+ end
60
+ end
61
+ objects
62
+ end
63
+
64
+ def apikey
65
+ @@apikey
66
+ end
67
+
68
+ def datacenter
69
+ @@datacenter
70
+ end
71
+ end
72
+
73
+ private
74
+ def self.robustly(http_options, &block)
75
+ retry_limit = http_options[:retry_limit] || default_retry_limit
76
+ attempts = 0
77
+ while attempts < retry_limit
78
+ begin
79
+ attempts += 1
80
+ return yield
81
+ rescue Timeout::Error => e
82
+ if attempts == retry_limit
83
+ raise e
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ def get(*args)
90
+ self.class.get(*args)
91
+ end
92
+
93
+ def post(*args)
94
+ self.class.post(*args)
95
+ end
96
+ end
97
+ end
File without changes
File without changes
File without changes
@@ -0,0 +1,35 @@
1
+ require 'yaml'
2
+ module MonkeyWrench
3
+ class Config < MonkeyWrench::Base
4
+ # Establishes a connection to the Mailchimp API.
5
+ #
6
+ # Can be called with either a path to a file containing credentials, or
7
+ # a hash of credentials. The formats of each are:
8
+ #
9
+ # File:
10
+ # mailchimp:
11
+ # datacenter: us1 # Or whatever DC you use
12
+ # apikey: your-api-key-goes-here
13
+ # Hash:
14
+ # { :datacenter => "us1", :apikey => "your-api-key-goes-here"}
15
+ #
16
+ # @example
17
+ # MonkeyWrench::Config.new(:datacenter => "us1",
18
+ # :apikey => "your-api-key-goes-here")
19
+ #
20
+ # @param [String, Hash] credentials accepts either a String pointing to a file
21
+ # containing the credentials, or a hash of credentials.
22
+ def initialize(credentials)
23
+ if credentials.is_a?(String)
24
+ config = YAML.load_file(credentials)["mailchimp"]
25
+ config.collect_kv!{|k,v| [k.to_sym, v]}
26
+ else
27
+ config = credentials
28
+ end
29
+ @@apikey = config[:apikey]
30
+ @@datacenter = config[:datacenter]
31
+ @@dryrun = config[:dryrun] || false
32
+ super({})
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,104 @@
1
+ module MonkeyWrench
2
+ class Error < StandardError
3
+ attr_reader :code
4
+
5
+ def initialize(message, code, extra_fields = {})
6
+ super(message)
7
+ @code = code
8
+ @extra_fields = extra_fields
9
+ end
10
+
11
+ def type
12
+ types[@code]
13
+ end
14
+
15
+ def method_missing(sym, *args, &block)
16
+ return @extra_fields[sym.to_s] if @extra_fields.has_key?(sym.to_s)
17
+ end
18
+
19
+ def types
20
+ {
21
+ -32601 => 'ServerError_MethodUnknown',
22
+ -32602 => 'ServerError_InvalidParameters',
23
+ -99 => 'Unknown_Exception',
24
+ -98 => 'Request_TimedOut',
25
+ -92 => 'Zend_Uri_Exception',
26
+ -91 => 'PDOException',
27
+ -91 => 'Avesta_Db_Exception',
28
+ -90 => 'XML_RPC2_Exception',
29
+ -90 => 'XML_RPC2_FaultException',
30
+ -50 => 'Too_Many_Connections',
31
+ 0 => 'Parse_Exception',
32
+ 100 => 'User_Unknown',
33
+ 101 => 'User_Disabled',
34
+ 102 => 'User_DoesNotExist',
35
+ 103 => 'User_NotApproved',
36
+ 104 => 'Invalid_ApiKey',
37
+ 105 => 'User_UnderMaintenance',
38
+ 120 => 'User_InvalidAction',
39
+ 121 => 'User_MissingEmail',
40
+ 122 => 'User_CannotSendCampaign',
41
+ 123 => 'User_MissingModuleOutbox',
42
+ 124 => 'User_ModuleAlreadyPurchased',
43
+ 125 => 'User_ModuleNotPurchased',
44
+ 126 => 'User_NotEnoughCredit',
45
+ 127 => 'MC_InvalidPayment',
46
+ 200 => 'List_DoesNotExist',
47
+ 210 => 'List_InvalidInterestFieldType',
48
+ 211 => 'List_InvalidOption',
49
+ 212 => 'List_InvalidUnsubMember',
50
+ 213 => 'List_InvalidBounceMember',
51
+ 214 => 'List_AlreadySubscribed',
52
+ 215 => 'List_NotSubscribed',
53
+ 220 => 'List_InvalidImport',
54
+ 221 => 'MC_PastedList_Duplicate',
55
+ 222 => 'MC_PastedList_InvalidImport',
56
+ 230 => 'Email_AlreadySubscribed',
57
+ 231 => 'Email_AlreadyUnsubscribed',
58
+ 232 => 'Email_NotExists',
59
+ 233 => 'Email_NotSubscribed',
60
+ 250 => 'List_MergeFieldRequired',
61
+ 251 => 'List_CannotRemoveEmailMerge',
62
+ 252 => 'List_Merge_InvalidMergeID',
63
+ 253 => 'List_TooManyMergeFields',
64
+ 254 => 'List_InvalidMergeField',
65
+ 270 => 'List_InvalidInterestGroup',
66
+ 271 => 'List_TooManyInterestGroups',
67
+ 300 => 'Campaign_DoesNotExist',
68
+ 301 => 'Campaign_StatsNotAvailable',
69
+ 310 => 'Campaign_InvalidAbsplit',
70
+ 311 => 'Campaign_InvalidContent',
71
+ 312 => 'Campaign_InvalidOption',
72
+ 313 => 'Campaign_InvalidStatus',
73
+ 314 => 'Campaign_NotSaved',
74
+ 315 => 'Campaign_InvalidSegment',
75
+ 316 => 'Campaign_InvalidRss',
76
+ 317 => 'Campaign_InvalidAuto',
77
+ 318 => 'MC_ContentImport_InvalidArchive',
78
+ 330 => 'Invalid_EcommOrder',
79
+ 350 => 'Absplit_UnknownError',
80
+ 351 => 'Absplit_UnknownSplitTest',
81
+ 352 => 'Absplit_UnknownTestType',
82
+ 353 => 'Absplit_UnknownWaitUnit',
83
+ 354 => 'Absplit_UnknownWinnerType',
84
+ 355 => 'Absplit_WinnerNotSelected',
85
+ 500 => 'Invalid_Analytics',
86
+ 501 => 'Invalid_DateTime',
87
+ 502 => 'Invalid_Email',
88
+ 503 => 'Invalid_SendType',
89
+ 504 => 'Invalid_Template',
90
+ 505 => 'Invalid_TrackingOptions',
91
+ 506 => 'Invalid_Options',
92
+ 507 => 'Invalid_Folder',
93
+ 508 => 'Invalid_URL',
94
+ 550 => 'Module_Unknown',
95
+ 551 => 'MonthlyPlan_Unknown',
96
+ 552 => 'Order_TypeUnknown',
97
+ 553 => 'Invalid_PagingLimit',
98
+ 554 => 'Invalid_PagingStart'
99
+ }
100
+ end
101
+ end
102
+ end
103
+
104
+
@@ -0,0 +1,69 @@
1
+ require 'cgi'
2
+ module MonkeyWrench
3
+ module Hash
4
+
5
+ # Takes a block that returns a [key, value] pair
6
+ # and builds a new hash based on those pairs
7
+ # Courtesy of http://snuxoll.com/post/2009/02/13/ruby-better-hashcollect
8
+ def collect_kv
9
+ result = {}
10
+ each do |k,v|
11
+ new_k, new_v = yield k, v
12
+ result[new_k] = new_v
13
+ end
14
+ result
15
+ end
16
+
17
+ def collect_kv!(&blk)
18
+ replace(self.collect_kv(&blk))
19
+ end
20
+
21
+ def escape_keys!
22
+ collect_kv!{|k,v| [CGI.escape(k.to_s), v]}
23
+ end
24
+
25
+ def to_mailchimp(index = nil, parent_name = nil)
26
+ result = self.collect_kv do |k,v|
27
+ if v.is_a?(Array) && v.first.is_a?(Hash)
28
+ i = 0
29
+ v = v.inject({}) do |acc,hash|
30
+ acc.merge!(hash.to_mailchimp(i, k))
31
+ i += 1
32
+ acc
33
+ end
34
+ elsif v.is_a?(Hash)
35
+ if parent_name
36
+ v = v.collect_kv do |key,val|
37
+ keyname = CGI.escape("#{parent_name.to_s}[#{key.to_s.upcase}]")
38
+ [keyname, val.to_s]
39
+ end
40
+ else
41
+ v = { k => v }.to_mailchimp(nil, k)
42
+ end
43
+ elsif v.is_a?(Array)
44
+ results = {}
45
+ i = 0
46
+ v.each do |val|
47
+ keyname = CGI.escape("#{k}[#{i}]")
48
+ results[keyname] = val.to_s
49
+ i += 1
50
+ end
51
+ v = results
52
+ end
53
+ k = k.to_s
54
+ k = "[#{index}][#{k.upcase}]" if index
55
+ k = [parent_name, k].join if k != parent_name.to_s
56
+ [CGI.escape(k), v]
57
+ end
58
+ if result.detect{|k,v| v.is_a?(Hash)}
59
+ result.values.first
60
+ else
61
+ result
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ class Hash
68
+ include MonkeyWrench::Hash
69
+ end
File without changes
@@ -0,0 +1,353 @@
1
+ require 'cgi'
2
+
3
+ module MonkeyWrench
4
+ class List < MonkeyWrench::Base
5
+ # Finds a given list by name
6
+ #
7
+ # @example
8
+ # MonkeyWrench::List.find_by_name("My Example List")
9
+ #
10
+ # @param [String] list_name the list name
11
+ # @return [MonkeyWrench::List] the first list found with a matching name
12
+ def self.find_by_name(list_name)
13
+ lists = find_all.detect{|list| list.name == list_name}
14
+ end
15
+
16
+ # Will compare another list against the current one and return true if
17
+ # they are the same (based on list ID)
18
+ #
19
+ # @example
20
+ # list1 = MonkeyWrench::List.find("0a649eafc3")
21
+ # list2 = MonkeyWrench::List.find("9f9d54a0c4")
22
+ # list3 = MonkeyWrench::List.find("0a649eafc3") # Same as list1!!
23
+ # list1 == list2 # false
24
+ # list1 == list3 # true
25
+ # @param [MonkeyWrench::List] other_list Other list to compare against
26
+ # @return [Boolean]
27
+ def ==(other_list)
28
+ other_list.is_a?(self.class) && self.id == other_list.id
29
+ end
30
+
31
+ # Finds a given list by ID
32
+ #
33
+ # @example
34
+ # MonkeyWrench::List.find("0a649eafc3")
35
+ #
36
+ # @param [String] id the unique Mailchimp list ID
37
+ # @return [MonkeyWrench::List] the list
38
+ def self.find(id)
39
+ find_all.find{|e| e.id == id}
40
+ end
41
+
42
+ # Finds all lists
43
+ #
44
+ # @example
45
+ # MonkeyWrench::List.find_all
46
+ #
47
+ # @return [Array<MonkeyWrench::List>]
48
+ def self.find_all
49
+ @@lists ||= post({ :method => "lists" }).map do |list|
50
+ List.new(list)
51
+ end
52
+ end
53
+
54
+ class << self
55
+ alias :all :find_all
56
+ end
57
+
58
+ # Clears the List cache (results from List.find_all are cached for
59
+ # performance reasons)
60
+ #
61
+ # @example
62
+ # MonkeyWrench::List.clear!
63
+ #
64
+ # @return nil
65
+ def self.clear!
66
+ @@lists = nil
67
+ end
68
+
69
+ # Returns all members for this list
70
+ #
71
+ # @example Find all members that have unsubscribed in the last 24 hours:
72
+ # MonkeyWrench.members(:status => "unsubscribed",
73
+ # :since => Time.now - 86400)
74
+ #
75
+ # @param [Hash] options additional option to include when searching.
76
+ # @option options [String] :status ('subscribed') Filter the list members by status. Can be one of the following: "subscribed", "unsubscribed", "cleaned", "updated".
77
+ # @option options [DateTime] :since Return all members whose status has changed or whose profile has changed since this date/time (in GMT).
78
+ # @option options [Integer] :start (0) For large datasets, the page number to start at.
79
+ # @option options [Integer] :limit (100) For large datasets, the number of results to return. Upper limit is set at 15000.
80
+ # @option options [Boolean] :full_details (true) Return full member details and not just email address and timestamp.
81
+ # @return [Array<MonkeyWrench::Member>]
82
+ def members(options = {})
83
+ if options[:since]
84
+ options[:since] = options[:since].strftime("%Y-%m-%d %H:%M:%S")
85
+ end
86
+ options.merge!(:id => self.id, :method => "listMembers")
87
+ response = post(options)
88
+ if options[:full_details]
89
+ response.map do |response_user|
90
+ member(response_user["email"])
91
+ end
92
+ else
93
+ response.map do |response_user|
94
+ MonkeyWrench::Member.new(response_user)
95
+ end
96
+ end
97
+ end
98
+
99
+ # Enumerates over each member and executes the provided block. Will
100
+ # automatically page and batch requests for members.
101
+ #
102
+ # @example
103
+ # list = MonkeyWrench::List.find("0a649eafc3")
104
+ # emails = []
105
+ # list.each_member do |member|
106
+ # emails << member.email
107
+ # end
108
+ #
109
+ # @param [Proc] &block code to execute for each member
110
+ def each_member(&block)
111
+ page = 0
112
+ loop do
113
+ batch = members(:start => page, :limit => 15000)
114
+ break if batch.empty?
115
+ batch.each do |member|
116
+ yield member
117
+ end
118
+ page += 1
119
+ end
120
+ end
121
+
122
+ # Updates details of list members
123
+ #
124
+ # @example Update a single member's email address
125
+ # list = MonkeyWrench::List.find("0a649eafc3")
126
+ # member = {:email => "foo@bar.com", :new_email => "bar@foo.com"}
127
+ # list.update_members(member)
128
+ #
129
+ # @example Update multiple members' email addresses
130
+ # list = MonkeyWrench::List.find("0a649eafc3")
131
+ # members = [{:email => "foo@bar.com", :new_email => "bar@foo.com"},
132
+ # {:email => "bob@bar.com", :new_email => "bob@foo.com"}]
133
+ # list.update_members(members)
134
+ #
135
+ # @param [Hash, Array<Hash>] members details of member(s) to update details
136
+ # of. Members are matched based on the value of :email, to update the
137
+ # email address assign the new address to :new_email. All other field
138
+ # names are lowercase symbols representing the MERGEVAR name in
139
+ # Mailchimp (e.g., FNAME is :fname)
140
+ # @param [Hash] options additional options when updating members.
141
+ # @option options [String] :email_type Change the email type preference for the member ('html', 'text', or 'mobile').
142
+ # @option options [Boolean] :replace_interests (true) replace the interest groups provided (will append interest groups to existing values when false).
143
+ def update_members(members, options = {})
144
+ members = members.is_a?(Array) ? members : [members]
145
+ options.merge!(:id => self.id, :method => "listUpdateMember")
146
+ members.each do |member|
147
+ mailchimp_args = {:email_address => member[:email]}
148
+ member[:email] = member[:new_email]
149
+ member.delete(:new_email)
150
+ mailchimp_args.merge!({ :merge_vars => member }.to_mailchimp)
151
+ post(options.merge(mailchimp_args))
152
+ end
153
+ end
154
+
155
+ # Find a member in this list with the given email address
156
+ #
157
+ # @example
158
+ # list = MonkeyWrench::List.find("0a649eafc3")
159
+ # list.member("glenn@rubypond.com")
160
+ #
161
+ # @param [String] email members email address
162
+ # @return [MonkeyWrench::Member]
163
+ def member(email)
164
+ response = post(:id => self.id, :method => "listMemberInfo", :email_address => email)
165
+ if response['error']
166
+ raise response['error']
167
+ else
168
+ MonkeyWrench::Member.new(response)
169
+ end
170
+ end
171
+
172
+ # Check if an email has subscribed to the list
173
+ #
174
+ # @example
175
+ # list = MonkeyWrench::List.find("0a649eafc3")
176
+ # list.member?("glenn@rubypond.com")
177
+ #
178
+ # @param [String] email members email address
179
+ # @return [Boolean]
180
+ def member?(email)
181
+ response = post(:id => self.id, :method => "listMemberInfo", :email_address => email)
182
+ if response['error']
183
+ false
184
+ else
185
+ true
186
+ end
187
+ end
188
+
189
+ # Subscribes a new member to the list
190
+ #
191
+ # @example Subscribe a new email address
192
+ # list.subscribe("foo@bar.com")
193
+ #
194
+ # @example Subscribe a new member with extended details
195
+ # list.subscribe({:email => "foo@bar.com", :type => :html})
196
+ #
197
+ # @example Subscribe multiple new members
198
+ # subscribers = [{:email => "foo@bar.com", :type => :html},
199
+ # {:email => "bar@foo.com", :type => :html}]
200
+ # list.subscribe(subscribe, :send_welcome => true, :double_optin => false)
201
+ #
202
+ # @param [String, Hash, Array<Hash>] contact_details the email address or hash of values for the new member
203
+ # @param [Hash] opts options when adding new member
204
+ # @option opts [Boolean] :send_welcome (false) if :double_optin if false and this is
205
+ # true, send the lists 'Welcome Email' to the member(s). Will not send email if
206
+ # updating an existing member.
207
+ # @option opts [Boolean] :double_optin (true) send an opt-in confirmation email
208
+ # @option opts [Boolean] :update_existing (false) update members that are already subscribed to the list or to return an error (false returns error)
209
+ # @option opts [Boolean] :replace_interests (true) replace interest groups or append to existing interest groups (false appends to groups)
210
+ def subscribe(contact_details, opts = {})
211
+ if contact_details.is_a?(Array)
212
+ return subscribe_many(contact_details, opts)
213
+ else
214
+ if contact_details.is_a?(Hash)
215
+ email_address = contact_details.delete(:email)
216
+ opts = opts.merge(contact_details)
217
+ else
218
+ email_address = contact_details
219
+ end
220
+ subscribe_one(email_address, opts)
221
+ return { :success => 1, :errors => []}
222
+ end
223
+ end
224
+
225
+ # Unsubscribes a person (or list of people) from the list
226
+ #
227
+ # @example Unsubscribe a single user
228
+ # list = MonkeyWrench::List.find("0a649eafc3")
229
+ # list.unsubscribe("glenn@rubypond.com", :send_goodbye => true) # Unsubscribe a single person
230
+ #
231
+ # @example Unsubscribe a list of users
232
+ # emails = ["glenn@rubypond.com", "me@glenngillen.com"]
233
+ # list.unsubscribe(emails, :send_goodbye => true) # Unsubscribe multiple people at once
234
+ #
235
+ # @param [String, Array<String>] email address(es) of people to unsubscribe.
236
+ # @param [Hash] opts additional option to include when unsubscribing.
237
+ # @option opts [Boolean] :delete_member (false) completely delete the member from your list instead of just unsubscribing.
238
+ # @option opts [Boolean] :send_goodbye (true) send the goodbye email to the email addresses.
239
+ # @option opts [Boolean] :send_notify (false) send the unsubscribe notification email to the address defined in the list email notification settings.
240
+ # @return [Hash] contains 2 keys. :success contains the number of successful actions, :error a list of all errors.
241
+ def unsubscribe(emails, opts = {})
242
+ emails = [*emails]
243
+ params = { :method => "listBatchUnsubscribe",
244
+ :id => self.id }
245
+ params[:delete_member] = opts[:delete_member] if opts.has_key?(:delete_member)
246
+ params[:send_goodbye] = opts[:send_goodbye] if opts.has_key?(:send_goodbye)
247
+ params[:send_notify] = opts[:send_notify] if opts.has_key?(:send_notify)
248
+ params.merge!({ :emails => emails }.to_mailchimp)
249
+ response = post(params)
250
+ return { :success => response["success_count"],
251
+ :errors => response["errors"] }
252
+ end
253
+
254
+ # Will flag the email(s) as opted-out for all future mailing for this list
255
+ #
256
+ # @example Opt-out a single user
257
+ # list = MonkeyWrench::List.find("0a649eafc3")
258
+ # list.opt_out("glenn@rubypond.com") # Opt-out a single person
259
+ #
260
+ # @example Opt-out a list of users
261
+ # emails = ["glenn@rubypond.com", "me@glenngillen.com"]
262
+ # list.opt_out(emails) # Opt-out multiple people at once
263
+ #
264
+ # @param [String, Array<String>] email address(es) of people to opt-out.
265
+ # @return [Hash] contains 2 keys. :success contains the number of successful actions, :error a list of all errors.
266
+ def opt_out(emails)
267
+ emails = [*emails]
268
+ subscribe(emails.map{|email| { :email => email }})
269
+ unsubscribe(emails, :send_goodbye => false, :send_notify => false)
270
+ end
271
+
272
+ private
273
+ def self.reserved_keys
274
+ [:email, :type, :double_optin, :update_existing, :replace_interests,
275
+ :send_welcome, :emails, :send_notify, :send_goodbye, :delete_member]
276
+ end
277
+
278
+ def subscribe_many(subscribers, opts = {})
279
+ if opts[:send_welcome]
280
+ subscribe_one_at_a_time(subscribers, opts)
281
+ else
282
+ subscribe_in_batches(subscribers, opts)
283
+ end
284
+ end
285
+
286
+ def batch_size
287
+ 1000
288
+ end
289
+
290
+ def each_batch(subscribers, batch_size, &block)
291
+ i = 0
292
+ while i < subscribers.size
293
+ start = Time.now
294
+ yield subscribers[i..(i+batch_size-1)], i
295
+ i += batch_size
296
+ end
297
+ end
298
+
299
+ def subscribe_in_batches(subscribers, opts)
300
+ cumulative_response = { :success => 0, :errors => [] }
301
+ each_batch(subscribers, batch_size) do |batch, i|
302
+ response = subscribe_one_batch(batch, opts)
303
+ cumulative_response[:success] += (response["success_count"] || 0)
304
+ cumulative_response[:errors] += (response["errors"] || [])
305
+ end
306
+ cumulative_response
307
+ end
308
+
309
+ def subscribe_one_batch(subscribers, opts)
310
+ params = { :id => self.id, :method => "listBatchSubscribe" }
311
+ params[:double_optin] = opts[:double_optin] if opts.has_key?(:double_optin)
312
+ params[:update_existing] = opts[:update_existing] if opts.has_key?(:update_existing)
313
+ params[:replace_interests] = opts[:replace_interests] if opts.has_key?(:replace_interests)
314
+ params.merge!({ :batch => subscribers }.to_mailchimp)
315
+ post(params, :timeout => timeout_for_batch(subscribers))
316
+ end
317
+
318
+ def timeout_for_batch(batch)
319
+ # 5 mins for a batch of 1000
320
+ ((batch.size.to_f / 1000) * (5 * 60)).to_i
321
+ end
322
+
323
+ def subscribe_one_at_a_time(subscribers, opts)
324
+ cumulative_response = { :success => 0, :errors => [] }
325
+ subscribers.each do |subscriber|
326
+ params = opts.merge(subscriber)
327
+ params.delete(:email)
328
+ if subscribe_one(subscriber[:email], params) == true
329
+ cumulative_response[:success] += 1
330
+ else
331
+ cumulative_response[:errors] << response["error"]
332
+ end
333
+ end
334
+ cumulative_response
335
+ end
336
+
337
+ def subscribe_one(email_address, opts = {})
338
+ params = {
339
+ :type => opts.delete(:type),
340
+ :double_optin => opts.delete(:double_optin),
341
+ :update_existing => opts.delete(:update_existing),
342
+ :replace_interests => opts.delete(:replace_interests),
343
+ :send_welcome => opts.delete(:send_welcome),
344
+ :email_address => email_address
345
+ }
346
+ params.reject!{ |k,v| v.nil? }
347
+ merge_vars = { :merge_vars => opts }.to_mailchimp
348
+ params.merge!(merge_vars)
349
+ params.merge!(:method => "listSubscribe", :id => self.id)
350
+ post(params)
351
+ end
352
+ end
353
+ end