madmimi 1.0.16 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (115) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +12 -0
  4. data/Gemfile.lock +93 -0
  5. data/README.rdoc +88 -37
  6. data/Rakefile +2 -30
  7. data/VERSION +1 -1
  8. data/config/paths.yml +23 -0
  9. data/lib/madmimi.rb +210 -123
  10. data/madmimi.gemspec +130 -22
  11. data/spec/cassettes/add_to_list/success.yml +54 -0
  12. data/spec/cassettes/add_to_list/user_does_not_exist.yml +165 -0
  13. data/spec/cassettes/add_to_list/user_existed.yml +54 -0
  14. data/spec/cassettes/add_to_list/user_exists.yml +171 -0
  15. data/spec/cassettes/add_to_list/user_updated.yml +174 -0
  16. data/spec/cassettes/add_user/user_did_not_exist.yml +54 -0
  17. data/spec/cassettes/add_user/user_exist.yml +303 -0
  18. data/spec/cassettes/add_user/user_existed.yml +54 -0
  19. data/spec/cassettes/add_user/user_missing.yml +138 -0
  20. data/spec/cassettes/add_user/user_updated.yml +153 -0
  21. data/spec/cassettes/add_users/users_did_not_exist.yml +54 -0
  22. data/spec/cassettes/add_users/users_exist.yml +326 -0
  23. data/spec/cassettes/add_users/users_existed.yml +54 -0
  24. data/spec/cassettes/add_users/users_missing.yml +305 -0
  25. data/spec/cassettes/add_users/users_updated.yml +327 -0
  26. data/spec/cassettes/add_users_to_list/gained_membership.yml +107 -0
  27. data/spec/cassettes/add_users_to_list/missing_membership.yml +174 -0
  28. data/spec/cassettes/add_users_to_list/users_did_not_exist.yml +54 -0
  29. data/spec/cassettes/add_users_to_list/users_exist.yml +365 -0
  30. data/spec/cassettes/add_users_to_list/users_existed.yml +54 -0
  31. data/spec/cassettes/add_users_to_list/users_missing.yml +341 -0
  32. data/spec/cassettes/add_users_to_list/users_updated.yml +365 -0
  33. data/spec/cassettes/audience_search/does_not_include_suppressed_users.yml +76 -0
  34. data/spec/cassettes/audience_search/includes_suppressed_users.yml +86 -0
  35. data/spec/cassettes/csv_import/success.yml +54 -0
  36. data/spec/cassettes/csv_import/users_exist.yml +273 -0
  37. data/spec/cassettes/csv_import/users_missing.yml +381 -0
  38. data/spec/cassettes/delete_list/exists.yml +57 -0
  39. data/spec/cassettes/delete_list/fail.yml +54 -0
  40. data/spec/cassettes/delete_list/not_exists.yml +56 -0
  41. data/spec/cassettes/delete_list/success.yml +54 -0
  42. data/spec/cassettes/delete_list/users_exist.yml +129 -0
  43. data/spec/cassettes/list_members/multiple.yml +68 -0
  44. data/spec/cassettes/list_members/single.yml +68 -0
  45. data/spec/cassettes/lists/multiple.yml +56 -0
  46. data/spec/cassettes/lists/single.yml +55 -0
  47. data/spec/cassettes/mailing_stats/mailing_does_not_exist.yml +50 -0
  48. data/spec/cassettes/mailing_stats/promotion_and_mailing_exist.yml +57 -0
  49. data/spec/cassettes/mailing_stats/promotion_does_not_exist.yml +50 -0
  50. data/spec/cassettes/members/multiple.yml +174 -0
  51. data/spec/cassettes/members/single.yml +61 -0
  52. data/spec/cassettes/memberships/multiple.yml +56 -0
  53. data/spec/cassettes/memberships/single.yml +108 -0
  54. data/spec/cassettes/new_list/exists.yml +57 -0
  55. data/spec/cassettes/new_list/fail.yml +52 -0
  56. data/spec/cassettes/new_list/not_exists.yml +56 -0
  57. data/spec/cassettes/new_list/success.yml +52 -0
  58. data/spec/cassettes/promotions/multiple.yml +63 -0
  59. data/spec/cassettes/promotions/single.yml +61 -0
  60. data/spec/cassettes/remove_from_all_list/user_does_not_have_memberships.yml +60 -0
  61. data/spec/cassettes/remove_from_all_lists/user_has_memberships.yml +113 -0
  62. data/spec/cassettes/remove_from_all_lists/user_with_memberships.yml +49 -0
  63. data/spec/cassettes/remove_from_list/user_does_not_exist.yml +49 -0
  64. data/spec/cassettes/remove_from_list/user_in_list_does_not_exist.yml +62 -0
  65. data/spec/cassettes/remove_from_list/user_in_list_exists.yml +68 -0
  66. data/spec/cassettes/remove_from_list/user_in_the_list.yml +49 -0
  67. data/spec/cassettes/remove_from_list/user_not_in_the_list.yml +49 -0
  68. data/spec/cassettes/save_promotion/only_plain_text.yml +56 -0
  69. data/spec/cassettes/save_promotion/only_raw_html.yml +56 -0
  70. data/spec/cassettes/save_promotion/promotion_exists.yml +65 -0
  71. data/spec/cassettes/save_promotion/promotion_missing.yml +63 -0
  72. data/spec/cassettes/save_promotion/raw_html_and_plain_text.yml +56 -0
  73. data/spec/cassettes/send_html/promotion_does_not_exist.yml +56 -0
  74. data/spec/cassettes/send_html/send_single.yml +53 -0
  75. data/spec/cassettes/send_html/send_to_all.yml +56 -0
  76. data/spec/cassettes/send_html/send_to_list.yml +56 -0
  77. data/spec/cassettes/send_mail/promotion_does_not_exist.yml +54 -0
  78. data/spec/cassettes/send_mail/send_single.yml +53 -0
  79. data/spec/cassettes/send_mail/send_to_all.yml +56 -0
  80. data/spec/cassettes/send_mail/send_to_list.yml +56 -0
  81. data/spec/cassettes/send_plaintext/promotion_does_not_exist.yml +56 -0
  82. data/spec/cassettes/send_plaintext/send_single.yml +53 -0
  83. data/spec/cassettes/send_plaintext/send_to_all.yml +56 -0
  84. data/spec/cassettes/send_plaintext/send_to_list.yml +56 -0
  85. data/spec/cassettes/send_plaintext/tracking_beacon_missing.yml +56 -0
  86. data/spec/cassettes/status/transactional_mail_does_not_exist.yml +51 -0
  87. data/spec/cassettes/status/transactional_mail_exists.yml +53 -0
  88. data/spec/cassettes/suppress_email/already_suppressed.yml +51 -0
  89. data/spec/cassettes/suppress_email/not_suppressed.yml +102 -0
  90. data/spec/cassettes/suppress_email/user_does_not_exist.yml +100 -0
  91. data/spec/cassettes/suppress_email/user_is_now_suppressed.yml +51 -0
  92. data/spec/cassettes/suppress_email/user_not_suppressed.yml +51 -0
  93. data/spec/cassettes/suppressed/member_does_not_exist.yml +49 -0
  94. data/spec/cassettes/suppressed/member_is_not_suppressed.yml +51 -0
  95. data/spec/cassettes/suppressed/member_is_suppressed.yml +51 -0
  96. data/spec/cassettes/suppressed_since/members_exist.yml +56 -0
  97. data/spec/cassettes/suppressed_since/no_members.yml +56 -0
  98. data/spec/cassettes/unsuppress_email/already_suppressed.yml +100 -0
  99. data/spec/cassettes/unsuppress_email/not_suppressed.yml +99 -0
  100. data/spec/cassettes/unsuppress_email/user_does_not_exist.yml +99 -0
  101. data/spec/cassettes/unsuppress_email/user_is_now_unsuppressed.yml +49 -0
  102. data/spec/cassettes/unsuppress_email/user_suppressed.yml +51 -0
  103. data/spec/cassettes/update_email/user_does_not_have_permission.yml +52 -0
  104. data/spec/cassettes/update_email/user_existed.yml +54 -0
  105. data/spec/cassettes/update_email/user_missing.yml +52 -0
  106. data/spec/cassettes/update_email/user_with_new_email.yml +345 -0
  107. data/spec/cassettes/update_email/user_with_old_email.yml +345 -0
  108. data/spec/madmimi_spec.rb +1166 -0
  109. data/spec/spec_helper.rb +15 -0
  110. metadata +210 -96
  111. data/test/fixtures/lists.xml +0 -7
  112. data/test/fixtures/promotions.xml +0 -10
  113. data/test/fixtures/search.xml +0 -38
  114. data/test/helper.rb +0 -43
  115. data/test/test_madmimi.rb +0 -63
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: be4672c1319bb8ab018016377765b23bf7dd99a5
4
+ data.tar.gz: edcb37c771c1c11a7ebe34342ebd8971d7d26285
5
+ SHA512:
6
+ metadata.gz: e577ff522f35537b25bea05a7d8aa1a4bf9064b8678f510058413cef9fd94a671a4fa179b29b8f216cf5c160ec1400d065de66e5470c5cbc9067480c102f22bb
7
+ data.tar.gz: 26eac26ebec69e51f232b1913f4238d3a3f041bdb1559e2aacc9dc27e1768606961933026d0d8bf9e664e7b52c862bd264e55dc14536648efc0e4397e55668ae
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem "activesupport", ">3.0.0"
4
+ gem "crack", ">0.1.7"
5
+ gem "httparty", ">=0.13.1"
6
+
7
+ group :development do
8
+ gem 'jeweler', '> 1.4'
9
+ gem 'rspec'
10
+ gem 'vcr'
11
+ gem 'webmock'
12
+ end
@@ -0,0 +1,93 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ activesupport (4.1.5)
5
+ i18n (~> 0.6, >= 0.6.9)
6
+ json (~> 1.7, >= 1.7.7)
7
+ minitest (~> 5.1)
8
+ thread_safe (~> 0.1)
9
+ tzinfo (~> 1.1)
10
+ addressable (2.3.5)
11
+ builder (3.2.2)
12
+ crack (0.4.2)
13
+ safe_yaml (~> 1.0.0)
14
+ descendants_tracker (0.0.3)
15
+ diff-lcs (1.2.5)
16
+ faraday (0.9.0)
17
+ multipart-post (>= 1.2, < 3)
18
+ git (1.2.6)
19
+ github_api (0.11.2)
20
+ addressable (~> 2.3)
21
+ descendants_tracker (~> 0.0.1)
22
+ faraday (~> 0.8, < 0.10)
23
+ hashie (>= 1.2)
24
+ multi_json (>= 1.7.5, < 2.0)
25
+ nokogiri (~> 1.6.0)
26
+ oauth2
27
+ hashie (2.0.5)
28
+ highline (1.6.20)
29
+ httparty (0.13.1)
30
+ json (~> 1.8)
31
+ multi_xml (>= 0.5.2)
32
+ i18n (0.6.11)
33
+ jeweler (2.0.1)
34
+ builder
35
+ bundler (>= 1.0)
36
+ git (>= 1.2.5)
37
+ github_api
38
+ highline (>= 1.6.15)
39
+ nokogiri (>= 1.5.10)
40
+ rake
41
+ rdoc
42
+ json (1.8.1)
43
+ jwt (0.1.11)
44
+ multi_json (>= 1.5)
45
+ mini_portile (0.5.2)
46
+ minitest (5.4.0)
47
+ multi_json (1.8.4)
48
+ multi_xml (0.5.5)
49
+ multipart-post (2.0.0)
50
+ nokogiri (1.6.1)
51
+ mini_portile (~> 0.5.0)
52
+ oauth2 (0.9.3)
53
+ faraday (>= 0.8, < 0.10)
54
+ jwt (~> 0.1.8)
55
+ multi_json (~> 1.3)
56
+ multi_xml (~> 0.5)
57
+ rack (~> 1.2)
58
+ rack (1.5.2)
59
+ rake (10.1.1)
60
+ rdoc (4.1.1)
61
+ json (~> 1.4)
62
+ rspec (3.0.0)
63
+ rspec-core (~> 3.0.0)
64
+ rspec-expectations (~> 3.0.0)
65
+ rspec-mocks (~> 3.0.0)
66
+ rspec-core (3.0.3)
67
+ rspec-support (~> 3.0.0)
68
+ rspec-expectations (3.0.3)
69
+ diff-lcs (>= 1.2.0, < 2.0)
70
+ rspec-support (~> 3.0.0)
71
+ rspec-mocks (3.0.3)
72
+ rspec-support (~> 3.0.0)
73
+ rspec-support (3.0.3)
74
+ safe_yaml (1.0.1)
75
+ thread_safe (0.3.4)
76
+ tzinfo (1.2.2)
77
+ thread_safe (~> 0.1)
78
+ vcr (2.9.2)
79
+ webmock (1.11.0)
80
+ addressable (>= 2.2.7)
81
+ crack (>= 0.3.2)
82
+
83
+ PLATFORMS
84
+ ruby
85
+
86
+ DEPENDENCIES
87
+ activesupport (> 3.0.0)
88
+ crack (> 0.1.7)
89
+ httparty (>= 0.13.1)
90
+ jeweler (> 1.4)
91
+ rspec
92
+ vcr
93
+ webmock
@@ -4,70 +4,124 @@ The power of Mad Mimi in your Ruby application. Deliver emails, track statistics
4
4
 
