lettermint 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/.gemini/settings.json +7 -0
- data/CHANGELOG.md +21 -0
- data/lib/lettermint/client.rb +3 -29
- data/lib/lettermint/email_message.rb +11 -2
- data/lib/lettermint/errors.rb +9 -0
- data/lib/lettermint/http_client.rb +11 -3
- data/lib/lettermint/resources/base.rb +33 -0
- data/lib/lettermint/resources/domains.rb +66 -0
- data/lib/lettermint/resources/messages.rb +81 -0
- data/lib/lettermint/resources/projects.rb +103 -0
- data/lib/lettermint/resources/routes.rb +93 -0
- data/lib/lettermint/resources/stats.rb +23 -0
- data/lib/lettermint/resources/suppressions.rb +59 -0
- data/lib/lettermint/resources/team.rb +38 -0
- data/lib/lettermint/resources/webhooks.rb +124 -0
- data/lib/lettermint/sending_api.rb +75 -0
- data/lib/lettermint/team_api.rb +90 -0
- data/lib/lettermint/version.rb +1 -1
- data/lib/lettermint.rb +15 -0
- metadata +13 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4755559f2d03919f11cb88181b581062497106993656e9d1b0a87738769718e5
|
|
4
|
+
data.tar.gz: c5b8df6ea0033b41290f4db33aff06854a1ff02404af2391643d6fc736c38167
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a7f78e00a6044e15e2e57efaa365d798a922e1d96fb58d418ba4279cb03785c48c05537e5c0921b177d75d4648c14b8ee0dcf81390dabe9402f21f9485184f5b
|
|
7
|
+
data.tar.gz: b254a766abe3503cbfb5fad105d9acf112e4d9ed2cb1c642caf42289a90f037ecdd3d2768b2148ee2f60590b5e3acff232e7fabdc409d249afb73e3f41120b08
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.0] - 2026-04-01
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `TeamAPI` for managing team resources (members, invitations)
|
|
13
|
+
- `SendingAPI` for domain and sender identity management
|
|
14
|
+
- Resource class infrastructure (`BaseResource`, `Resource`)
|
|
15
|
+
- Raw HTTP methods (`Client#get`, `#post`, `#put`, `#delete`) for arbitrary endpoint access
|
|
16
|
+
- `ConnectionError` for network-level failures
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- HTTP response body validation before JSON parsing
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- TypeError leak in webhook header parsing
|
|
25
|
+
- Blank recipient validation
|
|
26
|
+
|
|
27
|
+
[0.2.0]: https://github.com/onetimesecret/lettermint-ruby/compare/v0.1.0...v0.2.0
|
|
28
|
+
|
|
8
29
|
## [0.1.0] - 2026-02-18
|
|
9
30
|
|
|
10
31
|
### Added
|
data/lib/lettermint/client.rb
CHANGED
|
@@ -1,33 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Lettermint
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def initialize(api_token:, base_url: nil, timeout: nil)
|
|
8
|
-
validate_api_token!(api_token)
|
|
9
|
-
|
|
10
|
-
@configuration = Configuration.new
|
|
11
|
-
@configuration.base_url = base_url || Lettermint.configuration.base_url
|
|
12
|
-
@configuration.timeout = timeout || Lettermint.configuration.timeout
|
|
13
|
-
|
|
14
|
-
yield @configuration if block_given?
|
|
15
|
-
|
|
16
|
-
@http_client = HttpClient.new(
|
|
17
|
-
api_token: api_token,
|
|
18
|
-
base_url: @configuration.base_url,
|
|
19
|
-
timeout: @configuration.timeout
|
|
20
|
-
)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def email
|
|
24
|
-
EmailMessage.new(http_client: @http_client)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
private
|
|
28
|
-
|
|
29
|
-
def validate_api_token!(token)
|
|
30
|
-
raise ArgumentError, 'API token cannot be empty' if token.nil? || token.to_s.strip.empty?
|
|
31
|
-
end
|
|
32
|
-
end
|
|
4
|
+
# Backward compatibility: Client is an alias for SendingAPI.
|
|
5
|
+
# Use Lettermint::SendingAPI explicitly for clarity.
|
|
6
|
+
Client = SendingAPI
|
|
33
7
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Lettermint
|
|
4
|
-
class EmailMessage
|
|
4
|
+
class EmailMessage # rubocop:disable Metrics/ClassLength
|
|
5
5
|
def initialize(http_client:)
|
|
6
6
|
@http_client = http_client
|
|
7
7
|
reset
|
|
@@ -108,7 +108,8 @@ module Lettermint
|
|
|
108
108
|
|
|
109
109
|
def validate_required_fields
|
|
110
110
|
missing = %i[from subject].select { |f| blank?(f) }
|
|
111
|
-
missing << :to
|
|
111
|
+
missing << :to unless valid_recipients?
|
|
112
|
+
missing << :body unless body?
|
|
112
113
|
return if missing.empty?
|
|
113
114
|
|
|
114
115
|
raise ArgumentError, "Missing required field(s): #{missing.join(', ')}"
|
|
@@ -118,6 +119,14 @@ module Lettermint
|
|
|
118
119
|
@payload[field].nil? || @payload[field].to_s.strip.empty?
|
|
119
120
|
end
|
|
120
121
|
|
|
122
|
+
def valid_recipients?
|
|
123
|
+
@payload[:to]&.any? { |e| e.is_a?(String) && !e.strip.empty? }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def body?
|
|
127
|
+
!blank?(:html) || !blank?(:text)
|
|
128
|
+
end
|
|
129
|
+
|
|
121
130
|
def reset
|
|
122
131
|
@payload = {}
|
|
123
132
|
@idempotency_key = nil
|
data/lib/lettermint/errors.rb
CHANGED
|
@@ -45,6 +45,15 @@ module Lettermint
|
|
|
45
45
|
|
|
46
46
|
class TimeoutError < Error; end
|
|
47
47
|
|
|
48
|
+
class ConnectionError < Error
|
|
49
|
+
attr_reader :original_exception
|
|
50
|
+
|
|
51
|
+
def initialize(message:, original_exception: nil)
|
|
52
|
+
@original_exception = original_exception
|
|
53
|
+
super(message)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
48
57
|
class WebhookVerificationError < Error; end
|
|
49
58
|
|
|
50
59
|
class InvalidSignatureError < WebhookVerificationError; end
|
|
@@ -4,7 +4,7 @@ require 'faraday'
|
|
|
4
4
|
|
|
5
5
|
module Lettermint
|
|
6
6
|
class HttpClient
|
|
7
|
-
def initialize(api_token:, base_url:, timeout:)
|
|
7
|
+
def initialize(api_token:, base_url:, timeout:, auth_scheme: :project)
|
|
8
8
|
normalized_url = "#{base_url.chomp('/')}/"
|
|
9
9
|
@connection = Faraday.new(url: normalized_url) do |f|
|
|
10
10
|
f.request :json
|
|
@@ -14,7 +14,7 @@ module Lettermint
|
|
|
14
14
|
f.headers = {
|
|
15
15
|
'Content-Type' => 'application/json',
|
|
16
16
|
'Accept' => 'application/json',
|
|
17
|
-
|
|
17
|
+
**auth_headers(api_token, auth_scheme),
|
|
18
18
|
'User-Agent' => "Lettermint/#{Lettermint::VERSION} (Ruby; ruby #{RUBY_VERSION})"
|
|
19
19
|
}
|
|
20
20
|
end
|
|
@@ -57,13 +57,21 @@ module Lettermint
|
|
|
57
57
|
|
|
58
58
|
private
|
|
59
59
|
|
|
60
|
+
def auth_headers(token, scheme)
|
|
61
|
+
case scheme.to_sym
|
|
62
|
+
when :project then { 'x-lettermint-token' => token }
|
|
63
|
+
when :team then { 'Authorization' => "Bearer #{token}" }
|
|
64
|
+
else raise ArgumentError, "Unknown auth_scheme: #{scheme}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
60
68
|
def with_error_handling
|
|
61
69
|
response = yield
|
|
62
70
|
handle_response(response)
|
|
63
71
|
rescue Faraday::TimeoutError, Timeout::Error
|
|
64
72
|
raise Lettermint::TimeoutError, "Request timeout after #{@connection.options.timeout}s"
|
|
65
73
|
rescue Faraday::ConnectionFailed => e
|
|
66
|
-
raise Lettermint::
|
|
74
|
+
raise Lettermint::ConnectionError.new(message: e.message, original_exception: e)
|
|
67
75
|
end
|
|
68
76
|
|
|
69
77
|
def handle_response(response)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lettermint
|
|
4
|
+
module Resources
|
|
5
|
+
# Base class for Team API resources providing shared functionality.
|
|
6
|
+
class Base
|
|
7
|
+
def initialize(http_client:)
|
|
8
|
+
@http_client = http_client
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# Builds query parameters for list endpoints.
|
|
14
|
+
# @param page_size [Integer, nil] Number of items per page
|
|
15
|
+
# @param page_cursor [String, nil] Cursor for pagination
|
|
16
|
+
# @param sort [String, nil] Sort field (prefix with - for descending)
|
|
17
|
+
# @param include [String, nil] Related resources to include
|
|
18
|
+
# @param filters [Hash] Filter parameters (converted to filter[key]=value)
|
|
19
|
+
# @return [Hash, nil] Query parameters hash or nil if empty
|
|
20
|
+
def build_params(page_size: nil, page_cursor: nil, sort: nil, include: nil, **filters)
|
|
21
|
+
params = {
|
|
22
|
+
'page[size]' => page_size,
|
|
23
|
+
'page[cursor]' => page_cursor,
|
|
24
|
+
'sort' => sort,
|
|
25
|
+
'include' => include
|
|
26
|
+
}.compact
|
|
27
|
+
|
|
28
|
+
filters.each { |k, v| params["filter[#{k}]"] = v unless v.nil? }
|
|
29
|
+
params.empty? ? nil : params
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lettermint
|
|
4
|
+
module Resources
|
|
5
|
+
# Domains resource for managing sending domains and DNS verification.
|
|
6
|
+
class Domains < Base
|
|
7
|
+
# List all domains.
|
|
8
|
+
# @param page_size [Integer, nil] Number of items per page (default: 30)
|
|
9
|
+
# @param page_cursor [String, nil] Cursor for pagination
|
|
10
|
+
# @param sort [String, nil] Sort field: domain, created_at, status_changed_at (prefix - for desc)
|
|
11
|
+
# @param status [String, nil] Filter by status (verified, partially_verified, etc.)
|
|
12
|
+
# @param domain [String, nil] Filter by domain (partial match)
|
|
13
|
+
# @return [Hash] Paginated list of domains
|
|
14
|
+
def list(page_size: nil, page_cursor: nil, sort: nil, status: nil, domain: nil)
|
|
15
|
+
params = build_params(page_size:, page_cursor:, sort:, status:, domain:)
|
|
16
|
+
@http_client.get(path: '/domains', params: params)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Create a new domain.
|
|
20
|
+
# @param domain [String] Domain name (max 255 chars)
|
|
21
|
+
# @return [Hash] Created domain data
|
|
22
|
+
def create(domain:)
|
|
23
|
+
@http_client.post(path: '/domains', data: { domain: domain })
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Get domain details.
|
|
27
|
+
# @param id [String] Domain ID
|
|
28
|
+
# @param include [String, nil] Related data to include (dnsRecords, dnsRecordsCount, dnsRecordsExists)
|
|
29
|
+
# @return [Hash] Domain data with optional includes
|
|
30
|
+
def find(id, include: nil)
|
|
31
|
+
params = build_params(include: include)
|
|
32
|
+
@http_client.get(path: "/domains/#{id}", params: params)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Delete a domain.
|
|
36
|
+
# @param id [String] Domain ID
|
|
37
|
+
# @return [Hash] Confirmation message
|
|
38
|
+
def delete(id)
|
|
39
|
+
@http_client.delete(path: "/domains/#{id}")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Verify all DNS records for a domain.
|
|
43
|
+
# @param id [String] Domain ID
|
|
44
|
+
# @return [Hash] Verification result
|
|
45
|
+
def verify_dns(id)
|
|
46
|
+
@http_client.post(path: "/domains/#{id}/dns-records/verify")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Verify a specific DNS record.
|
|
50
|
+
# @param domain_id [String] Domain ID
|
|
51
|
+
# @param record_id [String] DNS record ID
|
|
52
|
+
# @return [Hash] Verification result
|
|
53
|
+
def verify_dns_record(domain_id, record_id)
|
|
54
|
+
@http_client.post(path: "/domains/#{domain_id}/dns-records/#{record_id}/verify")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Update projects associated with a domain.
|
|
58
|
+
# @param id [String] Domain ID
|
|
59
|
+
# @param project_ids [Array<String>] Array of project UUIDs
|
|
60
|
+
# @return [Hash] Updated domain data
|
|
61
|
+
def update_projects(id, project_ids:)
|
|
62
|
+
@http_client.put(path: "/domains/#{id}/projects", data: { project_ids: project_ids })
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lettermint
|
|
4
|
+
module Resources
|
|
5
|
+
# Messages resource for viewing sent and received messages.
|
|
6
|
+
class Messages < Base
|
|
7
|
+
# List messages.
|
|
8
|
+
# @param page_size [Integer, nil] Number of items per page (default: 30)
|
|
9
|
+
# @param page_cursor [String, nil] Cursor for pagination
|
|
10
|
+
# @param sort [String, nil] Sort field: type, status, from_email, subject, created_at, status_changed_at
|
|
11
|
+
# @param type [String, nil] Filter: inbound, outbound
|
|
12
|
+
# @param status [String, nil] Filter by status
|
|
13
|
+
# @param route_id [String, nil] Filter by route ID
|
|
14
|
+
# @param domain_id [String, nil] Filter by domain ID
|
|
15
|
+
# @param tag [String, nil] Filter by tag
|
|
16
|
+
# @param from_email [String, nil] Filter by sender email
|
|
17
|
+
# @param subject [String, nil] Filter by subject
|
|
18
|
+
# @param from_date [String, nil] Filter from date (Y-m-d)
|
|
19
|
+
# @param to_date [String, nil] Filter to date (Y-m-d)
|
|
20
|
+
# @return [Hash] Paginated list of messages
|
|
21
|
+
# rubocop:disable Metrics/ParameterLists
|
|
22
|
+
def list(page_size: nil, page_cursor: nil, sort: nil, type: nil, status: nil,
|
|
23
|
+
route_id: nil, domain_id: nil, tag: nil, from_email: nil, subject: nil,
|
|
24
|
+
from_date: nil, to_date: nil)
|
|
25
|
+
params = build_params(
|
|
26
|
+
page_size: page_size,
|
|
27
|
+
page_cursor: page_cursor,
|
|
28
|
+
sort: sort,
|
|
29
|
+
type: type,
|
|
30
|
+
status: status,
|
|
31
|
+
route_id: route_id,
|
|
32
|
+
domain_id: domain_id,
|
|
33
|
+
tag: tag,
|
|
34
|
+
from_email: from_email,
|
|
35
|
+
subject: subject,
|
|
36
|
+
from_date: from_date,
|
|
37
|
+
to_date: to_date
|
|
38
|
+
)
|
|
39
|
+
@http_client.get(path: '/messages', params: params)
|
|
40
|
+
end
|
|
41
|
+
# rubocop:enable Metrics/ParameterLists
|
|
42
|
+
|
|
43
|
+
# Get message details.
|
|
44
|
+
# @param id [String] Message ID
|
|
45
|
+
# @return [Hash] Message data
|
|
46
|
+
def find(id)
|
|
47
|
+
@http_client.get(path: "/messages/#{id}")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Get message events (delivery history).
|
|
51
|
+
# @param id [String] Message ID
|
|
52
|
+
# @param sort [String, nil] Sort field: timestamp, event
|
|
53
|
+
# @return [Hash] List of message events
|
|
54
|
+
def events(id, sort: nil)
|
|
55
|
+
params = sort ? { 'sort' => sort } : nil
|
|
56
|
+
@http_client.get(path: "/messages/#{id}/events", params: params)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Get raw message source (RFC822 format).
|
|
60
|
+
# @param id [String] Message ID
|
|
61
|
+
# @return [String] Raw message source (message/rfc822)
|
|
62
|
+
def source(id)
|
|
63
|
+
@http_client.get(path: "/messages/#{id}/source", headers: { 'Accept' => 'message/rfc822' })
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Get message HTML body.
|
|
67
|
+
# @param id [String] Message ID
|
|
68
|
+
# @return [String] HTML content (text/html)
|
|
69
|
+
def html(id)
|
|
70
|
+
@http_client.get(path: "/messages/#{id}/html", headers: { 'Accept' => 'text/html' })
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Get message plain text body.
|
|
74
|
+
# @param id [String] Message ID
|
|
75
|
+
# @return [String] Plain text content (text/plain)
|
|
76
|
+
def text(id)
|
|
77
|
+
@http_client.get(path: "/messages/#{id}/text", headers: { 'Accept' => 'text/plain' })
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lettermint
|
|
4
|
+
module Resources
|
|
5
|
+
# Projects resource for managing projects, members, and accessing routes.
|
|
6
|
+
class Projects < Base
|
|
7
|
+
# List all projects.
|
|
8
|
+
# @param page_size [Integer, nil] Number of items per page (default: 30)
|
|
9
|
+
# @param page_cursor [String, nil] Cursor for pagination
|
|
10
|
+
# @param sort [String, nil] Sort field: name, created_at (prefix - for desc)
|
|
11
|
+
# @param search [String, nil] Search filter
|
|
12
|
+
# @return [Hash] Paginated list of projects
|
|
13
|
+
def list(page_size: nil, page_cursor: nil, sort: nil, search: nil)
|
|
14
|
+
params = build_params(page_size: page_size, page_cursor: page_cursor, sort: sort, search: search)
|
|
15
|
+
@http_client.get(path: '/projects', params: params)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Create a new project.
|
|
19
|
+
# @param name [String] Project name (max 255 chars)
|
|
20
|
+
# @param smtp_enabled [Boolean, nil] Enable SMTP (default: false)
|
|
21
|
+
# @param initial_routes [String, nil] Initial routes: both, transactional, broadcast (default: both)
|
|
22
|
+
# @return [Hash] Created project data including api_token
|
|
23
|
+
def create(name:, smtp_enabled: nil, initial_routes: nil)
|
|
24
|
+
data = { name: name }
|
|
25
|
+
data[:smtp_enabled] = smtp_enabled unless smtp_enabled.nil?
|
|
26
|
+
data[:initial_routes] = initial_routes if initial_routes
|
|
27
|
+
@http_client.post(path: '/projects', data: data)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Get project details.
|
|
31
|
+
#
|
|
32
|
+
# Note: Returns a Hash, not a resource object. To access routes for a project,
|
|
33
|
+
# use `projects.routes(project_id)` rather than chaining on the find result.
|
|
34
|
+
#
|
|
35
|
+
# @param id [String] Project ID
|
|
36
|
+
# @param include [String, nil] Related data: routes, domains, teamMembers, messageStats (+ Count/Exists variants)
|
|
37
|
+
# @return [Hash] Project data with optional includes
|
|
38
|
+
def find(id, include: nil)
|
|
39
|
+
params = include ? { 'include' => include } : nil
|
|
40
|
+
@http_client.get(path: "/projects/#{id}", params: params)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Update a project.
|
|
44
|
+
# @param id [String] Project ID
|
|
45
|
+
# @param name [String, nil] New project name
|
|
46
|
+
# @param smtp_enabled [Boolean, nil] Enable/disable SMTP
|
|
47
|
+
# @param default_route_id [String, nil] Default route UUID
|
|
48
|
+
# @return [Hash] Updated project data
|
|
49
|
+
def update(id, name: nil, smtp_enabled: nil, default_route_id: nil)
|
|
50
|
+
data = {}
|
|
51
|
+
data[:name] = name if name
|
|
52
|
+
data[:smtp_enabled] = smtp_enabled unless smtp_enabled.nil?
|
|
53
|
+
data[:default_route_id] = default_route_id if default_route_id
|
|
54
|
+
@http_client.put(path: "/projects/#{id}", data: data)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Delete a project.
|
|
58
|
+
# @param id [String] Project ID
|
|
59
|
+
# @return [Hash] Confirmation message
|
|
60
|
+
def delete(id)
|
|
61
|
+
@http_client.delete(path: "/projects/#{id}")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Rotate the project API token.
|
|
65
|
+
# @param id [String] Project ID
|
|
66
|
+
# @return [Hash] Contains new_token
|
|
67
|
+
def rotate_token(id)
|
|
68
|
+
@http_client.post(path: "/projects/#{id}/rotate-token")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Update project members (replace all).
|
|
72
|
+
# @param id [String] Project ID
|
|
73
|
+
# @param team_member_ids [Array<String>] Array of team member IDs
|
|
74
|
+
# @return [Hash] Confirmation
|
|
75
|
+
def update_members(id, team_member_ids:)
|
|
76
|
+
@http_client.put(path: "/projects/#{id}/members", data: { team_member_ids: team_member_ids })
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Add a member to the project.
|
|
80
|
+
# @param project_id [String] Project ID
|
|
81
|
+
# @param member_id [String] Team member ID
|
|
82
|
+
# @return [Hash] Confirmation
|
|
83
|
+
def add_member(project_id, member_id)
|
|
84
|
+
@http_client.post(path: "/projects/#{project_id}/members/#{member_id}")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Remove a member from the project.
|
|
88
|
+
# @param project_id [String] Project ID
|
|
89
|
+
# @param member_id [String] Team member ID
|
|
90
|
+
# @return [Hash] Confirmation
|
|
91
|
+
def remove_member(project_id, member_id)
|
|
92
|
+
@http_client.delete(path: "/projects/#{project_id}/members/#{member_id}")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Get a routes accessor scoped to this project.
|
|
96
|
+
# @param project_id [String] Project ID
|
|
97
|
+
# @return [Routes] Routes resource scoped to the project
|
|
98
|
+
def routes(project_id)
|
|
99
|
+
Routes.new(http_client: @http_client, project_id: project_id)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lettermint
|
|
4
|
+
module Resources
|
|
5
|
+
# Routes resource for managing project routes (transactional, broadcast, inbound).
|
|
6
|
+
# Can be instantiated with a project_id for scoped operations or without for direct route access.
|
|
7
|
+
class Routes < Base
|
|
8
|
+
# @param http_client [HttpClient] HTTP client instance
|
|
9
|
+
# @param project_id [String, nil] Optional project ID for scoped operations
|
|
10
|
+
def initialize(http_client:, project_id: nil)
|
|
11
|
+
super(http_client: http_client)
|
|
12
|
+
@project_id = project_id
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# List routes for a project.
|
|
16
|
+
# Requires project_id to be set (via constructor or projects.routes(id)).
|
|
17
|
+
# @param page_size [Integer, nil] Number of items per page (default: 30)
|
|
18
|
+
# @param page_cursor [String, nil] Cursor for pagination
|
|
19
|
+
# @param sort [String, nil] Sort field: name, slug, created_at (prefix - for desc)
|
|
20
|
+
# @param route_type [String, nil] Filter: transactional, broadcast, inbound
|
|
21
|
+
# @param is_default [Boolean, nil] Filter by default route status
|
|
22
|
+
# @param search [String, nil] Search filter
|
|
23
|
+
# @return [Hash] Paginated list of routes
|
|
24
|
+
# rubocop:disable Metrics/ParameterLists
|
|
25
|
+
def list(page_size: nil, page_cursor: nil, sort: nil, route_type: nil,
|
|
26
|
+
is_default: nil, search: nil)
|
|
27
|
+
raise ArgumentError, 'project_id required for listing routes' unless @project_id
|
|
28
|
+
|
|
29
|
+
params = build_params(
|
|
30
|
+
page_size: page_size,
|
|
31
|
+
page_cursor: page_cursor,
|
|
32
|
+
sort: sort,
|
|
33
|
+
route_type: route_type,
|
|
34
|
+
is_default: is_default,
|
|
35
|
+
search: search
|
|
36
|
+
)
|
|
37
|
+
@http_client.get(path: "/projects/#{@project_id}/routes", params: params)
|
|
38
|
+
end
|
|
39
|
+
# rubocop:enable Metrics/ParameterLists
|
|
40
|
+
|
|
41
|
+
# Create a new route in a project.
|
|
42
|
+
# Requires project_id to be set.
|
|
43
|
+
# @param name [String] Route name (max 255 chars)
|
|
44
|
+
# @param route_type [String] Type: transactional, broadcast, inbound
|
|
45
|
+
# @param slug [String, nil] Optional slug (max 255 chars)
|
|
46
|
+
# @return [Hash] Created route data
|
|
47
|
+
def create(name:, route_type:, slug: nil)
|
|
48
|
+
raise ArgumentError, 'project_id required for creating routes' unless @project_id
|
|
49
|
+
|
|
50
|
+
data = { name: name, route_type: route_type }
|
|
51
|
+
data[:slug] = slug if slug
|
|
52
|
+
@http_client.post(path: "/projects/#{@project_id}/routes", data: data)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Get route details.
|
|
56
|
+
# @param id [String] Route ID
|
|
57
|
+
# @param include [String, nil] Related data: project, statistics
|
|
58
|
+
# @return [Hash] Route data
|
|
59
|
+
def find(id, include: nil)
|
|
60
|
+
params = include ? { 'include' => include } : nil
|
|
61
|
+
@http_client.get(path: "/routes/#{id}", params: params)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Update a route.
|
|
65
|
+
# @param id [String] Route ID
|
|
66
|
+
# @param name [String, nil] New route name
|
|
67
|
+
# @param settings [Hash, nil] Route settings (track_opens, track_clicks, disable_hosted_unsubscribe)
|
|
68
|
+
# @param inbound_settings [Hash, nil] Inbound settings (inbound_domain, spam_threshold, etc.)
|
|
69
|
+
# @return [Hash] Updated route data
|
|
70
|
+
def update(id, name: nil, settings: nil, inbound_settings: nil)
|
|
71
|
+
data = {}
|
|
72
|
+
data[:name] = name if name
|
|
73
|
+
data[:settings] = settings if settings
|
|
74
|
+
data[:inbound_settings] = inbound_settings if inbound_settings
|
|
75
|
+
@http_client.put(path: "/routes/#{id}", data: data)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Delete a route.
|
|
79
|
+
# @param id [String] Route ID
|
|
80
|
+
# @return [Hash] Confirmation message
|
|
81
|
+
def delete(id)
|
|
82
|
+
@http_client.delete(path: "/routes/#{id}")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Verify inbound domain for a route.
|
|
86
|
+
# @param id [String] Route ID
|
|
87
|
+
# @return [Hash] Verification result
|
|
88
|
+
def verify_inbound_domain(id)
|
|
89
|
+
@http_client.post(path: "/routes/#{id}/verify-inbound-domain")
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lettermint
|
|
4
|
+
module Resources
|
|
5
|
+
# Stats resource for retrieving email statistics.
|
|
6
|
+
class Stats < Base
|
|
7
|
+
# Get statistics for a date range.
|
|
8
|
+
# @param from [String] Start date (Y-m-d format, required)
|
|
9
|
+
# @param to [String] End date (Y-m-d format, required, max 90 days from start)
|
|
10
|
+
# @param project_id [String, nil] Filter by project ID
|
|
11
|
+
# @param route_id [String, nil] Filter by a single route ID
|
|
12
|
+
# @param route_ids [Array<String>, String, nil] Filter by multiple route IDs
|
|
13
|
+
# @return [Hash] Stats data with totals and daily breakdown
|
|
14
|
+
def get(from:, to:, project_id: nil, route_id: nil, route_ids: nil)
|
|
15
|
+
params = { 'from' => from, 'to' => to }
|
|
16
|
+
params['project_id'] = project_id if project_id
|
|
17
|
+
params['route_id'] = route_id if route_id
|
|
18
|
+
params['route_ids'] = route_ids if route_ids
|
|
19
|
+
@http_client.get(path: '/stats', params: params)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lettermint
|
|
4
|
+
module Resources
|
|
5
|
+
# Suppressions resource for managing email suppression lists.
|
|
6
|
+
class Suppressions < Base
|
|
7
|
+
# List suppressions.
|
|
8
|
+
# @param page_size [Integer, nil] Number of items per page (default: 30)
|
|
9
|
+
# @param page_cursor [String, nil] Cursor for pagination
|
|
10
|
+
# @param sort [String, nil] Sort field: value, created_at, reason
|
|
11
|
+
# @param scope [String, nil] Filter: team, project, route
|
|
12
|
+
# @param route_id [String, nil] Filter by route ID
|
|
13
|
+
# @param project_id [String, nil] Filter by project ID
|
|
14
|
+
# @param value [String, nil] Filter by suppression value (email/domain/extension)
|
|
15
|
+
# @param reason [String, nil] Filter: spam_complaint, hard_bounce, unsubscribe, manual
|
|
16
|
+
# @return [Hash] Paginated list of suppressions
|
|
17
|
+
# rubocop:disable Metrics/ParameterLists
|
|
18
|
+
def list(page_size: nil, page_cursor: nil, sort: nil, scope: nil,
|
|
19
|
+
route_id: nil, project_id: nil, value: nil, reason: nil)
|
|
20
|
+
params = build_params(
|
|
21
|
+
page_size: page_size,
|
|
22
|
+
page_cursor: page_cursor,
|
|
23
|
+
sort: sort,
|
|
24
|
+
scope: scope,
|
|
25
|
+
route_id: route_id,
|
|
26
|
+
project_id: project_id,
|
|
27
|
+
value: value,
|
|
28
|
+
reason: reason
|
|
29
|
+
)
|
|
30
|
+
@http_client.get(path: '/suppressions', params: params)
|
|
31
|
+
end
|
|
32
|
+
# rubocop:enable Metrics/ParameterLists
|
|
33
|
+
|
|
34
|
+
# Create a suppression entry.
|
|
35
|
+
# @param reason [String] Reason: spam_complaint, hard_bounce, unsubscribe, manual
|
|
36
|
+
# @param scope [String] Scope: team, project, route
|
|
37
|
+
# @param email [String, nil] Single email to suppress (max 255 chars)
|
|
38
|
+
# @param emails [Array<String>, nil] Multiple emails to suppress (max 1000)
|
|
39
|
+
# @param route_id [String, nil] Route ID (required if scope is route)
|
|
40
|
+
# @param project_id [String, nil] Project ID (required if scope is project)
|
|
41
|
+
# @return [Hash] Created suppression data
|
|
42
|
+
def create(reason:, scope:, email: nil, emails: nil, route_id: nil, project_id: nil) # rubocop:disable Metrics/ParameterLists
|
|
43
|
+
data = { reason: reason, scope: scope }
|
|
44
|
+
data[:email] = email if email
|
|
45
|
+
data[:emails] = emails if emails
|
|
46
|
+
data[:route_id] = route_id if route_id
|
|
47
|
+
data[:project_id] = project_id if project_id
|
|
48
|
+
@http_client.post(path: '/suppressions', data: data)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Delete a suppression entry.
|
|
52
|
+
# @param id [String] Suppression ID
|
|
53
|
+
# @return [Hash] Confirmation message
|
|
54
|
+
def delete(id)
|
|
55
|
+
@http_client.delete(path: "/suppressions/#{id}")
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lettermint
|
|
4
|
+
module Resources
|
|
5
|
+
# Team resource for managing team settings, usage, and members.
|
|
6
|
+
class Team < Base
|
|
7
|
+
# Get team details.
|
|
8
|
+
# @param include [String, nil] Related data to include (features, featuresCount, featuresExists)
|
|
9
|
+
# @return [Hash] Team data
|
|
10
|
+
def get(include: nil)
|
|
11
|
+
params = include ? { 'include' => include } : nil
|
|
12
|
+
@http_client.get(path: '/team', params: params)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Update team settings.
|
|
16
|
+
# @param name [String] New team name (max 255 chars)
|
|
17
|
+
# @return [Hash] Updated team data
|
|
18
|
+
def update(name:)
|
|
19
|
+
@http_client.put(path: '/team', data: { name: name })
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Get team usage statistics.
|
|
23
|
+
# @return [Hash] Current period and up to 12 historical periods
|
|
24
|
+
def usage
|
|
25
|
+
@http_client.get(path: '/team/usage')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# List team members.
|
|
29
|
+
# @param page_size [Integer, nil] Number of items per page (default: 30)
|
|
30
|
+
# @param page_cursor [String, nil] Cursor for pagination
|
|
31
|
+
# @return [Hash] Paginated list of team members
|
|
32
|
+
def members(page_size: nil, page_cursor: nil)
|
|
33
|
+
params = build_params(page_size: page_size, page_cursor: page_cursor)
|
|
34
|
+
@http_client.get(path: '/team/members', params: params)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lettermint
|
|
4
|
+
module Resources
|
|
5
|
+
# Webhooks resource for managing webhook endpoints and viewing deliveries.
|
|
6
|
+
class Webhooks < Base
|
|
7
|
+
# List all webhooks.
|
|
8
|
+
# @param page_size [Integer, nil] Number of items per page (default: 30)
|
|
9
|
+
# @param page_cursor [String, nil] Cursor for pagination
|
|
10
|
+
# @param sort [String, nil] Sort field: name, url, created_at (prefix - for desc)
|
|
11
|
+
# @param enabled [Boolean, nil] Filter by enabled status
|
|
12
|
+
# @param event [String, nil] Filter by event type
|
|
13
|
+
# @param route_id [String, nil] Filter by route ID
|
|
14
|
+
# @param search [String, nil] Search filter
|
|
15
|
+
# @return [Hash] Paginated list of webhooks
|
|
16
|
+
# rubocop:disable Metrics/ParameterLists
|
|
17
|
+
def list(page_size: nil, page_cursor: nil, sort: nil, enabled: nil,
|
|
18
|
+
event: nil, route_id: nil, search: nil)
|
|
19
|
+
params = build_params(
|
|
20
|
+
page_size: page_size,
|
|
21
|
+
page_cursor: page_cursor,
|
|
22
|
+
sort: sort,
|
|
23
|
+
enabled: enabled,
|
|
24
|
+
event: event,
|
|
25
|
+
route_id: route_id,
|
|
26
|
+
search: search
|
|
27
|
+
)
|
|
28
|
+
@http_client.get(path: '/webhooks', params: params)
|
|
29
|
+
end
|
|
30
|
+
# rubocop:enable Metrics/ParameterLists
|
|
31
|
+
|
|
32
|
+
# Create a new webhook.
|
|
33
|
+
# @param route_id [String] Route ID to attach webhook to
|
|
34
|
+
# @param name [String] Webhook name (max 255 chars)
|
|
35
|
+
# @param url [String] Webhook URL (max 500 chars)
|
|
36
|
+
# @param events [Array<String>] Event types to subscribe (min 1)
|
|
37
|
+
# @param enabled [Boolean, nil] Enable webhook (default: true)
|
|
38
|
+
# @return [Hash] Created webhook data including secret (shown only once)
|
|
39
|
+
def create(route_id:, name:, url:, events:, enabled: nil)
|
|
40
|
+
data = { route_id: route_id, name: name, url: url, events: events }
|
|
41
|
+
data[:enabled] = enabled unless enabled.nil?
|
|
42
|
+
@http_client.post(path: '/webhooks', data: data)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Get webhook details.
|
|
46
|
+
# @param id [String] Webhook ID
|
|
47
|
+
# @return [Hash] Webhook data including secret
|
|
48
|
+
def find(id)
|
|
49
|
+
@http_client.get(path: "/webhooks/#{id}")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Update a webhook.
|
|
53
|
+
# @param id [String] Webhook ID
|
|
54
|
+
# @param name [String, nil] New webhook name
|
|
55
|
+
# @param url [String, nil] New webhook URL
|
|
56
|
+
# @param enabled [Boolean, nil] Enable/disable webhook
|
|
57
|
+
# @param events [Array<String>, nil] Event types (min 1)
|
|
58
|
+
# @return [Hash] Updated webhook data
|
|
59
|
+
def update(id, name: nil, url: nil, enabled: nil, events: nil)
|
|
60
|
+
data = {}
|
|
61
|
+
data[:name] = name if name
|
|
62
|
+
data[:url] = url if url
|
|
63
|
+
data[:enabled] = enabled unless enabled.nil?
|
|
64
|
+
data[:events] = events if events
|
|
65
|
+
@http_client.put(path: "/webhooks/#{id}", data: data)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Delete a webhook.
|
|
69
|
+
# @param id [String] Webhook ID
|
|
70
|
+
# @return [Hash] Confirmation message
|
|
71
|
+
def delete(id)
|
|
72
|
+
@http_client.delete(path: "/webhooks/#{id}")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Test a webhook by sending a test delivery.
|
|
76
|
+
# @param id [String] Webhook ID
|
|
77
|
+
# @return [Hash] Contains delivery_id for tracking
|
|
78
|
+
def test(id)
|
|
79
|
+
@http_client.post(path: "/webhooks/#{id}/test")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Regenerate webhook secret.
|
|
83
|
+
# @param id [String] Webhook ID
|
|
84
|
+
# @return [Hash] Updated webhook data with new secret
|
|
85
|
+
def regenerate_secret(id)
|
|
86
|
+
@http_client.post(path: "/webhooks/#{id}/regenerate-secret")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# List webhook deliveries.
|
|
90
|
+
# @param webhook_id [String] Webhook ID
|
|
91
|
+
# @param page_size [Integer, nil] Number of items per page
|
|
92
|
+
# @param page_cursor [String, nil] Cursor for pagination
|
|
93
|
+
# @param sort [String, nil] Sort field: created_at, attempt_number
|
|
94
|
+
# @param status [String, nil] Filter: pending, success, failed, client_error, server_error, timeout
|
|
95
|
+
# @param event_type [String, nil] Filter by event type
|
|
96
|
+
# @param from_date [String, nil] Filter from date (Y-m-d)
|
|
97
|
+
# @param to_date [String, nil] Filter to date (Y-m-d)
|
|
98
|
+
# @return [Hash] Paginated list of deliveries
|
|
99
|
+
# rubocop:disable Metrics/ParameterLists
|
|
100
|
+
def deliveries(webhook_id, page_size: nil, page_cursor: nil, sort: nil,
|
|
101
|
+
status: nil, event_type: nil, from_date: nil, to_date: nil)
|
|
102
|
+
params = build_params(
|
|
103
|
+
page_size: page_size,
|
|
104
|
+
page_cursor: page_cursor,
|
|
105
|
+
sort: sort,
|
|
106
|
+
status: status,
|
|
107
|
+
event_type: event_type,
|
|
108
|
+
from_date: from_date,
|
|
109
|
+
to_date: to_date
|
|
110
|
+
)
|
|
111
|
+
@http_client.get(path: "/webhooks/#{webhook_id}/deliveries", params: params)
|
|
112
|
+
end
|
|
113
|
+
# rubocop:enable Metrics/ParameterLists
|
|
114
|
+
|
|
115
|
+
# Get a specific delivery.
|
|
116
|
+
# @param webhook_id [String] Webhook ID
|
|
117
|
+
# @param delivery_id [String] Delivery ID
|
|
118
|
+
# @return [Hash] Delivery data including payload and response
|
|
119
|
+
def delivery(webhook_id, delivery_id)
|
|
120
|
+
@http_client.get(path: "/webhooks/#{webhook_id}/deliveries/#{delivery_id}")
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lettermint
|
|
4
|
+
# Client for the Lettermint Sending API (project-level email sending).
|
|
5
|
+
# Authenticates with project tokens via x-lettermint-token header.
|
|
6
|
+
class SendingAPI
|
|
7
|
+
attr_reader :configuration
|
|
8
|
+
|
|
9
|
+
def initialize(api_token:, base_url: nil, timeout: nil)
|
|
10
|
+
validate_api_token!(api_token)
|
|
11
|
+
|
|
12
|
+
@configuration = Configuration.new
|
|
13
|
+
@configuration.base_url = base_url || Lettermint.configuration.base_url
|
|
14
|
+
@configuration.timeout = timeout || Lettermint.configuration.timeout
|
|
15
|
+
|
|
16
|
+
yield @configuration if block_given?
|
|
17
|
+
|
|
18
|
+
@http_client = HttpClient.new(
|
|
19
|
+
api_token: api_token,
|
|
20
|
+
base_url: @configuration.base_url,
|
|
21
|
+
timeout: @configuration.timeout,
|
|
22
|
+
auth_scheme: :project
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def email
|
|
27
|
+
EmailMessage.new(http_client: @http_client)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Makes a GET request to an arbitrary API endpoint.
|
|
31
|
+
#
|
|
32
|
+
# @param path [String] The API endpoint path (e.g., '/domains')
|
|
33
|
+
# @param params [Hash, nil] Query parameters to include in the request
|
|
34
|
+
# @param headers [Hash, nil] Additional HTTP headers
|
|
35
|
+
# @return [Hash] The parsed JSON response body
|
|
36
|
+
def get(path, params: nil, headers: nil)
|
|
37
|
+
@http_client.get(path: path, params: params, headers: headers)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Makes a POST request to an arbitrary API endpoint.
|
|
41
|
+
#
|
|
42
|
+
# @param path [String] The API endpoint path
|
|
43
|
+
# @param data [Hash, nil] The request body (will be JSON-encoded)
|
|
44
|
+
# @param headers [Hash, nil] Additional HTTP headers
|
|
45
|
+
# @return [Hash] The parsed JSON response body
|
|
46
|
+
def post(path, data: nil, headers: nil)
|
|
47
|
+
@http_client.post(path: path, data: data, headers: headers)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Makes a PUT request to an arbitrary API endpoint.
|
|
51
|
+
#
|
|
52
|
+
# @param path [String] The API endpoint path
|
|
53
|
+
# @param data [Hash, nil] The request body (will be JSON-encoded)
|
|
54
|
+
# @param headers [Hash, nil] Additional HTTP headers
|
|
55
|
+
# @return [Hash] The parsed JSON response body
|
|
56
|
+
def put(path, data: nil, headers: nil)
|
|
57
|
+
@http_client.put(path: path, data: data, headers: headers)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Makes a DELETE request to an arbitrary API endpoint.
|
|
61
|
+
#
|
|
62
|
+
# @param path [String] The API endpoint path
|
|
63
|
+
# @param headers [Hash, nil] Additional HTTP headers
|
|
64
|
+
# @return [Hash] The parsed JSON response body
|
|
65
|
+
def delete(path, headers: nil)
|
|
66
|
+
@http_client.delete(path: path, headers: headers)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def validate_api_token!(token)
|
|
72
|
+
raise ArgumentError, 'API token cannot be empty' if token.nil? || token.to_s.strip.empty?
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lettermint
|
|
4
|
+
# Client for the Lettermint Team API (team-level management operations).
|
|
5
|
+
# Authenticates with team tokens (lm_team_*) via Authorization: Bearer header.
|
|
6
|
+
class TeamAPI
|
|
7
|
+
attr_reader :configuration
|
|
8
|
+
|
|
9
|
+
def initialize(team_token:, base_url: nil, timeout: nil)
|
|
10
|
+
validate_team_token!(team_token)
|
|
11
|
+
|
|
12
|
+
@configuration = Configuration.new
|
|
13
|
+
@configuration.base_url = base_url || Lettermint.configuration.base_url
|
|
14
|
+
@configuration.timeout = timeout || Lettermint.configuration.timeout
|
|
15
|
+
|
|
16
|
+
yield @configuration if block_given?
|
|
17
|
+
|
|
18
|
+
@http_client = HttpClient.new(
|
|
19
|
+
api_token: team_token,
|
|
20
|
+
base_url: @configuration.base_url,
|
|
21
|
+
timeout: @configuration.timeout,
|
|
22
|
+
auth_scheme: :team
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Health check endpoint (accepts both token types)
|
|
27
|
+
# @return [Hash] Parsed response body, e.g. { 'ok' => true } on success
|
|
28
|
+
def ping
|
|
29
|
+
@http_client.get(path: '/ping')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Team resource accessor
|
|
33
|
+
# @return [Resources::Team]
|
|
34
|
+
def team
|
|
35
|
+
Resources::Team.new(http_client: @http_client)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Domains resource accessor
|
|
39
|
+
# @return [Resources::Domains]
|
|
40
|
+
def domains
|
|
41
|
+
Resources::Domains.new(http_client: @http_client)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Projects resource accessor
|
|
45
|
+
# @return [Resources::Projects]
|
|
46
|
+
def projects
|
|
47
|
+
Resources::Projects.new(http_client: @http_client)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Webhooks resource accessor
|
|
51
|
+
# @return [Resources::Webhooks]
|
|
52
|
+
def webhooks
|
|
53
|
+
Resources::Webhooks.new(http_client: @http_client)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Messages resource accessor
|
|
57
|
+
# @return [Resources::Messages]
|
|
58
|
+
def messages
|
|
59
|
+
Resources::Messages.new(http_client: @http_client)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Suppressions resource accessor
|
|
63
|
+
# @return [Resources::Suppressions]
|
|
64
|
+
def suppressions
|
|
65
|
+
Resources::Suppressions.new(http_client: @http_client)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Stats resource accessor
|
|
69
|
+
# @return [Resources::Stats]
|
|
70
|
+
def stats
|
|
71
|
+
Resources::Stats.new(http_client: @http_client)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Routes resource accessor (top-level for direct route access)
|
|
75
|
+
# For project-scoped routes, use projects.routes(project_id)
|
|
76
|
+
# @return [Resources::Routes]
|
|
77
|
+
def routes
|
|
78
|
+
Resources::Routes.new(http_client: @http_client)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def validate_team_token!(token)
|
|
84
|
+
raise ArgumentError, 'Team token cannot be empty' if token.nil? || token.to_s.strip.empty?
|
|
85
|
+
return if token.to_s.start_with?('lm_team_')
|
|
86
|
+
|
|
87
|
+
raise ArgumentError, "Invalid team token format (expected 'lm_team_*')"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
data/lib/lettermint/version.rb
CHANGED
data/lib/lettermint.rb
CHANGED
|
@@ -7,6 +7,21 @@ require_relative 'lettermint/configuration'
|
|
|
7
7
|
require_relative 'lettermint/http_client'
|
|
8
8
|
require_relative 'lettermint/email_message'
|
|
9
9
|
require_relative 'lettermint/webhook'
|
|
10
|
+
|
|
11
|
+
# Resources (Team API)
|
|
12
|
+
require_relative 'lettermint/resources/base'
|
|
13
|
+
require_relative 'lettermint/resources/team'
|
|
14
|
+
require_relative 'lettermint/resources/domains'
|
|
15
|
+
require_relative 'lettermint/resources/projects'
|
|
16
|
+
require_relative 'lettermint/resources/routes'
|
|
17
|
+
require_relative 'lettermint/resources/webhooks'
|
|
18
|
+
require_relative 'lettermint/resources/messages'
|
|
19
|
+
require_relative 'lettermint/resources/suppressions'
|
|
20
|
+
require_relative 'lettermint/resources/stats'
|
|
21
|
+
|
|
22
|
+
# API Clients
|
|
23
|
+
require_relative 'lettermint/sending_api'
|
|
24
|
+
require_relative 'lettermint/team_api'
|
|
10
25
|
require_relative 'lettermint/client'
|
|
11
26
|
|
|
12
27
|
module Lettermint
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lettermint
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Delano
|
|
@@ -31,6 +31,7 @@ executables: []
|
|
|
31
31
|
extensions: []
|
|
32
32
|
extra_rdoc_files: []
|
|
33
33
|
files:
|
|
34
|
+
- ".gemini/settings.json"
|
|
34
35
|
- CHANGELOG.md
|
|
35
36
|
- LICENSE
|
|
36
37
|
- README.md
|
|
@@ -41,6 +42,17 @@ files:
|
|
|
41
42
|
- lib/lettermint/email_message.rb
|
|
42
43
|
- lib/lettermint/errors.rb
|
|
43
44
|
- lib/lettermint/http_client.rb
|
|
45
|
+
- lib/lettermint/resources/base.rb
|
|
46
|
+
- lib/lettermint/resources/domains.rb
|
|
47
|
+
- lib/lettermint/resources/messages.rb
|
|
48
|
+
- lib/lettermint/resources/projects.rb
|
|
49
|
+
- lib/lettermint/resources/routes.rb
|
|
50
|
+
- lib/lettermint/resources/stats.rb
|
|
51
|
+
- lib/lettermint/resources/suppressions.rb
|
|
52
|
+
- lib/lettermint/resources/team.rb
|
|
53
|
+
- lib/lettermint/resources/webhooks.rb
|
|
54
|
+
- lib/lettermint/sending_api.rb
|
|
55
|
+
- lib/lettermint/team_api.rb
|
|
44
56
|
- lib/lettermint/types.rb
|
|
45
57
|
- lib/lettermint/version.rb
|
|
46
58
|
- lib/lettermint/webhook.rb
|