gandi_v5 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +24 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +20 -0
  5. data/.travis.yml +23 -0
  6. data/CHANGELOG.md +3 -0
  7. data/Gemfile +6 -0
  8. data/Guardfile +40 -0
  9. data/LICENSE.md +32 -0
  10. data/README.md +94 -0
  11. data/Rakefile +3 -0
  12. data/TODO.md +29 -0
  13. data/bin/console +13 -0
  14. data/gandi_v5.gemspec +41 -0
  15. data/lib/gandi_v5/billing/info/prepaid.rb +33 -0
  16. data/lib/gandi_v5/billing/info.rb +26 -0
  17. data/lib/gandi_v5/billing.rb +28 -0
  18. data/lib/gandi_v5/data/converter/array_of.rb +35 -0
  19. data/lib/gandi_v5/data/converter/symbol.rb +26 -0
  20. data/lib/gandi_v5/data/converter/time.rb +28 -0
  21. data/lib/gandi_v5/data/converter.rb +41 -0
  22. data/lib/gandi_v5/data.rb +244 -0
  23. data/lib/gandi_v5/domain/auto_renew.rb +64 -0
  24. data/lib/gandi_v5/domain/contact.rb +102 -0
  25. data/lib/gandi_v5/domain/contract.rb +22 -0
  26. data/lib/gandi_v5/domain/dates.rb +44 -0
  27. data/lib/gandi_v5/domain/renewal_information.rb +41 -0
  28. data/lib/gandi_v5/domain/restore_information.rb +18 -0
  29. data/lib/gandi_v5/domain/sharing_space.rb +21 -0
  30. data/lib/gandi_v5/domain.rb +431 -0
  31. data/lib/gandi_v5/email/mailbox/responder.rb +36 -0
  32. data/lib/gandi_v5/email/mailbox.rb +236 -0
  33. data/lib/gandi_v5/email/offer.rb +27 -0
  34. data/lib/gandi_v5/email/slot.rb +134 -0
  35. data/lib/gandi_v5/email.rb +11 -0
  36. data/lib/gandi_v5/error/gandi_error.rb +21 -0
  37. data/lib/gandi_v5/error.rb +9 -0
  38. data/lib/gandi_v5/live_dns/domain.rb +211 -0
  39. data/lib/gandi_v5/live_dns/record_set.rb +79 -0
  40. data/lib/gandi_v5/live_dns/zone/snapshot.rb +62 -0
  41. data/lib/gandi_v5/live_dns/zone.rb +301 -0
  42. data/lib/gandi_v5/live_dns.rb +30 -0
  43. data/lib/gandi_v5/organization.rb +66 -0
  44. data/lib/gandi_v5/version.rb +5 -0
  45. data/lib/gandi_v5.rb +178 -0
  46. data/spec/.rubocop.yml +4 -0
  47. data/spec/features/domain_spec.rb +45 -0
  48. data/spec/features/livedns_domain_spec.rb +8 -0
  49. data/spec/features/livedns_zone_spec.rb +45 -0
  50. data/spec/features/mailbox_spec.rb +18 -0
  51. data/spec/fixtures/bodies/GandiV5_Billing/info.yaml +10 -0
  52. data/spec/fixtures/bodies/GandiV5_Domain/availability.yaml +15 -0
  53. data/spec/fixtures/bodies/GandiV5_Domain/fetch_contacts.yaml +8 -0
  54. data/spec/fixtures/bodies/GandiV5_Domain/get.yaml +37 -0
  55. data/spec/fixtures/bodies/GandiV5_Domain/list.yaml +20 -0
  56. data/spec/fixtures/bodies/GandiV5_Domain/renewal_info.yaml +12 -0
  57. data/spec/fixtures/bodies/GandiV5_Domain/restore_info.yaml +5 -0
  58. data/spec/fixtures/bodies/GandiV5_Domain/tld.yaml +10 -0
  59. data/spec/fixtures/bodies/GandiV5_Domain/tlds.yaml +7 -0
  60. data/spec/fixtures/bodies/GandiV5_Email_Mailbox/get.yaml +16 -0
  61. data/spec/fixtures/bodies/GandiV5_Email_Mailbox/list.yaml +8 -0
  62. data/spec/fixtures/bodies/GandiV5_Email_Slot/get.yaml +10 -0
  63. data/spec/fixtures/bodies/GandiV5_Email_Slot/list.yaml +8 -0
  64. data/spec/fixtures/bodies/GandiV5_LiveDNS_Domain/get.yaml +4 -0
  65. data/spec/fixtures/bodies/GandiV5_LiveDNS_Domain/list.yaml +2 -0
  66. data/spec/fixtures/bodies/GandiV5_LiveDNS_Zone/get.yaml +11 -0
  67. data/spec/fixtures/bodies/GandiV5_LiveDNS_Zone/list.yaml +11 -0
  68. data/spec/fixtures/bodies/GandiV5_LiveDNS_Zone_Snapshot/get.yaml +9 -0
  69. data/spec/fixtures/bodies/GandiV5_Organization/get.yaml +17 -0
  70. data/spec/fixtures/vcr/Domain_features/List_domains.yml +54 -0
  71. data/spec/fixtures/vcr/Domain_features/Renew_domain.yml +133 -0
  72. data/spec/fixtures/vcr/LiveDNS_Domain_features/List_domains.yml +32 -0
  73. data/spec/fixtures/vcr/LiveDNS_Zone_features/List_zones.yml +42 -0
  74. data/spec/fixtures/vcr/LiveDNS_Zone_features/Make_and_save_snapshot.yml +72 -0
  75. data/spec/fixtures/vcr/LiveDNS_Zone_features/Save_zone_to_file.yml +28 -0
  76. data/spec/fixtures/vcr/Mailbox_features/List_mailboxes.yml +39 -0
  77. data/spec/spec_helper.rb +60 -0
  78. data/spec/test.env +1 -0
  79. data/spec/units/gandi_v5/billing/info/prepaid_spec.rb +20 -0
  80. data/spec/units/gandi_v5/billing/info_spec.rb +4 -0
  81. data/spec/units/gandi_v5/billing_spec.rb +41 -0
  82. data/spec/units/gandi_v5/data/converter/array_of_spec.rb +18 -0
  83. data/spec/units/gandi_v5/data/converter/symbol_spec.rb +16 -0
  84. data/spec/units/gandi_v5/data/converter/time_spec.rb +16 -0
  85. data/spec/units/gandi_v5/data/converter_spec.rb +31 -0
  86. data/spec/units/gandi_v5/data_spec.rb +340 -0
  87. data/spec/units/gandi_v5/domain/auto_renew_spec.rb +70 -0
  88. data/spec/units/gandi_v5/domain/contact_spec.rb +36 -0
  89. data/spec/units/gandi_v5/domain/contract_spec.rb +4 -0
  90. data/spec/units/gandi_v5/domain/dates_spec.rb +4 -0
  91. data/spec/units/gandi_v5/domain/renewal_information_spec.rb +81 -0
  92. data/spec/units/gandi_v5/domain/restore_information_spec.rb +4 -0
  93. data/spec/units/gandi_v5/domain/sharing_space_spec.rb +4 -0
  94. data/spec/units/gandi_v5/domain_spec.rb +451 -0
  95. data/spec/units/gandi_v5/email/mailbox/responder_spec.rb +131 -0
  96. data/spec/units/gandi_v5/email/mailbox_spec.rb +384 -0
  97. data/spec/units/gandi_v5/email/offer_spec.rb +17 -0
  98. data/spec/units/gandi_v5/email/slot_spec.rb +102 -0
  99. data/spec/units/gandi_v5/error/gandi_error_spec.rb +30 -0
  100. data/spec/units/gandi_v5/error_spec.rb +4 -0
  101. data/spec/units/gandi_v5/live_dns/domain_spec.rb +247 -0
  102. data/spec/units/gandi_v5/live_dns/record_set_spec.rb +74 -0
  103. data/spec/units/gandi_v5/live_dns/zone/snapshot_spec.rb +37 -0
  104. data/spec/units/gandi_v5/live_dns/zone_spec.rb +329 -0
  105. data/spec/units/gandi_v5/live_dns_spec.rb +17 -0
  106. data/spec/units/gandi_v5/organization_spec.rb +30 -0
  107. data/spec/units/gandi_v5_spec.rb +204 -0
  108. metadata +406 -0