5
5
  == Installation
6
6
 
7
- gem install madmimi - or if you prefer to live on the edge, just clone this repository and build it from scratch.
7
+ $ gem install madmimi
8
+
9
+ or if you prefer to live on the edge, just clone this repository and build it from scratch.
10
+
11
+ == Dependencies
12
+
13
+ * active_support (I intend to remove this in the not too distant future, and build my own implementation.)
8
14
 
9
15
  == Basic Usage
10
16
 
11
- Dependencies:
12
- active_support (I intend to remove this in the not too distant future, and build my own implementation.)
17
+ mimi = MadMimi.new('emailaddress', 'api_key')
18
+ mimi.lists # get all of your Mad Mimi lists returned as a hash
13
19
 
14
- mimi = MadMimi.new('emailaddress', 'api_key')
20
+ You can pass +raise_exceptions+ to the initializer, if you wish to receive exceptions for failing requests:
15
21
 
16
- mimi.lists -> get all of your Mad Mimi lists returned as a hash
22
+ MadMimi.new('emailaddress', 'api_key', { :raise_exceptions => true })
23
+ mimi.lists # will raise an Exception if request fails
24
+
25
+ You can set +verify_ssl+ to either true or false in initializer, if you wish verify SSL or not in requests:
26
+
27
+ MadMimi.new('emailaddress', 'api_key', { :verify_ssl => true })
17
28
 
