letmesendemail 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.
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module LetMeSendEmail
6
+ # HTTP client and entry point for all API resources.
7
+ class Client
8
+ RETRYABLE_HTTP_STATUSES = [408, 500, 502, 503, 504].freeze
9
+ SAFE_METHODS = %i[get delete].freeze
10
+ MAX_RETRY_DELAY_SECONDS = 300
11
+ BASE_RETRY_DELAY_SECONDS = 0.1
12
+
13
+ # @return [Resources::Emails]
14
+ attr_reader :emails
15
+ # @return [Resources::Domains]
16
+ attr_reader :domains
17
+ # @return [Resources::Contacts]
18
+ attr_reader :contacts
19
+ # @return [Resources::ContactCategories]
20
+ attr_reader :contact_categories
21
+ # @return [Resources::EmailTopics]
22
+ attr_reader :email_topics
23
+
24
+ # @param api_key [String, nil]
25
+ # @param config [Config, nil]
26
+ # @raise [ArgumentError] when configuration is invalid
27
+ def initialize(api_key: nil, config: nil)
28
+ raise ArgumentError, 'provide api_key or config, not both' if api_key && config
29
+
30
+ @config = config || Config.new(api_key || raise(ArgumentError, 'api_key is required'))
31
+ raise ArgumentError, 'config must be a LetMeSendEmail::Config' unless @config.is_a?(Config)
32
+
33
+ @config = @config.dup.validate!.freeze
34
+ @transport = HttpTransport.new(@config)
35
+ initialize_resources
36
+ end
37
+
38
+ # Performs an API request. Resource classes provide the primary public API.
39
+ # @param method [Symbol]
40
+ # @param path [String]
41
+ # @param body [Hash, nil]
42
+ # @param extra_headers [Hash]
43
+ # @return [Models::Model]
44
+ # @raise [Error]
45
+ def request(method, path, body: nil, extra_headers: {})
46
+ retry_eligible = retry_eligible?(method, extra_headers)
47
+ max_attempts = retry_eligible ? @config.retries + 1 : 1
48
+ last_error = nil
49
+
50
+ max_attempts.times do |attempt|
51
+ return @transport.call(method, path, body: body, extra_headers: extra_headers)
52
+ rescue RateLimitError => e
53
+ raise unless attempt < max_attempts - 1 && e.retry_after
54
+
55
+ last_error = e
56
+ sleep(e.retry_after)
57
+ rescue NetworkError, TimeoutError => e
58
+ raise unless attempt < max_attempts - 1
59
+
60
+ last_error = e
61
+ sleep(retry_delay(attempt))
62
+ rescue ApiError => e
63
+ raise unless RETRYABLE_HTTP_STATUSES.include?(e.status_code) && attempt < max_attempts - 1
64
+
65
+ last_error = e
66
+ sleep(retry_delay(attempt))
67
+ end
68
+
69
+ raise last_error
70
+ end
71
+
72
+ # Encodes an untrusted identifier as exactly one URL path segment.
73
+ # @param id [String]
74
+ # @return [String]
75
+ # @raise [ArgumentError] when the identifier is empty or unsafe
76
+ def resource_path_segment(id)
77
+ raise ArgumentError, 'resource id must be a string' unless id.is_a?(String)
78
+
79
+ value = id
80
+ if value.empty? || value != value.strip || %w[. ..].include?(value) || value.match?(/[[:cntrl:]]/)
81
+ raise ArgumentError, 'resource id must be non-empty and must not be a dot segment or contain unsafe whitespace'
82
+ end
83
+
84
+ URI::DEFAULT_PARSER.escape(value, /[^A-Za-z0-9\-._~]/)
85
+ end
86
+
87
+ private
88
+
89
+ def initialize_resources
90
+ @emails = Resources::Emails.new(self)
91
+ @domains = Resources::Domains.new(self)
92
+ @contacts = Resources::Contacts.new(self)
93
+ @contact_categories = Resources::ContactCategories.new(self)
94
+ @email_topics = Resources::EmailTopics.new(self)
95
+ end
96
+
97
+ def retry_eligible?(method, headers)
98
+ return true if SAFE_METHODS.include?(method)
99
+
100
+ key = headers['Idempotency-Key'] || headers['idempotency-key']
101
+ %i[post put].include?(method) && key.is_a?(String) && !key.empty?
102
+ end
103
+
104
+ def retry_delay(attempt)
105
+ base = [BASE_RETRY_DELAY_SECONDS * (2**attempt), MAX_RETRY_DELAY_SECONDS].min
106
+ [base + (rand * base * 0.25), MAX_RETRY_DELAY_SECONDS].min
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module LetMeSendEmail
6
+ # Validated client configuration.
7
+ class Config
8
+ DEFAULT_BASE_URL = 'https://letmesend.email/api/v1'
9
+ DEFAULT_TIMEOUT_MS = 30_000
10
+ DEFAULT_RETRIES = 0
11
+ MAX_RETRIES = 20
12
+
13
+ attr_reader :base_url
14
+ attr_accessor :api_key, :timeout_ms, :retries
15
+
16
+ # @param api_key [String] API bearer token
17
+ def initialize(api_key)
18
+ @api_key = api_key
19
+ @base_url = DEFAULT_BASE_URL
20
+ @timeout_ms = DEFAULT_TIMEOUT_MS
21
+ @retries = DEFAULT_RETRIES
22
+ end
23
+
24
+ # @param url [String] absolute API base URL
25
+ # @return [String]
26
+ def base_url=(url)
27
+ @base_url = url.to_s.sub(%r{/+\z}, '')
28
+ end
29
+
30
+ # Validates and normalizes configuration before use.
31
+ # @return [self]
32
+ # @raise [ArgumentError] for unsafe or unsupported configuration
33
+ def validate!
34
+ validate_api_key!
35
+ validate_base_url!
36
+ validate_timeout!
37
+ validate_retries!
38
+ @api_key = api_key.dup.freeze
39
+ @base_url = base_url.dup.freeze
40
+ self
41
+ end
42
+
43
+ private
44
+
45
+ def validate_api_key!
46
+ return if api_key.is_a?(String) && !api_key.empty? && api_key == api_key.strip && !api_key.match?(/[[:cntrl:]]/)
47
+
48
+ raise ArgumentError, 'api_key must be a non-empty string without surrounding whitespace or control characters'
49
+ end
50
+
51
+ def validate_base_url!
52
+ uri = URI.parse(base_url)
53
+ unless valid_base_uri?(uri)
54
+ raise ArgumentError,
55
+ 'base_url must be an absolute HTTP(S) URL without credentials, query, or fragment'
56
+ end
57
+
58
+ self.base_url = base_url
59
+ rescue URI::InvalidURIError
60
+ raise ArgumentError, 'base_url must be an absolute HTTP(S) URL without credentials, query, or fragment'
61
+ end
62
+
63
+ def valid_base_uri?(uri)
64
+ uri.is_a?(URI::HTTP) && %w[http https].include?(uri.scheme) && uri.host &&
65
+ !uri.userinfo && !uri.query && !uri.fragment
66
+ end
67
+
68
+ def validate_timeout!
69
+ return if timeout_ms.is_a?(Numeric) && timeout_ms.positive?
70
+
71
+ raise ArgumentError, 'timeout_ms must be greater than zero'
72
+ end
73
+
74
+ def validate_retries!
75
+ return if retries.is_a?(Integer) && retries.between?(0, MAX_RETRIES)
76
+
77
+ raise ArgumentError, "retries must be an integer between 0 and #{MAX_RETRIES}"
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LetMeSendEmail
4
+ # Base class for all SDK failures.
5
+ class Error < StandardError
6
+ attr_reader :status_code, :api_code, :validation_errors, :request_id, :response_headers, :raw_body
7
+
8
+ def initialize(message = nil, status_code: nil, api_code: nil,
9
+ validation_errors: nil, request_id: nil,
10
+ response_headers: nil, raw_body: nil)
11
+ super(message)
12
+ @status_code = status_code
13
+ @api_code = api_code
14
+ @validation_errors = validation_errors || {}
15
+ @request_id = request_id
16
+ @response_headers = response_headers || {}
17
+ @raw_body = raw_body
18
+ end
19
+ end
20
+
21
+ # An API response that does not have a more specific mapping.
22
+ class ApiError < Error; end
23
+ # HTTP 401 authentication failure.
24
+ class AuthenticationError < Error; end
25
+ # HTTP 403 authorization failure.
26
+ class AuthorizationError < Error; end
27
+ # HTTP 400, 413, or 422 validation failure.
28
+ class ValidationError < Error; end
29
+
30
+ # HTTP 429 failure with rate-limit metadata.
31
+ class RateLimitError < Error
32
+ attr_reader :retry_after, :limit, :remaining, :reset_at
33
+
34
+ def initialize(message = nil, status_code: nil, api_code: nil,
35
+ validation_errors: nil, request_id: nil,
36
+ response_headers: nil, raw_body: nil,
37
+ retry_after: nil, limit: nil, remaining: nil, reset_at: nil)
38
+ super(message, status_code: status_code, api_code: api_code,
39
+ validation_errors: validation_errors, request_id: request_id,
40
+ response_headers: response_headers, raw_body: raw_body)
41
+ @retry_after = retry_after
42
+ @limit = limit
43
+ @remaining = remaining
44
+ @reset_at = reset_at
45
+ end
46
+ end
47
+
48
+ # HTTP 404 failure.
49
+ class NotFoundError < Error; end
50
+ # HTTP 409 failure.
51
+ class ConflictError < Error; end
52
+ # Connection, socket, TLS, or response transport failure.
53
+ class NetworkError < Error; end
54
+ # HTTP connection, read, or write timeout.
55
+ class TimeoutError < Error; end
56
+ # Invalid webhook request or signature.
57
+ class WebhookVerificationError < Error; end
58
+ # Invalid webhook signing secret.
59
+ class WebhookSigningError < Error; end
60
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'openssl'
6
+ require 'time'
7
+ require 'uri'
8
+
9
+ module LetMeSendEmail
10
+ # Internal net/http transport.
11
+ class HttpTransport
12
+ MAX_RETRY_AFTER_SECONDS = 300
13
+
14
+ def initialize(config)
15
+ @config = config
16
+ end
17
+
18
+ def call(method, path, body: nil, extra_headers: {})
19
+ uri = URI.parse("#{@config.base_url}/#{path.sub(%r{^/}, '')}")
20
+ response = perform(uri, method, body, extra_headers)
21
+ headers = response.each_header.to_h { |key, value| [key.downcase, value] }
22
+ status = response.code.to_i
23
+ raw_body = response.body.to_s
24
+ parsed = parse_response_body(raw_body, status, headers)
25
+
26
+ return Models.wrap(parsed) if status.between?(200, 299)
27
+
28
+ raise error_from_status(status, parsed, headers, raw_body)
29
+ rescue Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout => e
30
+ raise TimeoutError, e.message
31
+ rescue SocketError, SystemCallError, IOError, OpenSSL::SSL::SSLError, Net::HTTPBadResponse => e
32
+ raise NetworkError, e.message
33
+ end
34
+
35
+ private
36
+
37
+ def perform(uri, method, body, extra_headers)
38
+ http = Net::HTTP.new(uri.host, uri.port)
39
+ http.use_ssl = uri.scheme == 'https'
40
+ timeout_seconds = @config.timeout_ms / 1000.0
41
+ http.open_timeout = timeout_seconds
42
+ http.read_timeout = timeout_seconds
43
+ http.write_timeout = timeout_seconds if http.respond_to?(:write_timeout=)
44
+
45
+ request = request_class(method).new(uri)
46
+ request['Authorization'] = "Bearer #{@config.api_key}"
47
+ request['Content-Type'] = 'application/json'
48
+ request['Accept'] = 'application/json'
49
+ request['User-Agent'] = "letmesendemail-ruby/#{LetMeSendEmail::VERSION}"
50
+ extra_headers.each { |key, value| request[key] = value }
51
+ request.body = JSON.generate(body) if body
52
+ http.request(request)
53
+ end
54
+
55
+ def request_class(method)
56
+ {
57
+ get: Net::HTTP::Get,
58
+ post: Net::HTTP::Post,
59
+ put: Net::HTTP::Put,
60
+ delete: Net::HTTP::Delete
61
+ }.fetch(method) { raise ArgumentError, "unknown HTTP method: #{method}" }
62
+ end
63
+
64
+ def parse_response_body(raw_body, status, headers)
65
+ return {} if raw_body.empty?
66
+
67
+ parsed = JSON.parse(raw_body)
68
+ return parsed if parsed.is_a?(Hash)
69
+
70
+ raise_response_error('API response must be a JSON object.', status, headers, raw_body)
71
+ rescue JSON::ParserError => e
72
+ raise_response_error("API returned invalid JSON: #{e.message}", status, headers, raw_body)
73
+ end
74
+
75
+ def raise_response_error(message, status, headers, raw_body)
76
+ raise ApiError.new(message, status_code: status, response_headers: headers, raw_body: raw_body)
77
+ end
78
+
79
+ def error_from_status(status, body, headers, raw_body)
80
+ message = body['message'] || 'Unknown error.'
81
+ keywords = error_keywords(status, body, headers, raw_body)
82
+
83
+ case status
84
+ when 400, 413, 422 then ValidationError.new(message, **keywords)
85
+ when 401 then AuthenticationError.new(message, **keywords)
86
+ when 403 then AuthorizationError.new(message, **keywords)
87
+ when 404 then NotFoundError.new(message, **keywords)
88
+ when 409 then ConflictError.new(message, **keywords)
89
+ when 429 then rate_limit_error(message, keywords, headers)
90
+ else ApiError.new(message, **keywords)
91
+ end
92
+ end
93
+
94
+ def error_keywords(status, body, headers, raw_body)
95
+ {
96
+ status_code: status,
97
+ api_code: body['name'],
98
+ validation_errors: body['errors'],
99
+ request_id: headers['x-request-id'],
100
+ response_headers: headers,
101
+ raw_body: raw_body
102
+ }
103
+ end
104
+
105
+ def rate_limit_error(message, keywords, headers)
106
+ RateLimitError.new(
107
+ message,
108
+ **keywords,
109
+ retry_after: parse_retry_after(headers['retry-after']),
110
+ limit: parse_integer(headers['x-ratelimit-limit']),
111
+ remaining: parse_integer(headers['x-ratelimit-remaining']),
112
+ reset_at: headers['x-ratelimit-reset']
113
+ )
114
+ end
115
+
116
+ def parse_retry_after(value)
117
+ return if value.nil?
118
+
119
+ seconds = value.match?(/\A\d+\z/) ? value.to_i : (Time.httpdate(value) - Time.now).ceil
120
+ seconds if seconds.between?(1, MAX_RETRY_AFTER_SECONDS)
121
+ rescue ArgumentError
122
+ nil
123
+ end
124
+
125
+ def parse_integer(value)
126
+ Integer(value, exception: false)
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module LetMeSendEmail
6
+ # Plain-data wrappers for API responses.
7
+ module Models
8
+ # Application-friendly representation of an API response.
9
+ #
10
+ # Nested hashes and arrays are recursively wrapped. Use {#to_h} for a
11
+ # defensive plain-data copy suitable for database persistence.
12
+ class Model
13
+ include Enumerable
14
+
15
+ # @param attributes [Hash] response attributes
16
+ def initialize(attributes)
17
+ raise ArgumentError, 'attributes must be a Hash' unless attributes.is_a?(Hash)
18
+
19
+ @attributes = attributes.each_with_object({}) do |(key, value), result|
20
+ result[key.to_s.dup.freeze] = Models.wrap(value)
21
+ end.freeze
22
+ end
23
+
24
+ # @param key [String, Symbol] field name
25
+ # @return [Object, nil] wrapped field value
26
+ def [](key)
27
+ @attributes[key.to_s]
28
+ end
29
+
30
+ # @param key [String, Symbol] field name
31
+ # @return [Object] wrapped field value
32
+ def fetch(key, *defaults, &)
33
+ @attributes.fetch(key.to_s, *defaults, &)
34
+ end
35
+
36
+ # @param key [String, Symbol] field name
37
+ # @return [Boolean] whether the field exists
38
+ def key?(key)
39
+ @attributes.key?(key.to_s)
40
+ end
41
+
42
+ # Iterates through response fields.
43
+ # @yieldparam key [String]
44
+ # @yieldparam value [Object]
45
+ # @return [Enumerator, self]
46
+ def each(&block)
47
+ return enum_for(:each) unless block
48
+
49
+ @attributes.each(&block)
50
+ end
51
+
52
+ # @return [Hash] recursive defensive plain-data copy
53
+ def to_h
54
+ Models.unwrap(@attributes)
55
+ end
56
+
57
+ # Rails-compatible JSON representation.
58
+ # @return [Hash]
59
+ def as_json(*)
60
+ to_h
61
+ end
62
+
63
+ # @return [String] JSON document
64
+ def to_json(*args)
65
+ to_h.to_json(*args)
66
+ end
67
+
68
+ # @return [String]
69
+ def inspect
70
+ "#<#{self.class.name} #{@attributes.inspect}>"
71
+ end
72
+ end
73
+
74
+ module_function
75
+
76
+ # Recursively wraps hashes returned by the API.
77
+ # @param value [Object]
78
+ # @return [Object]
79
+ def wrap(value)
80
+ return value.dup.freeze if value.is_a?(String)
81
+ return Model.new(value) if value.is_a?(Hash)
82
+ return value.map { |item| wrap(item) }.freeze if value.is_a?(Array)
83
+
84
+ value
85
+ end
86
+
87
+ # Recursively converts models into plain mutable Ruby data.
88
+ # @param value [Object]
89
+ # @return [Object]
90
+ def unwrap(value)
91
+ return value.dup if value.is_a?(String)
92
+ return value.to_h if value.is_a?(Model)
93
+ return value.to_h { |key, item| [key.dup, unwrap(item)] } if value.is_a?(Hash)
94
+ return value.map { |item| unwrap(item) } if value.is_a?(Array)
95
+
96
+ value
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module LetMeSendEmail
6
+ module Resources
7
+ # Shared resource URL and pagination behavior.
8
+ class Base
9
+ # @param client [Client]
10
+ def initialize(client)
11
+ @client = client
12
+ end
13
+
14
+ private
15
+
16
+ def path_for(collection, id)
17
+ "/#{collection}/#{@client.resource_path_segment(id)}"
18
+ end
19
+
20
+ def list_path(collection, per_page:, after:, before:)
21
+ raise ArgumentError, 'after and before cannot be used together' if after && before
22
+
23
+ params = {}
24
+ params[:per_page] = per_page unless per_page.nil?
25
+ params[:after] = after unless after.nil?
26
+ params[:before] = before unless before.nil?
27
+ params.empty? ? "/#{collection}" : "/#{collection}?#{URI.encode_www_form(params)}"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LetMeSendEmail
4
+ module Resources
5
+ # Contact Categories API operations.
6
+ class ContactCategories < Base
7
+ # Creates a contact category.
8
+ # @return [Models::Model]
9
+ def create(name:, slug: nil)
10
+ body = { name: name }
11
+ body[:slug] = slug if slug
12
+ @client.request(:post, '/contact-categories', body: body)
13
+ end
14
+
15
+ # Lists one cursor page of contact categories.
16
+ # @return [Models::Model]
17
+ def list(per_page: nil, after: nil, before: nil)
18
+ @client.request(:get, list_path('contact-categories', per_page: per_page, after: after, before: before))
19
+ end
20
+
21
+ # Retrieves a contact category.
22
+ # @param id [String]
23
+ # @return [Models::Model]
24
+ def get(id)
25
+ @client.request(:get, path_for('contact-categories', id))
26
+ end
27
+
28
+ # Updates a contact category.
29
+ # @param id [String]
30
+ # @return [Models::Model]
31
+ def update(id, name:, slug: nil)
32
+ body = { name: name }
33
+ body[:slug] = slug if slug
34
+ @client.request(:put, path_for('contact-categories', id), body: body)
35
+ end
36
+
37
+ # Deletes a contact category.
38
+ # @param id [String]
39
+ # @return [Models::Model]
40
+ def delete(id)
41
+ @client.request(:delete, path_for('contact-categories', id))
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LetMeSendEmail
4
+ module Resources
5
+ # Contacts API operations.
6
+ class Contacts < Base
7
+ # Creates a contact.
8
+ # @return [Models::Model]
9
+ def create(email:, first_name: nil, last_name: nil, phone: nil,
10
+ is_globally_unsubscribed: nil, categories: nil, email_topics: nil)
11
+ body = { email: email }
12
+ body[:first_name] = first_name if first_name
13
+ body[:last_name] = last_name if last_name
14
+ body[:phone] = phone if phone
15
+ body[:is_globally_unsubscribed] = is_globally_unsubscribed unless is_globally_unsubscribed.nil?
16
+ body[:categories] = categories if categories
17
+ body[:email_topics] = email_topics if email_topics
18
+ @client.request(:post, '/contacts', body: body)
19
+ end
20
+
21
+ # Lists one cursor page of contacts.
22
+ # @return [Models::Model]
23
+ def list(per_page: nil, after: nil, before: nil)
24
+ @client.request(:get, list_path('contacts', per_page: per_page, after: after, before: before))
25
+ end
26
+
27
+ # Retrieves a contact.
28
+ # @param id [String]
29
+ # @return [Models::Model]
30
+ def get(id)
31
+ @client.request(:get, path_for('contacts', id))
32
+ end
33
+
34
+ # Updates a contact.
35
+ # @param id [String]
36
+ # @return [Models::Model]
37
+ def update(id, first_name: nil, last_name: nil, phone: nil,
38
+ is_globally_unsubscribed: nil, categories: nil, email_topics: nil,
39
+ sync_categories: nil, sync_email_topics: nil)
40
+ body = {}
41
+ body[:first_name] = first_name if first_name
42
+ body[:last_name] = last_name if last_name
43
+ body[:phone] = phone if phone
44
+ body[:is_globally_unsubscribed] = is_globally_unsubscribed unless is_globally_unsubscribed.nil?
45
+ body[:categories] = categories if categories
46
+ body[:email_topics] = email_topics if email_topics
47
+ body[:sync_categories] = sync_categories unless sync_categories.nil?
48
+ body[:sync_email_topics] = sync_email_topics unless sync_email_topics.nil?
49
+ @client.request(:put, path_for('contacts', id), body: body)
50
+ end
51
+
52
+ # Deletes a contact.
53
+ # @param id [String]
54
+ # @return [Models::Model]
55
+ def delete(id)
56
+ @client.request(:delete, path_for('contacts', id))
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LetMeSendEmail
4
+ module Resources
5
+ # Domains API operations.
6
+ class Domains < Base
7
+ # Lists one cursor page of domains.
8
+ # @return [Models::Model]
9
+ def list(per_page: nil, after: nil, before: nil)
10
+ @client.request(:get, list_path('domains', per_page: per_page, after: after, before: before))
11
+ end
12
+
13
+ # Retrieves a domain.
14
+ # @param id [String]
15
+ # @return [Models::Model]
16
+ def get(id)
17
+ @client.request(:get, path_for('domains', id))
18
+ end
19
+
20
+ # Requests verification for a domain name.
21
+ # @param domain [String]
22
+ # @return [Models::Model]
23
+ def verify(domain)
24
+ @client.request(:post, '/domains/verify', body: { domain: domain })
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LetMeSendEmail
4
+ module Resources
5
+ # Email Topics API operations.
6
+ class EmailTopics < Base
7
+ # Creates an email topic.
8
+ # @return [Models::Model]
9
+ def create(name:, slug:, auto_subscribe: nil, public: nil,
10
+ description: nil, domain_id: nil)
11
+ body = { name: name, slug: slug }
12
+ body[:auto_subscribe] = auto_subscribe unless auto_subscribe.nil?
13
+ body[:public] = public unless public.nil?
14
+ body[:description] = description if description
15
+ body[:domain] = { id: domain_id } if domain_id
16
+ @client.request(:post, '/email-topics', body: body)
17
+ end
18
+
19
+ # Lists one cursor page of email topics.
20
+ # @return [Models::Model]
21
+ def list(per_page: nil, after: nil, before: nil)
22
+ @client.request(:get, list_path('email-topics', per_page: per_page, after: after, before: before))
23
+ end
24
+
25
+ # Retrieves an email topic.
26
+ # @param id [String]
27
+ # @return [Models::Model]
28
+ def get(id)
29
+ @client.request(:get, path_for('email-topics', id))
30
+ end
31
+
32
+ # Updates an email topic.
33
+ # @param id [String]
34
+ # @return [Models::Model]
35
+ def update(id, name: nil, slug: nil, description: nil, public: nil, auto_subscribe: nil)
36
+ body = {}
37
+ body[:name] = name if name
38
+ body[:slug] = slug if slug
39
+ body[:description] = description if description
40
+ body[:public] = public unless public.nil?
41
+ body[:auto_subscribe] = auto_subscribe unless auto_subscribe.nil?
42
+ @client.request(:put, path_for('email-topics', id), body: body)
43
+ end
44
+
45
+ # Deletes an email topic.
46
+ # @param id [String]
47
+ # @return [Models::Model]
48
+ def delete(id)
49
+ @client.request(:delete, path_for('email-topics', id))
50
+ end
51
+ end
52
+ end
53
+ end