gandi_v5 0.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 (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