18
29
  === Audience Members and Lists
19
30
 
20
- mimi.memberships('email') -> returns a hash of the lists that specific email address is subscribed to
31
+ mimi.memberships('email') # returns a hash of the lists that specific email address is subscribed to
32
+
33
+ mimi.new_list('New list name') # make a new list
34
+
35
+ mimi.delete_list('New list name') # delete the list I just created
36
+
37
+ mimi.csv_import("name,email\ndave,dave@example.com\n") # import from a csv string
38
+
39
+ mimi.add_user({ :email => 'dave@example.com', :first_name => 'Dave' }) # add new audience member
40
+
41
+ mimi.add_users([
42
+ { :email => 'dave@example.com', :first_name => 'Dave' },
43
+ { :email => 'custom@example.com', :custom_field_1 => 'Dummy value' },
44
+ { :email => 'smith@example.com', :last_name => 'Smith' }
45
+ ]) # add audience members in bulk
46
+
47
+ mimi.add_to_list('dave@example.com', 'Test List') # add this email address to a specific list
48
+ mimi.add_to_list('dave@example.com', 'Test List', {
49
+ :first_name => 'Dave',
50
+ :last_name => 'Example',
51
+ :custom_field => 'Custom value'
52
+ }) # add additional data with this email
21
53
 
22
- mimi.new_list('New list name') -> make a new list
54
+ mimi.remove_from_list('dave@example.com', 'Test List') # remove this email address from a specific list
23
55
 
24
- mimi.delete_list('New list name') -> delete the list I just created
56
+ mimi.remove_from_all_lists('dave@example.com') # remove this email address from all lists
25
57
 
26
- mimi.csv_import("name,email\ndave,dave@example.com\n") -> import from a csv string
58
+ # this API call needs advanced permissions (manually requested)
59
+ mimi.update_email('dave@example.com', 'john@example.com') # changes email address for user 'dave@example.com' to 'john@example.com'
27
60
 