@@ -0,0 +1,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'mailbox/responder'
4
+
5
+ class GandiV5
6
+ class Email
7
+ # A mailbox that lives within a domain.
8
+ # @!attribute [r] address
9
+ # @return [String] full email address.
10
+ # @!attribute [r] fqdn
11
+ # @return [String] domain name.
12
+ # @!attribute [r] uuid
13
+ # @return [String]
14
+ # @!attribute [r] login
15
+ # @return [String] mailbox login.
16
+ # @!attribute [r] type
17
+ # @return [:standard, :premium, :free]
18
+ # @!attribute [r] quota_used
19
+ # @return [Integer]
20
+ # @!attribute [r] aliases
21
+ # @return [nil, Array<String>] mailbox alias list.
22
+ # A local-part (what comes before the "@") of an email address. It can contain a wildcard
23
+ # "*" before or after at least two characters to redirect everything thats matches the
24
+ # local-part pattern.
25
+ # @!attribute [r] fallback_email
26
+ # @return [nil, String] fallback email addresse.
27
+ # @!attribute [r] responder
28
+ # @return [nil, GandiV5::Email::Mailbox::Responder]
29
+ class Mailbox
30
+ include GandiV5::Data
31
+
32
+ TYPES = %i[standard premium free].freeze
33
+ QUOTAS = {
34
+ free: 3 * 1024**3,
35
+ standard: 3 * 1024**3,
36
+ premium: 50 * 1024**3
37
+ }.freeze
38
+
39
+ members :address, :login, :quota_used, :aliases, :fallback_email
40
+ member :type, gandi_key: 'mailbox_type', converter: GandiV5::Data::Converter::Symbol
41
+ member :uuid, gandi_key: 'id'
42
+ member :fqdn, gandi_key: 'domain'
43
+ member :responder, converter: GandiV5::Email::Mailbox::Responder
44
+
45
+ alias mailbox_uuid uuid
46
+
47
+ # Delete the mailbox and it's contents.
48
+ # If you delete a mailbox for which you have purchased a slot,
49
+ # this action frees the slot so it once again becomes available
50
+ # for use with a new mailbox, or for deletion.
51
+ # @see https://api.gandi.net/docs/email#delete-v5-email-mailboxes-domain-mailbox_id
52
+ # @return [String] The confirmation message from Gandi.
53
+ # @raise [GandiV5::Error::GandiError::GandiError] if Gandi returns an error.
54
+ def delete
55
+ data = GandiV5.delete url
56
+ data['message']
57
+ end
58
+
59
+ # Purge the contents of the mailbox.
60
+ # @see https://api.gandi.net/docs/email#delete-v5-email-mailboxes-domain-mailbox_id-contents
61
+ # @return [String] The confirmation message from Gandi.
62
+ # @raise [GandiV5::Error::GandiError::GandiError] if Gandi returns an error.
63
+ def purge
64
+ data = GandiV5.delete "#{url}/contents"
65
+ data['message']
66
+ end
67
+
68
+ # Requery Gandi fo this mailbox's information.
69
+ # @return [GandiV5::Email::Mailbox]
70
+ # @raise [GandiV5::Error::GandiError::GandiError] if Gandi returns an error.
71
+ def refresh
72
+ data = GandiV5.get url
73
+ from_gandi data
74
+ end
75
+
76
+ # Update the mailbox's settings.
77
+ # @see https://api.gandi.net/docs/email#patch-v5-email-mailboxes-domain-mailbox_id
78
+ # @param login [String, #to_s] the login name (and first part of email address).
79
+ # @param password [String, #to_s] the password to use.
80
+ # @param aliases [Array<String, #to_s>] any alternative email address to be used.
81
+ # @param responder [Hash, GandiV5::Mailbox::Responder, #to_gandi, #to_h]
82
+ # auto responder settings.
83
+ # @return [String] The confirmation message from Gandi.
84
+ # @raise [GandiV5::Error::GandiError::GandiError] if Gandi returns an error.
85
+ # rubocop:disable Metrics/AbcSize
86
+ def update(**body)
87
+ return 'Nothing to update.' if body.empty?
88
+
89
+ check_password body[:password] if body.key?(:password)
90
+
91
+ body[:password] = crypt_password(body[:password]) if body.key?(:password)
92
+ if (responder = body[:responder])
93
+ body[:responder] = responder.respond_to?(:to_gandi) ? responder.to_gandi : responder.to_h
94
+ end
95
+
96
+ data = GandiV5.patch url, body.to_json
97
+ refresh
98
+ data['message']
99
+ end
100
+ # rubocop:enable Metrics/AbcSize
101
+
102
+ # Create a new mailbox.
103
+ # Note that before you can create a mailbox, you must have a slot available.
104
+ # @see https://api.gandi.net/docs/email#post-v5-email-mailboxes-domain
105
+ # @param fqdn [String, #to_s] the fully qualified domain name for the mailbox.
106
+ # @param login [String, #to_s] the login name (and first part of email address).
107
+ # @param password [String, #to_s] the password to use.
108
+ # @param aliases [Array<String, #to_s>] any alternative email address to be used.
109
+ # @param type [:standard, :premium] the type of mailbox slot to use.
110
+ # @return [String] The confirmation message from Gandi.
111
+ # @raise [GandiV5::Error::GandiError::GandiError] if Gandi returns an error.
112
+ # TODO: Fetch created mailbox
113
+ def self.create(fqdn, login, password, aliases: [], type: :standard)
114
+ # TODO: Check type is valid
115
+ check_password password
116
+ # TODO: Check if a slot is available
117
+
118
+ body = {
119
+ mailbox_type: type,
120
+ login: login,
121
+ password: crypt_password(password),
122
+ aliases: aliases.push
123
+ }.to_json
124
+
125
+ data = GandiV5.post url(fqdn), body
126
+ data['message']
127
+ end
128
+
129
+ # Get information for a mailbox.
130
+ # @see https://api.gandi.net/docs/email#get-v5-email-mailboxes-domain-mailbox_id
131
+ # @param fqdn [String, #to_s] the fully qualified domain name for the mailbox.
132
+ # @param uuid [String, #to_s] unique identifier of the mailbox.
133
+ # @return [GandiV5::Email::Mailbox]
134
+ # @raise [GandiV5::Error::GandiError::GandiError] if Gandi returns an error.
135
+ def self.fetch(fqdn, uuid)
136
+ data = GandiV5.get url(fqdn, uuid)
137
+ from_gandi data
138
+ end
139
+
140
+ # List mailboxes for a domain.
141
+ # @see https://api.gandi.net/docs/email#get-v5-email-mailboxes-domain
142
+ # @param fqdn [String, #to_s] the fully qualified domain name for the mailboxes.
143
+ # @param page [Integer, #each<Integer>] which page(s) of results to get.
144
+ # If page is not provided keep querying until an empty list is returned.
145
+ # If page responds to .each then iterate until an empty list is returned.
146
+ # @param per_page [Integer, #to_s] (optional default 100) how many results ot get per page.
147
+ # @param sort_by [#to_s] (optional default "login")
148
+ # how to sort the results ("login", "-login").
149
+ # @param login [String] (optional) filter the list by login (pattern)
150
+ # e.g. ("alice" "*lice", "alic*").
151
+ # @return [Array<GandiV5::Email::Mailbox>]
152
+ # @raise [GandiV5::Error::GandiError::GandiError] if Gandi returns an error.
153
+ def self.list(fqdn, page: (1..), **params)
154
+ page = [page.to_i] unless page.respond_to?(:each)
155
+
156
+ params['~login'] = params.delete(:login)
157
+ params.reject! { |_k, v| v.nil? }
158
+
159
+ mailboxes = []
160
+ page.each do |page_number|
161
+ data = GandiV5.get url(fqdn), params: params.merge(page: page_number)
162
+ break if data.empty?
163
+
164
+ mailboxes += data.map { |mailbox| from_gandi mailbox }
165
+ break if data.count < params.fetch(:per_page, 100)
166
+ end
167
+ mailboxes
168
+ end
169
+
170
+ # Get the quota for this type of mailbox.
171
+ # @return [Integer] bytes.
172
+ def quota
173
+ QUOTAS[type]
174
+ end
175
+
176
+ # Get the quota usage for this mailbox.
177
+ # @return [Float] fraction of quota used (typically between 0.0 and 1.0)
178
+ def quota_usage
179
+ quota_used.to_f / quota
180
+ end
181
+
182
+ # Returns the string representation of the mailbox.
183
+ # Includes the type, address, quota usage, activeness of responder (if present)
184
+ # and aliases (if present).
185
+ # @return [String]
186
+ def to_s
187
+ s = "[#{type}] #{address} (#{quota_used}/#{quota} (#{(quota_usage * 100).round}%))"
188
+ s += " with #{responder.active? ? 'active' : 'inactive'} responder" if responder
189
+ s += " aka: #{aliases.join(', ')}" if aliases&.any?
190
+ s
191
+ end
192
+
193
+ private
194
+
195
+ # rubocop:disable Style/GuardClause
196
+ def self.check_password(password)
197
+ if !(9..200).cover?(password.length)
198
+ fail ArgumentError, 'password must be between 9 and 200 characters'
199
+ elsif password.count('A-Z') < 1
200
+ fail ArgumentError, 'password must contain at least one upper case character'
201
+ elsif password.count('0-9') < 3
202
+ fail ArgumentError, 'password must contain at least three numbers'
203
+ elsif password.count('^a-z^A-Z^0-9') < 1
204
+ fail ArgumentError, 'password must contain at least one special character'
205
+ end
206
+ end
207
+ private_class_method :check_password
208
+ # rubocop:enable Style/GuardClause
209
+
210
+ def check_password(password)
211
+ self.class.send :check_password, password
212
+ end
213
+
214
+ def url
215
+ "#{BASE}email/mailboxes/#{CGI.escape fqdn}/#{CGI.escape uuid}"
216
+ end
217
+
218
+ def self.url(fqdn, uuid = nil)
219
+ "#{BASE}email/mailboxes/#{CGI.escape fqdn}" +
220
+ (uuid ? "/#{CGI.escape uuid}" : '')
221
+ end
222
+ private_class_method :url
223
+
224
+ def self.crypt_password(password)
225
+ # You can also send a hashed password in sha512-crypt ie: {SHA512-CRYPT}$6$xxxx$yyyy
226
+ salt = SecureRandom.random_number(36**8).to_s(36)
227
+ password.crypt('$6$' + salt)
228
+ end
229
+ private_class_method :crypt_password
230
+
231
+ def crypt_password(password)
232
+ self.class.send :crypt_password, password
233
+ end
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GandiV5
4
+ class Email
5
+ # The current status of your mailbox offer.
6
+ # @!attribute [r] status
7
+ # @return [:active, :inactive]
8
+ # @!attribute [r] version
9
+ # @return [1, 2]
10
+ class Offer
11
+ include GandiV5::Data
12
+
13
+ member :version
14
+ member :status, converter: GandiV5::Data::Converter::Symbol
15
+
16
+ # Get the current status of your mailbox offer.
17
+ # @see https://api.gandi.net/docs/email#get-v5-email-offers-domain
18
+ # @param fqdn [String, #to_s] the fully qualified domain name to get the offer for.
19
+ # @return [GandiV5::Email::Offer]
20
+ # @raise [GandiV5::Error::GandiError::GandiError] if Gandi returns an error.
21
+ def self.fetch(fqdn)
22
+ data = GandiV5.get "#{BASE}email/offers/#{CGI.escape fqdn}"
23
+ from_gandi data
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GandiV5
4
+ class Email
5
+ # A slot is attached to a domain and (optionally) contains a mailbox.
6
+ # There must be an available slot for a mailbox to be created.
7
+ # @!attribute [r] capacity
8
+ # @return [Integer] slot capacity (in MB).
9
+ # @!attribute [r] created_at
10
+ # @return [Time]
11
+ # @!attribute [r] id
12
+ # @return [Integer]
13
+ # @!attribute [r] mailbox_type
14
+ # @return [:standard, :premium]
15
+ # @!attribute [r] status
16
+ # @return [:active, :inactive]
17
+ # @!attribute [r] refundable
18
+ # @return [Boolean]
19
+ # @!attribute [r] refund_amount
20
+ # @return [nil, Numeric] refunded amount if you delete this slot now.
21
+ # @!attribute [r] refund_currency
22
+ # @return [nil, String] refund currency.
23
+ class Slot
24
+ include GandiV5::Data
25
+
26
+ attr_reader :fqdn
27
+
28
+ members :id, :refundable, :refund_amount, :refund_currency
29
+ member(
30
+ :capacity,
31
+ converter: GandiV5::Data::Converter.new(
32
+ from_gandi: ->(value) { value * 1_024**2 }
33
+ )
34
+ )
35
+ member :created_at, converter: GandiV5::Data::Converter::Time
36
+ member :mailbox_type, converter: GandiV5::Data::Converter::Symbol
37
+ member :status, converter: GandiV5::Data::Converter::Symbol
38
+
39
+ alias slot_id id
40
+
41
+ # Create a new GandiV5::Email::Slot
42
+ # @param string [fqdn] the fully qualified domain this slot belongs to.
43
+ # @param members [Hash<Symbol => Object>]
44
+ # @return [GandiV5::Email::Slot]
45
+ def initialize(fqdn: nil, **members)
46
+ super(**members)
47
+ @fqdn = fqdn if fqdn
48
+ end
49
+
50
+ # Delete this slot if it is inactive and refundable.
51
+ # When you delete a slot, the prepaid account that was used to purchase the slot
52
+ # will be refunded for the remaining time that will not be used.
53
+ # @see GandiV5::Email::Mailbox#delete
54
+ # @see https://api.gandi.net/docs/email#delete-v5-email-slots-domain-slot_id
55
+ # @return [String] The confirmation message from Gandi.
56
+ # @raise [GandiV5::Error::GandiError::GandiError] if Gandi returns an error.
57
+ # TODO: check for inactiveness
58
+ # TODO: check for refundableness
59
+ def delete
60
+ data = GandiV5.delete url
61
+ data['message']
62
+ end
63
+
64
+ # Requery Gandi for this slot's information.
65
+ # @see https://api.gandi.net/docs/email#get-v5-email-slots-domain-slot_id
66
+ # @return [GandiV5::Email::Slot]
67
+ # @raise [GandiV5::Error::GandiError::GandiError] if Gandi returns an error.
68
+ def refresh
69
+ data = GandiV5.get url
70
+ from_gandi data
71
+ end
72
+
73
+ # Creates a new slot. You must have slots available before you can create a mailbox.
74
+ # If you have used the two free standard 3GB mailbox slots that are included with the domain,
75
+ # but require more mailboxes on that domain, you must first purchase additional slots.
76
+ # @see https://api.gandi.net/docs/email#post-v5-email-slots-domain
77
+ # @param fqdn [String, #to_s] the fully qualified domain name to add the slot to.
78
+ # @param type [:standard, :premium] Tyhe type of slot to add.
79
+ # @return [String] The confirmation message from Gandi.
80
+ # @raise [GandiV5::Error::GandiError::GandiError] if Gandi returns an error.
81
+ # TODO: Fetch created slot
82
+ def self.create(fqdn, type = :standard)
83
+ body = {
84
+ mailbox_type: type
85
+ }.to_json
86
+
87
+ data = GandiV5.post url(fqdn), body
88
+ data['message']
89
+ end
90
+
91
+ # Get information for a slot.
92
+ # @see https://api.gandi.net/docs/email#get-v5-email-slots-domain-slot_id
93
+ # @param fqdn [String, #to_s] the fully qualified domain name the slot is on.
94
+ # @param id [String, #to_s] the ID of the slot to fetch.
95
+ # @return [GandiV5::Email::Slot]
96
+ # @raise [GandiV5::Error::GandiError::GandiError] if Gandi returns an error.
97
+ def self.fetch(fqdn, id)
98
+ data = GandiV5.get url(fqdn, id)
99
+ slot = from_gandi data
100
+ slot.instance_eval { @fqdn = fqdn }
101
+ slot
102
+ end
103
+
104
+ # List slots for a domain.
105
+ # @see https://api.gandi.net/docs/email#
106
+ # @param fqdn [String, #to_s] the fully qualified domain name to list slots for.
107
+ # @return [Array<GandiV5::Email::Slot>]
108
+ # @raise [GandiV5::Error::GandiError::GandiError] if Gandi returns an error.
109
+ def self.list(fqdn)
110
+ data = GandiV5.get url(fqdn)
111
+ data.map { |item| from_gandi item }
112
+ .each { |item| item.instance_eval { @fqdn = fqdn } }
113
+ end
114
+
115
+ # Check if the slot is active (in use)
116
+ # @return [Boolean]
117
+ def active?
118
+ status.eql?(:active)
119
+ end
120
+
121
+ private
122
+
123
+ def url
124
+ "#{BASE}email/slots/#{CGI.escape fqdn}/#{id}"
125
+ end
126
+
127
+ def self.url(fqdn, id = nil)
128
+ "#{BASE}email/slots/#{CGI.escape fqdn}" +
129
+ (id ? "/#{id}" : '')
130
+ end
131
+ private_class_method :url
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'email/mailbox'
4
+ require_relative 'email/offer'
5
+ require_relative 'email/slot'
6
+
7
+ class GandiV5
8
+ # Gandi Email Mailbox Management API.
9
+ class Email
10
+ end
11
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GandiV5
4
+ class Error < RuntimeError
5
+ # Generic error class for errors returned by Gandi.
6
+ class GandiError < GandiV5::Error
7
+ # Generate a new GandiV5::Error::GandiError::GandiError from the hash returned by Gandi.
8
+ # @param hash [Hash] the hash returned by Gandi.
9
+ # @return [GandiV5::Error::GandiError::GandiError]
10
+ def self.from_hash(hash)
11
+ hash['errors'] ||= []
12
+
13
+ new(
14
+ (hash['errors'].count > 1 ? "\n" : '') +
15
+ hash['errors'].map { |err| "#{err['location']}->#{err['name']}: #{err['description']}" }
16
+ .join("\n")
17
+ )
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'error/gandi_error'
4
+
5
+ class GandiV5
6
+ # Generic error class for errors occuring using the API.
7
+ class Error < RuntimeError
8
+ end
9
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GandiV5
4
+ class LiveDNS
5
+ # A domain name within the LiveDNS system.
6
+ # @!attribute [r] fqdn
7
+ # @return [String]
8
+ # @!attribute [r] zone_uuid
9
+ # @return [String]
10
+ class Domain
11
+ include GandiV5::Data
12
+
13
+ members :fqdn
14
+
15
+ member(
16
+ :zone_uuid,
17
+ gandi_key: 'zone',
18
+ converter: GandiV5::Data::Converter.new(from_gandi: ->(zone) { zone&.split('/')&.last })
19
+ )
20
+
21
+ # Refetch the information for this domain from Gandi.
22
+ # @return [GandiV5::LiveDNS::Domain]
23
+ # @raise [GandiV5::Error::GandiError::GandiError] if Gandi returns an error.
24
+ def refresh
25
+ data = GandiV5.get url
26
+ from_gandi data
27
+ end
28
+
29
+ # @overload fetch_records()
30
+ # Fetch all records for this domain.
31
+ # @overload fetch_records(name)
32
+ # Fetch records for a name.
33
+ # @param name [String] the name to fetch records for.
34
+ # @overload fetch_records(name, type)
35
+ # Fetch records of a type for a name.
36
+ # @param name [String] the name to fetch records for.
37
+ # @param type [String] the record type to fetch.
38
+ # @return [Array<GandiV5::LiveDNS::RecordSet>]
39
+ # @raise [GandiV5::Error::GandiError::GandiError] if Gandi returns an error.
40
+ def fetch_records(name = nil, type = nil)
41
+ GandiV5::LiveDNS.require_valid_record_type type if type
42
+
43
+ url_ = "#{url}/records"
44
+ url_ += "/#{CGI.escape name}" if name
45
+ url_ += "/#{CGI.escape type}" if type
46
+
47
+ data = GandiV5.get url_
48
+ data = [data] unless data.is_a?(Array)
49
+ data.map { |item| GandiV5::LiveDNS::RecordSet.from_gandi item }
50
+ end
51
+
52
+ # @overload fetch_zone_lines()
53
+ # Fetch all records for this domain.
54
+ # @overload fetch_zone_lines(name)
55
+ # Fetch records for a name.
56
+ # @param name [String] the name to fetch records for.
57
+ # @overload fetch_zone_lines(name, type)
58
+ # Fetch records of a type for a name.
59
+ # @param name [String] the name to fetch records for.
60
+ # @param type [String] the record type to fetch.
61
+ # @return [String]
62
+ # @raise [GandiV5::Error::GandiError::GandiError] if Gandi returns an error.
63
+ def fetch_zone_lines(name = nil, type = nil)
64
+ GandiV5::LiveDNS.require_valid_record_type type if type
65
+
66
+ url_ = "#{url}/records"
67
+ url_ += "/#{CGI.escape name}" if name
68
+ url_ += "/#{CGI.escape type}" if type
69
+
70
+ GandiV5.get url_, accept: 'text/plain'
71
+ end
72
+
73
+ # Add record to this domain.
74
+ # @param name [String]
75
+ # @param type [String]
76
+ # @param ttl [Integer]
77
+ # @param values [Array<String>]
78
+ # @return [String] The confirmation message from Gandi.
79
+ # @raise [GandiV5::Error::GandiError::GandiError] if Gandi returns an error.
80
+ def add_record(name, type, ttl, *values)
81
+ GandiV5::LiveDNS.require_valid_record_type type
82
+ fail ArgumentError, 'ttl must be positive and non-zero' unless ttl.positive?
83
+ fail ArgumentError, 'there must be at least one value' if values.none?
84
+
85
+ body = {
86
+ rrset_name: name,
87
+ rrset_type: type,
88
+ rrset_ttl: ttl,
89
+ rrset_values: values
90
+ }.to_json
91
+ data = GandiV5.post "#{url}/records", body
92
+ data['message']
93
+ end
94
+
95
+ # @overload delete_records()
96
+ # Delete all records for this domain.
97
+ # @overload delete_records(name)
98
+ # Delete records for a name.
99
+ # @param name [String] the name to delete records for.
100
+ # @overload delete_records(name, type)
101
+ # Delete records of a type for a name.
102
+ # @param name [String] the name to delete records for.
103
+ # @param type [String] the record type to delete.
104
+ # @return [nil]
105
+ # @raise [GandiV5::Error::GandiError::GandiError] if Gandi returns an error.
106
+ def delete_records(name = nil, type = nil)
107
+ GandiV5::LiveDNS.require_valid_record_type(type) if type
108
+
109
+ url_ = "#{url}/records"
110
+ url_ += "/#{CGI.escape name}" if name
111
+ url_ += "/#{CGI.escape type}" if type
112
+ GandiV5.delete url_
113
+ end
114
+
115
+ # Replace all records for this domain.
116
+ # @param records
117
+ # [Array<Hash<:name, :type => String, :ttl => Integer, :values => Array<String>>>]
118
+ # the records to add.
119
+ # @param text [String] zone file lines to replace the records with.
120
+ # @return [String] The confirmation message from Gandi.
121
+ # @raise [ArgumentError] if neither/both of records & test are passed.
122
+ # @raise [GandiV5::Error::GandiError::GandiError] if Gandi returns an error.
123
+ def replace_records(records: nil, text: nil)
124
+ unless [records, text].count(&:nil?).eql?(1)
125
+ fail ArgumentError, 'you must pass ONE of records: or text:'
126
+ end
127
+
128
+ if records
129
+ body = {
130
+ items: records.map { |r| r.transform_keys { |k| "rrset_#{k}" } }
131
+ }.to_json
132
+ data = GandiV5.put "#{url}/records", body
133
+ elsif text
134
+ data = GandiV5.put "#{url}/records", text, 'content-type': 'text/plain'
135
+ end
136
+ data['message']
137
+ end
138
+
139
+ # Replace records for a name in this domain.
140
+ # @param name [String]
141
+ # @param records
142
+ # [Array<Hash<type: String, ttl: Integer, values: Array<String>>>]
143
+ # the records to add.
144
+ # @return [String] The confirmation message from Gandi.
145
+ # @raise [GandiV5::Error::GandiError::GandiError] if Gandi returns an error.
146
+ def replace_records_for(name, *records)
147
+ body = {
148
+ items: records.map { |r| r.transform_keys { |k| "rrset_#{k}" } }
149
+ }.to_json
150
+ data = GandiV5.put "#{url}/records/#{name}", body
151
+ data['message']
152
+ end
153
+
154
+ GandiV5::LiveDNS::RECORD_TYPES.each do |type|
155
+ # Replace records of a given type for a name in this domain.
156
+ # TODO: @param name [Type] description.
157
+ # TODO: @param ttl [Type] description.
158
+ # TODO: documentation for *values
159
+ # @return [String] The confirmation message from Gandi.
160
+ # @raise [GandiV5::Error::GandiError::GandiError] if Gandi returns an error.
161
+ define_method "replace_#{type.downcase}_records_for" do |name, ttl, *values|
162
+ body = {
163
+ rrset_ttl: ttl,
164
+ rrset_values: values
165
+ }.to_json
166
+ data = GandiV5.put "#{url}/records/#{name}/#{type}", body
167
+ data['message']
168
+ end
169
+ end
170
+
171
+ # Change the zone used by this domain.
172
+ # @param uuid [String, #uuid, #to_s] the UUID of the zone this domain should now use.
173
+ # @return [String] The confirmation message from Gandi.
174
+ # @raise [GandiV5::Error::GandiError::GandiError] if Gandi returns an error.
175
+ def change_zone(uuid)
176
+ uuid = uuid.uuid if uuid.respond_to?(:uuid)
177
+ data = GandiV5.patch url, { zone_uuid: uuid }.to_json
178
+ self.zone_uuid = uuid
179
+ data['message']
180
+ end
181
+
182
+ # List the domains.
183
+ # @return [Array<GandiV5::LiveDNS::Domain>]
184
+ # @raise [GandiV5::Error::GandiError::GandiError] if Gandi returns an error.
185
+ def self.list
186
+ data = GandiV5.get url
187
+ data.map { |item| from_gandi item }
188
+ end
189
+
190
+ # Get a domain.
191
+ # @param fqdn [String, #to_s] the fully qualified domain name to fetch.
192
+ # @return [GandiV5::LiveDNS::Domain]
193
+ # @raise [GandiV5::Error::GandiError::GandiError] if Gandi returns an error.
194
+ def self.fetch(fqdn)
195
+ data = GandiV5.get url(fqdn)
196
+ from_gandi data
197
+ end
198
+
199
+ private
200
+
201
+ def url
202
+ "#{BASE}domains/#{CGI.escape(fqdn)}"
203
+ end
204
+
205
+ def self.url(fqdn = nil)
206
+ "#{BASE}domains" + (fqdn ? "/#{CGI.escape(fqdn)}" : '')
207
+ end
208
+ private_class_method :url
209
+ end
210
+ end
211
+ end