madmimi 1.0.16 → 1.1.0

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 (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