28
- mimi.add_to_list('dave@example.com', 'Test List') -> add this email address to a specific list
61
+ mimi.members # get all audience members
29
62
 
30
- mimi.remove_from_list('dave@example.com', 'Test List') -> remove this email address from a specific list
63
+ mimi.list_members('Test List') # get audience members in specific list
31
64
 
32
- mimi.suppressed_since('unix timestamp') -> get a TXT of all addresses that were suppressed since this timestamp
65
+ mimi.suppressed?('dave@example.com') # check if audience member is suppressed
66
+
67
+ mimi.suppress_email('dave@example.com') # move email to suppressed list
68
+
69
+ mimi.unsuppress_email('dave@example.com') # move email from suppressed list
70
+
71
+ mimi.suppressed_since('unix timestamp') # get a TXT of all addresses that were suppressed since this timestamp
33
72
 
34
73
  === Promotions
35
74
 
36
- mimi.promotions -> returns a hash of your promotions
75
+ mimi.promotions # returns a hash of your promotions
37
76
 
38
- mimi.save_promotion('promotion_name', 'raw_html', 'plain_text') -> saves a promotion (creates the promotion if it does not exist)
77
+ mimi.save_promotion('promotion_name', 'raw_html', 'plain_text') # saves a promotion (creates the promotion if it does not exist)
39
78
 
40
- mimi.mailing_stats('promotion_id', 'mailing_id') -> get stats on a specific mailing
79
+ mimi.mailing_stats('promotion_id', 'mailing_id') # get stats on a specific mailing
41
80
 
42
81
  == Sending E-Mail (using the Mailer API)
43
82
 
44
83
  === Replacing keys in your email body text:
45
84
 
46
- options = { 'promotion_name' => 'Test Promotion', 'recipients' => 'Nicholas Young <nicholas@madmimi.com>', 'from' => 'MadMimi Ruby <rubygem@madmimi.com>', 'subject' => 'Test Subject' }
85
+ options = {
86
+ 'promotion_name' => 'Test Promotion',
87
+ 'recipients' => 'Nicholas Young <nicholas@madmimi.com>',
88
+ 'from' => 'MadMimi Ruby <rubygem@madmimi.com>',
89
+ 'subject' => 'Test Subject'
90
+ }
91
+ yaml_body = {
92
+ 'greeting' => 'Hello',
93
+ 'name' => 'Nicholas'
94
+ }
47
95
 
48
- yaml_body = { 'greeting' => 'Hello', 'name' => 'Nicholas' }
49
-
50
- mimi.send_mail(options, yaml_body)
96
+ mimi.send_mail(options, yaml_body)
51
97
 
52
98
  === Sending Raw HTML (presumably generated by your app)
53
99
 
54
- options = { 'promotion_name' => 'Test Promotion', 'recipients' => 'Nicholas Young <nicholas@madmimi.com>', 'from' => 'MadMimi Ruby <rubygem@madmimi.com>', 'subject' => 'Test Subject' }
55
-
56
- raw_html = "<html><head><title>My great promotion!</title></head><body>Body stuff[[tracking_beacon]]</body></html>"
100
+ options = {
101
+ 'promotion_name' => 'Test Promotion',
102
+ 'recipients' => 'Nicholas Young <nicholas@madmimi.com>',
103
+ 'from' => 'MadMimi Ruby <rubygem@madmimi.com>',
104
+ 'subject' => 'Test Subject'
105
+ }
106
+ raw_html = "<html><head><title>My great promotion!</title></head><body>Body stuff[[tracking_beacon]]</body></html>"
57
107
 
58
- mimi.send_html(options, raw_html)
108
+ mimi.send_html(options, raw_html)
59
109
 
60
110
  === Sending Plain Text
61
111
 
62
- options = { 'promotion_name' => 'Test Promotion', 'recipients' => 'Nicholas Young <nicholas@madmimi.com>', 'from' => 'MadMimi Ruby <rubygem@madmimi.com>', 'subject' => 'Test Subject' }
112
+ options = {
113
+ 'promotion_name' => 'Test Promotion',
114
+ 'recipients' => 'Nicholas Young <nicholas@madmimi.com>',
115
+ 'from' => 'MadMimi Ruby <rubygem@madmimi.com>',
116
+ 'subject' => 'Test Subject'
117
+ }
118
+ plain_text = "Plain text email contents [[unsubscribe]]"
63
119
 
64
- plain_text = "Plain text email contents [[unsubscribe]]"
65
-
66
- mimi.send_plaintext(options, plain_text)
120
+ mimi.send_plaintext(options, plain_text)
67
121
 
68
122
  === Getting the status of a transactional mailing
69
123
 
70
- mimi.status('transaction_id') -> get the status on a specific transactional mailing
124
+ mimi.status('transaction_id') # get the status on a specific transactional mailing
71
125
 
72
126
  === Return values
73
127
 
@@ -82,18 +136,14 @@ there is probably an error or uncompleted operation.
82
136
 
83
137
  === Specific options keys
84
138
 
85
- 'raw_html': Must include at least one of the [[tracking_beacon]] or [[peek_image]] tags.
86
-
87
- 'promotion_name': If a promotion doesn't exist under the given name, it will be created.
88
- If it exists and you specify raw_html, the promotion body will be replaced.
89
-
90
- 'list_name': For all of the #send methods, if 'list_name' is provided, the recipients
139
+ * 'raw_html': Must include at least one of the [[tracking_beacon]] or [[peek_image]] tags.
140
+ * 'promotion_name': If a promotion doesn't exist under the given name, it will be created. If it exists and you specify raw_html, the promotion body will be replaced.
141
+ * 'list_name': For all of the #send methods, if 'list_name' is provided, the recipients
91
142
  will be those for an already-existing "audience."
92
-
93
- 'to_all': Set to true to send a promotion, plain_text or raw_html to all your audience members
143
+ * 'to_all': Set to true to send a promotion, plain_text or raw_html to all your audience members
94
144
 
95
145
  == Note on Patches/Pull Requests
96
-
146
+
97
147
  * Fork the project.
98
148
  * Make your feature addition or bug fix.
99
149
  * Add tests for it. This is important so I don't break it in a
@@ -105,6 +155,7 @@ will be those for an already-existing "audience."
105
155
  == Contributors
106
156
  tuker
107
157
  marcheiligers
158
+ maximgladkov
108
159
 
109
160
  == Copyright
110
161
 
data/Rakefile CHANGED
@@ -9,11 +9,7 @@ begin
9
9
  gem.description = %Q{Send emails, track statistics, and manage your subscriber base with ease.}
10
10
  gem.email = "nicholas@madmimi.com"
11
11
  gem.homepage = "http://github.com/madmimi/madmimi-gem"
12
- gem.authors = ["Nicholas Young", "Marc Heiligers"]
13
- gem.add_dependency "crack", ">0.1.7"
14
- gem.add_development_dependency "jeweler", ">1.4"
15
- gem.add_development_dependency "fakeweb", ">1.2"
16
- gem.add_development_dependency "shoulda", ">2.10"
12
+ gem.authors = ["Nicholas Young", "Marc Heiligers", "Maxim Gladkov"]
17
13
  # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
18
14
  end
19
15
  Jeweler::GemcutterTasks.new
@@ -21,31 +17,7 @@ rescue LoadError
21
17
  puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
22
18
  end
23
19
 
24
- require 'rake/testtask'
25
- Rake::TestTask.new(:test) do |test|
26
- test.libs << 'lib' << 'test'
27
- test.pattern = 'test/**/test_*.rb'
28
- test.verbose = true
29
- end
30
-
31
- begin
32
- require 'rcov/rcovtask'
33
- Rcov::RcovTask.new do |test|
34
- test.libs << 'test'
35
- test.pattern = 'test/**/test_*.rb'
36
- test.verbose = true
37
- end
38
- rescue LoadError
39
- task :rcov do
40
- abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
41
- end
42
- end
43
-
44
- task :test => :check_dependencies
45
-
46
- task :default => :test
47
-
48
- require 'rake/rdoctask'
20
+ require 'rdoc/task'
49
21
  Rake::RDocTask.new do |rdoc|
50
22
  version = File.exist?('VERSION') ? File.read('VERSION') : ""
51
23
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.16
1
+ 1.1.0
@@ -0,0 +1,23 @@
1
+ :create_list: /audience_lists.json
2
+ :remove_from_all_lists: /audience_lists/remove_all
3
+ :destroy_list: /audience_lists/%{list}
4
+ :add_to_list: /audience_lists/%{list}/add
5
+ :remove_from_list: /audience_lists/%{list}/remove
6
+ :audience_lists: /audience_lists/lists.xml
7
+ :audience_list_members: /audience_lists/%{list}/members.xml
8
+ :audience_members: /audience_members
9
+ :get_audience_members: /audience_members.xml
10
+ :search: /audience_members/search.xml
11
+ :memberships: /audience_members/%{email}/lists.xml
12
+ :is_suppressed: /audience_members/%{email}/is_suppressed
13
+ :update_user_email: /audience_members/%{email}/update_email
14
+ :suppressed_since: /audience_members/suppressed_since/%{timestamp}.txt
15
+ :suppress_user: /suppressed_audience_members
16
+ :unsuppress_user: /suppressed_audience_members/%{email}
17
+ :promotions: /promotions.xml
18
+ :promotion_save: /promotions/save
19
+ :mailing_stats: /promotions/%{promotion_id}/mailings/%{mailing_id}.xml
20
+ :mailer: /mailer
21
+ :mailer_to_list: /mailer/to_list
22
+ :mailer_to_all: /mailer/to_all
23
+ :mailer_status: /mailers/status/%{transaction_id}
@@ -14,7 +14,7 @@
14
14
  # The above copyright notice and this permission notice shall be included in
15
15
  # all copies or substantial portions of the Software.
16
16
 
17
- # Except as contained in this notice, the name(s) of the above copyright holder(s)
17
+ # Except as contained in this notice, the name(s) of the above copyright holder(s)
18
18
  # shall not be used in advertising or otherwise to promote the sale, use or other
19
19
  # dealings in this Software without prior written authorization.
20
20
 
@@ -26,38 +26,47 @@
26
26
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
27
27
  # THE SOFTWARE.
28
28
 
29
+ require 'active_support/core_ext/string'
30
+ require 'active_support/core_ext/hash'
29
31
  require 'uri'
30
- require 'net/http'
31
- require 'net/https'
32
- require 'crack'
32
+ require 'rubygems'
33
+ require 'httparty'
33
34
  require 'csv'
35
+ require 'yaml'
36
+ require 'crack'
34
37
 
35
38
  class MadMimi
36
39
 
37
- class MadMimiError < StandardError; end
38
-
39
- BASE_URL = 'api.madmimi.com'
40
-
41
- NEW_LISTS_PATH = '/audience_lists'
42
- AUDIENCE_MEMBERS_PATH = '/audience_members'
43
- AUDIENCE_LISTS_PATH = '/audience_lists/lists.xml'
44
- MEMBERSHIPS_PATH = '/audience_members/%email%/lists.xml'
45
- SUPPRESSED_SINCE_PATH = '/audience_members/suppressed_since/%timestamp%.txt'
46
- SUPPRESS_USER_PATH = ' /audience_members/%email%/suppress_email'
47
- SEARCH_PATH = '/audience_members/search.xml'
48
-
49
- PROMOTIONS_PATH = '/promotions.xml'
50
- PROMOTION_SAVE_PATH = '/promotions/save'
51
-
52
- MAILING_STATS_PATH = '/promotions/%promotion_id%/mailings/%mailing_id%.xml'
53
-
54
- MAILER_PATH = '/mailer'
55
- MAILER_TO_LIST_PATH = '/mailer/to_list'
56
- MAILER_TO_ALL_PATH = '/mailer/to_all'
57
- MAILER_STATUS_PATH = '/mailers/status'
58
-
59
- def initialize(username, api_key)
60
- @api_settings = { :username => username, :api_key => api_key }
40
+ MadMimiError = Class.new(StandardError)
41
+
42
+ include HTTParty
43
+
44
+ base_uri 'api.madmimi.com'
45
+
46
+ parser(
47
+ Proc.new do |body, format|
48
+ begin
49
+ case format
50
+ when :json
51
+ Crack::JSON.parse(body)
52
+ when :xml
53
+ Crack::XML.parse(body)
54
+ else
55
+ body
56
+ end
57
+ rescue Crack::ParseError, REXML::ParseException
58
+ body
59
+ end
60
+ end
61
+ )
62
+
63
+ def initialize(username, api_key, options = {})
64
+ @api_settings = options.reverse_merge({
65
+ :verify_ssl => true
66
+ }).merge({
67
+ :username => username,
68
+ :api_key => api_key
69
+ })
61
70
  end
62
71
 
63
72
  def username
@@ -68,95 +77,143 @@ class MadMimi
68
77
  @api_settings[:api_key]
69
78
  end
70
79
 
71
- def default_opt
72
- { :username => username, :api_key => api_key }
80
+ def raise_exceptions?
81
+ @api_settings[:raise_exceptions]
82
+ end
83
+
84
+ def raise_exceptions=(raise_exceptions)
85
+ @api_settings[:raise_exceptions] = raise_exceptions
86
+ end
87
+
88
+ def verify_ssl?
89
+ @api_settings[:verify_ssl]
90
+ end
91
+
92
+ def verify_ssl=(verify_ssl)
93
+ @api_settings[:verify_ssl] = verify_ssl
73
94
  end
74
95
 
75
96
  # Audience and lists
76
97
  def lists
77
- request = do_request(AUDIENCE_LISTS_PATH, :get)
78
- Crack::XML.parse(request)
98
+ wrap_with_array('lists', 'list') do
99
+ do_request(path(:audience_lists), :get, :format => :xml)
100
+ end
79
101
  end
80
102
 
81
103
  def memberships(email)
82
- request = do_request(MEMBERSHIPS_PATH.gsub('%email%', email), :get)
83
- Crack::XML.parse(request)
104
+ wrap_with_array('lists', 'list') do
105
+ do_request(path(:memberships, :email => email), :get)
106
+ end
84
107
  end
85
108
 
86
109
  def new_list(list_name)
87
- do_request(NEW_LISTS_PATH, :post, :name => list_name)
110
+ do_request(path(:create_list), :post, :name => list_name)
88
111
  end
89
112
 
90
113
  def delete_list(list_name)
91
- do_request("#{NEW_LISTS_PATH}/#{URI.escape(list_name)}", :post, :'_method' => 'delete')
114
+ do_request(path(:destroy_list, :list => list_name), :delete)
92
115
  end
93
116
 
94
117
  def csv_import(csv_string)
95
- do_request(AUDIENCE_MEMBERS_PATH, :post, :csv_file => csv_string)
118
+ do_request(path(:audience_members), :post, :csv_file => csv_string)
96
119
  end
97
120
 
98
- def add_user(options)
99
- csv_data = build_csv(options)
100
- do_request(AUDIENCE_MEMBERS_PATH, :post, :csv_file => csv_data)
121
+ def add_user(hash_or_array)
122
+ csv_import(build_csv(hash_or_array))
101
123
  end
102
124
 
103
- def add_to_list(email, list_name)
104
- do_request("#{NEW_LISTS_PATH}/#{URI.escape(list_name)}/add", :post, :email => email)
125
+ alias :add_users :add_user
126
+
127
+ def add_to_list(email, list_name, options={})
128
+ do_request(path(:add_to_list, :list => list_name), :post, options.merge(:email => email))
105
129
  end
106
130
 
107
131
  def remove_from_list(email, list_name)
108
- do_request("#{NEW_LISTS_PATH}/#{URI.escape(list_name)}/remove", :post, :email => email)
132
+ do_request(path(:remove_from_list, :list => list_name), :post, :email => email)
109
133
  end
110
134
 
111
- def suppressed_since(timestamp)
112
- do_request(SUPPRESSED_SINCE_PATH.gsub('%timestamp%', timestamp), :get)
135
+ def remove_from_all_lists(email)
136
+ do_request(path(:remove_from_all_lists), :post, :email => email)
113
137
  end
114
-
138
+
139
+ def update_email(existing_email, new_email)
140
+ do_request(path(:update_user_email, :email => existing_email), :post, :email => existing_email, :new_email => new_email)
141
+ end
142
+
143
+ def members
144
+ wrap_with_array('audience', 'member') do
145
+ do_request(path(:get_audience_members), :get)
146
+ end
147
+ end
148
+
149
+ def list_members(list_name)
150
+ wrap_with_array('audience', 'member') do
151
+ do_request(path(:audience_list_members, :list => list_name), :get)
152
+ end
153
+ end
154
+
155
+ def suppressed_since(timestamp, show_suppression_reason = false)
156
+ do_request(path(:suppressed_since, :timestamp => timestamp), :get, {
157
+ :show_suppression_reason => show_suppression_reason
158
+ })
159
+ end
160
+
115
161
  def suppress_email(email)
116
- do_request(SUPPRESS_USER_PATH.gsub('%email%', email), :post)
162
+ return '' if suppressed?(email)
163
+
164
+ process_json_response do
165
+ do_request(path(:suppress_user), :post, :audience_member_id => email, :format => :json)
166
+ end
167
+ end
168
+
169
+ def unsuppress_email(email)
170
+ return '' unless suppressed?(email)
171
+
172
+ process_json_response do
173
+ do_request(path(:unsuppress_user, :email => email), :delete, :format => :json)
174
+ end
175
+ end
176
+
177
+ def suppressed?(email)
178
+ response = do_request(path(:is_suppressed, :email => email), :get)
179
+ response == 'true'
117
180
  end
118
181
 
119
182
  def audience_search(query_string, raw = false)
120
- request = do_request(SEARCH_PATH, :get, :raw => raw, :query => query_string)
121
- Crack::XML.parse(request)
183
+ do_request(path(:search), :get, :raw => raw, :query => query_string)
122
184
  end
123
185
 
124
- # Not the most elegant, but it works for now. :)
125
186
  def add_users_to_list(list_name, arr)
126
- arr.each do |a|
127
- a[:add_list] = list_name
128
- add_user(a)
129
- end
187
+ add_users(arr.map{ |a| a[:add_list] = list_name; a })
130
188
  end
131
-
189
+
132
190
  # Promotions
133
191
  def promotions
134
- request = do_request(PROMOTIONS_PATH, :get)
135
- Crack::XML.parse(request)
192
+ wrap_with_array('promotions', 'promotion') do
193
+ do_request(path(:promotions), :get)
194
+ end
136
195
  end
137
-
196
+
138
197
  def save_promotion(promotion_name, raw_html, plain_text = nil)
139
198
  options = { :promotion_name => promotion_name }
140
-
199
+
141
200
  unless raw_html.nil?
142
201
  check_for_tracking_beacon raw_html
143
202
  check_for_opt_out raw_html
144
203
  options[:raw_html] = raw_html
145
204
  end
146
-
205
+
147
206
  unless plain_text.nil?
148
207
  check_for_opt_out plain_text
149
208
  options[:raw_plain_text] = plain_text
150
209
  end
151
-
152
- do_request PROMOTION_SAVE_PATH, :post, options
210
+
211
+ do_request(path(:promotion_save), :post, options)
153
212
  end
154
-
213
+
155
214
  # Stats
156
215
  def mailing_stats(promotion_id, mailing_id)
157
- path = MAILING_STATS_PATH.gsub('%promotion_id%', promotion_id).gsub('%mailing_id%', mailing_id)
158
- request = do_request(path, :get)
159
- Crack::XML.parse(request)
216
+ do_request(path(:mailing_stats, :promotion_id => promotion_id, :mailing_id => mailing_id), :get)
160
217
  end
161
218
 
162
219
  # Mailer API
@@ -164,12 +221,12 @@ class MadMimi
164
221
  options = opt.dup
165
222
  options[:body] = yaml_body.to_yaml
166
223
  if !options[:list_name].nil? || options[:to_all]
167
- do_request(options[:to_all] ? MAILER_TO_ALL_PATH : MAILER_TO_LIST_PATH, :post, options, true)
224
+ do_request(path(options[:to_all] ? :mailer_to_all : :mailer_to_list), :post, options, true)
168
225
  else
169
- do_request(MAILER_PATH, :post, options, true)
226
+ do_request(path(:mailer), :post, options, true)
170
227
  end
171
228
  end
172
-
229
+
173
230
  def send_html(opt, html)
174
231
  options = opt.dup
175
232
  if html.include?('[[tracking_beacon]]') || html.include?('[[peek_image]]')
@@ -178,9 +235,9 @@ class MadMimi
178
235
  unless html.include?('[[unsubscribe]]') || html.include?('[[opt_out]]')
179
236
  raise MadMimiError, "When specifying list_name, include the [[unsubscribe]] or [[opt_out]] macro in your HTML before sending."
180
237
  end
181
- do_request(options[:to_all] ? MAILER_TO_ALL_PATH : MAILER_TO_LIST_PATH, :post, options, true)
238
+ do_request(path(options[:to_all] ? :mailer_to_all : :mailer_to_list), :post, options, true)
182
239
  else
183
- do_request(MAILER_PATH, :post, options, true)
240
+ do_request(path(:mailer), :post, options, true)
184
241
  end
185
242
  else
186
243
  raise MadMimiError, "You'll need to include either the [[tracking_beacon]] or [[peek_image]] macro in your HTML before sending."
@@ -192,89 +249,119 @@ class MadMimi
192
249
  options[:raw_plain_text] = plaintext
193
250
  if !options[:list_name].nil? || options[:to_all]
194
251
  if plaintext.include?('[[unsubscribe]]') || plaintext.include?('[[opt_out]]')
195
- do_request(options[:to_all] ? MAILER_TO_ALL_PATH : MAILER_TO_LIST_PATH, :post, options, true)
252
+ do_request(path(options[:to_all] ? :mailer_to_all : :mailer_to_list), :post, options, true)
196
253
  else
197
254
  raise MadMimiError, "You'll need to include either the [[unsubscribe]] or [[opt_out]] macro in your text before sending."
198
255
  end
199
256
  else
200
- do_request(MAILER_PATH, :post, options, true)
257
+ do_request(path(:mailer), :post, options, true)
201
258
  end
202
259
  end
203
-
260
+
204
261
  def status(transaction_id)
205
- do_request "#{MAILER_STATUS_PATH}/#{transaction_id}", :get, {}, true
262
+ do_request(path(:mailer_status, :transaction_id => transaction_id), :get, {}, true)
206
263
  end
207
264
 
208
265
  private
209
266
 
210
267
  # Refactor this method asap
211
- def do_request(path, req_type = :get, options = {}, transactional = false)
212
- options = options.merge(default_opt)
213
- form_data = options.inject({}) { |m, (k, v)| m[k.to_s] = v; m }
214
- resp = href = ""
215
-
216
- if transactional == true
217
- http = Net::HTTP.new(BASE_URL, 443)
218
- http.use_ssl = true
219
- http.ssl_version = "SSLv3" if http.respond_to?(:ssl_version)
220
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
221
- else
222
- http = Net::HTTP.new(BASE_URL, 80)
223
- end
224
-
225
- case req_type
226
-
227
- when :get then
228
- begin
229
- http.start do |http|
230
- req = Net::HTTP::Get.new(path)
231
- req.set_form_data(form_data)
232
- response = http.request(req)
233
- resp = response.body.strip
234
- end
235
- resp
236
- rescue SocketError
237
- raise "Host unreachable."
238
- end
239
- when :post then
240
- begin
241
- http.start do |http|
242
- req = Net::HTTP::Post.new(path)
243
- req.set_form_data(form_data)
244
- response = http.request(req)
245
- resp = response.body.strip
246
- end
247
- rescue SocketError
248
- raise "Host unreachable."
249
- end
250
- end
268
+ def do_request(path, method = :get, options = {}, transactional = false)
269
+ options = default_options.deep_merge({
270
+ :format => options.delete(:format) || extract_format(path),
271
+ :body => options
272
+ })
273
+
274
+ path = convert_to_secure(path) if transactional
275
+
276
+ response = self.class.send(method, path, options)
277
+
278
+ response.value if raise_exceptions?
279
+ response.parsed_response
251
280
  end
252
281
 
253
- def build_csv(hash)
282
+ def build_csv(hash_or_array)
283
+ hashes = Array.wrap(hash_or_array)
284
+ columns = hashes.map(&:keys).flatten.uniq
285
+
254
286
  if CSV.respond_to?(:generate_row) # before Ruby 1.9
255
287
  buffer = ''
256
- CSV.generate_row(hash.keys, hash.keys.size, buffer)
257
- CSV.generate_row(hash.values, hash.values.size, buffer)
288
+ CSV.generate_row(columns, columns.size, buffer)
289
+ hashes.each do |hash|
290
+ values = columns.map{ |c| hash[c] }
291
+ CSV.generate_row(values, values.size, buffer)
292
+ end
258
293
  buffer
259
294
  else # Ruby 1.9 and after
260
295
  CSV.generate do |csv|
261
- csv << hash.keys
262
- csv << hash.values
296
+ csv << columns
297
+ hashes.each do |hash|
298
+ csv << columns.map{ |c| hash[c] }
299
+ end
263
300
  end
264
301
  end
265
302
  end
266
-
303
+
267
304
  def check_for_tracking_beacon(content)
268
305
  unless content.include?('[[tracking_beacon]]') || content.include?('[[peek_image]]')
269
306
  raise MadMimiError, "You'll need to include either the [[tracking_beacon]] or [[peek_image]] macro in your HTML before sending."
270
307
  end
271
308
  true
272
309
  end
273
-
310
+
274
311
  def check_for_opt_out(content)
275
312
  unless content.include?('[[opt_out]]') || content.include?('[[unsubscribe]]')
276
313
  raise MadMimiError, "When specifying list_name or sending to all, include the [[unsubscribe]] or [[opt_out]] macro in your HTML before sending."
277
314
  end
278
315
  true
279
316
  end
280
- end
317
+
318
+ def process_json_response
319
+ json_response = yield
320
+ begin
321
+ json_response["success"] ? '' : json_response["error"]
322
+ rescue JSON::ParserError
323
+ json_response
324
+ end
325
+ end
326
+
327
+ def default_options
328
+ {
329
+ :body => {
330
+ :username => username,
331
+ :api_key => api_key
332
+ },
333
+ :verify => verify_ssl?
334
+ }
335
+ end
336
+
337
+ def extract_format(path)
338
+ File.extname(path)[1..-1].try(:to_sym)
339
+ end
340
+
341
+ def convert_to_secure(path)
342
+ "#{ self.class.base_uri.gsub('http://', 'https://') }#{ path }"
343
+ end
344
+
345
+ def path(key, arguments={})
346
+ escaped_arguments = arguments.inject({}){ |h, (k, v)| h[k] = URI.escape(v.to_s); h }
347
+ paths[key] % escaped_arguments
348
+ end
349
+
350
+ def paths
351
+ @paths ||= YAML.load(File.read(paths_config_file))
352
+ end
353
+
354
+ def paths_config_file
355
+ @paths_config_file ||= File.join(File.dirname(File.expand_path(__FILE__)), '../config/paths.yml')
356
+ end
357
+
358
+ def wrap_with_array(*args)
359
+ yield.tap do |response|
360
+ obj = args[0..-2].inject(response){ |r, arg| r.try(:[], arg) }
361
+
362
+ if obj && obj[args.last] && obj[args.last].is_a?(Hash)
363
+ obj[args.last] = Array.wrap(obj[args.last])
364
+ end
365
+ end
366
+ end
367
+ end