sevk 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +21 -0
- data/README.md +6 -6
- data/lib/sevk/client.rb +140 -63
- data/lib/sevk/error.rb +38 -5
- data/lib/sevk/markup/document_parser.rb +83 -0
- data/lib/sevk/markup/html_document.rb +82 -0
- data/lib/sevk/markup/number_formatting.rb +40 -0
- data/lib/sevk/markup/render_limits.rb +52 -0
- data/lib/sevk/markup/renderer.rb +232 -729
- data/lib/sevk/markup/style_helpers.rb +131 -0
- data/lib/sevk/markup/syntax_highlighting.rb +227 -0
- data/lib/sevk/markup/tag_scanner.rb +112 -0
- data/lib/sevk/markup/template_engine.rb +247 -0
- data/lib/sevk/resources/activity.rb +23 -0
- data/lib/sevk/resources/audiences.rb +22 -11
- data/lib/sevk/resources/base.rb +15 -3
- data/lib/sevk/resources/broadcasts.rb +56 -12
- data/lib/sevk/resources/contacts.rb +32 -17
- data/lib/sevk/resources/domains.rb +22 -8
- data/lib/sevk/resources/emails.rb +85 -34
- data/lib/sevk/resources/inbound.rb +123 -0
- data/lib/sevk/resources/outbound.rb +55 -0
- data/lib/sevk/resources/segments.rb +34 -20
- data/lib/sevk/resources/subscriptions.rb +18 -11
- data/lib/sevk/resources/templates.rb +20 -8
- data/lib/sevk/resources/topics.rb +31 -20
- data/lib/sevk/resources/usage.rb +15 -0
- data/lib/sevk/resources/webhooks.rb +21 -7
- data/lib/sevk/version.rb +1 -1
- data/lib/sevk/webhook_verifier.rb +91 -0
- data/lib/sevk.rb +24 -238
- metadata +21 -94
- data/Rakefile +0 -42
- data/lib/sevk/resources/events.rb +0 -18
- data/sevk.gemspec +0 -41
|
@@ -1,55 +1,106 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative '../markup/renderer'
|
|
4
5
|
|
|
5
6
|
module Sevk
|
|
6
7
|
module Resources
|
|
7
8
|
class Emails < Base
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
# @param subject [String] Email subject
|
|
12
|
-
# @param html [String] HTML content
|
|
13
|
-
# @param from [String] Sender email
|
|
14
|
-
# @param from_name [String, nil] Sender name
|
|
15
|
-
# @param reply_to [String, nil] Reply-to address
|
|
16
|
-
# @param text [String, nil] Plain text content
|
|
17
|
-
# @param headers [Hash, nil] Custom headers
|
|
18
|
-
# @param tags [Array, nil] Tags
|
|
19
|
-
# @param attachments [Array<Hash>, nil] Attachments (max 10, 10MB total)
|
|
20
|
-
# Each attachment: { filename: String, content: String (base64), content_type: String }
|
|
21
|
-
# @return [Hash] Response with :id or :ids
|
|
22
|
-
def send(to:, subject:, html:, from:, from_name: nil, reply_to: nil, text: nil, headers: nil, tags: nil, attachments: nil)
|
|
9
|
+
# Sends one transactional email, rendering local markup when supplied.
|
|
10
|
+
def send(to:, subject:, from:, html: nil, text: nil, markup: nil, reply_to: nil, cc: nil, bcc: nil,
|
|
11
|
+
attachments: nil, tracking: nil, headers: nil, scheduled_at: nil, options: nil)
|
|
23
12
|
body = {
|
|
24
13
|
to: to,
|
|
25
14
|
subject: subject,
|
|
26
|
-
html: html,
|
|
27
15
|
from: from
|
|
28
16
|
}
|
|
29
|
-
body[:
|
|
30
|
-
body[:
|
|
31
|
-
body[:text] = text
|
|
32
|
-
body[:
|
|
33
|
-
body[:
|
|
34
|
-
body[:
|
|
35
|
-
|
|
17
|
+
body[:html] = html unless html.nil?
|
|
18
|
+
body[:html] = Sevk::Markup::Renderer.new.render(markup) if markup && !markup.empty?
|
|
19
|
+
body[:text] = text unless text.nil?
|
|
20
|
+
body[:reply_to] = reply_to unless reply_to.nil?
|
|
21
|
+
body[:cc] = cc unless cc.nil?
|
|
22
|
+
body[:bcc] = bcc unless bcc.nil?
|
|
23
|
+
body[:attachments] = normalize_attachments(attachments) unless attachments.nil?
|
|
24
|
+
body[:tracking] = tracking unless tracking.nil?
|
|
25
|
+
body[:headers] = headers unless headers.nil?
|
|
26
|
+
body[:scheduledAt] = scheduled_at unless scheduled_at.nil?
|
|
27
|
+
|
|
28
|
+
client.post('/emails', body, headers: idempotency_headers(options))
|
|
36
29
|
end
|
|
37
30
|
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
31
|
+
# Sends up to 100 transactional emails and returns per-request results.
|
|
32
|
+
def send_bulk(emails, options: nil)
|
|
33
|
+
unless emails.is_a?(Array)
|
|
34
|
+
return client.post('/emails/bulk', { emails: emails }, headers: idempotency_headers(options))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
wire_emails = emails.map do |email|
|
|
38
|
+
next email unless email.is_a?(Hash)
|
|
39
|
+
|
|
40
|
+
payload = email.dup
|
|
41
|
+
symbol_markup = payload.delete(:markup)
|
|
42
|
+
string_markup = payload.delete('markup')
|
|
43
|
+
mk = symbol_markup.nil? ? string_markup : symbol_markup
|
|
44
|
+
unless mk.nil? || mk.empty?
|
|
45
|
+
payload.delete(:html)
|
|
46
|
+
payload.delete('html')
|
|
47
|
+
payload[:html] = Sevk::Markup::Renderer.new.render(mk)
|
|
48
|
+
end
|
|
49
|
+
attachments = payload[:attachments] || payload['attachments']
|
|
50
|
+
if attachments
|
|
51
|
+
payload.delete('attachments')
|
|
52
|
+
payload[:attachments] = normalize_attachments(attachments)
|
|
53
|
+
end
|
|
54
|
+
payload
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
client.post('/emails/bulk', { emails: wire_emails }, headers: idempotency_headers(options))
|
|
44
58
|
end
|
|
45
59
|
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
# @param id [String] Email ID
|
|
49
|
-
# @return [Hash] Email details
|
|
60
|
+
# Retrieves a sent email and its current delivery state.
|
|
50
61
|
def get(id)
|
|
51
62
|
client.get("/emails/#{id}")
|
|
52
63
|
end
|
|
64
|
+
|
|
65
|
+
# moves an unattempted scheduled email to a new delivery time.
|
|
66
|
+
def update(id, scheduled_at:)
|
|
67
|
+
client.patch("/emails/#{id}", { scheduledAt: scheduled_at })
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# cancels an unattempted scheduled email.
|
|
71
|
+
def cancel(id)
|
|
72
|
+
client.post("/emails/#{id}/cancel")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def normalize_attachments(attachments)
|
|
78
|
+
return attachments unless attachments.is_a?(Array)
|
|
79
|
+
|
|
80
|
+
attachments.map do |attachment|
|
|
81
|
+
next attachment unless attachment.is_a?(Hash)
|
|
82
|
+
|
|
83
|
+
normalized = attachment.dup
|
|
84
|
+
content_type = normalized.delete(:content_type)
|
|
85
|
+
content_type = normalized.delete('content_type') if content_type.nil?
|
|
86
|
+
unless content_type.nil? || normalized.key?(:contentType) || normalized.key?('contentType')
|
|
87
|
+
normalized[:contentType] = content_type
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
normalized
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Builds the optional idempotency header used to make retries safe.
|
|
95
|
+
def idempotency_headers(options)
|
|
96
|
+
return nil unless options
|
|
97
|
+
|
|
98
|
+
return nil unless options.key?(:idempotency_key) || options.key?('idempotency_key')
|
|
99
|
+
|
|
100
|
+
key = options.key?(:idempotency_key) ? options[:idempotency_key] : options['idempotency_key']
|
|
101
|
+
|
|
102
|
+
{ 'Idempotency-Key' => key.to_s }
|
|
103
|
+
end
|
|
53
104
|
end
|
|
54
105
|
end
|
|
55
106
|
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Sevk
|
|
6
|
+
module Resources
|
|
7
|
+
class Inbound < Base
|
|
8
|
+
# Lists received email with optional pagination and filtering.
|
|
9
|
+
def list(page: nil, limit: nil, after: nil, before: nil, search: nil, status: nil, domain_id: nil,
|
|
10
|
+
broadcast_id: nil, is_spam: nil, is_virus: nil)
|
|
11
|
+
params = {
|
|
12
|
+
page: page,
|
|
13
|
+
limit: limit,
|
|
14
|
+
after: after,
|
|
15
|
+
before: before,
|
|
16
|
+
search: search,
|
|
17
|
+
status: status,
|
|
18
|
+
domainId: domain_id,
|
|
19
|
+
broadcastId: broadcast_id,
|
|
20
|
+
isSpam: is_spam,
|
|
21
|
+
isVirus: is_virus
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
client.get("/inbound#{build_query_string(params)}")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Retrieves a received email with parsed content and attachment metadata.
|
|
28
|
+
def get(id)
|
|
29
|
+
client.get("/inbound/#{id}")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Deletes a received email and its stored content.
|
|
33
|
+
def delete(id)
|
|
34
|
+
client.delete("/inbound/#{id}")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Returns aggregate inbound processing statistics, optionally for one domain.
|
|
38
|
+
def stats(domain_id: nil)
|
|
39
|
+
params = { domainId: domain_id }
|
|
40
|
+
|
|
41
|
+
client.get("/inbound/stats#{build_query_string(params)}")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Downloads the original content and metadata for an inbound attachment.
|
|
45
|
+
def download_attachment(email_id, attachment_id)
|
|
46
|
+
client.get("/inbound/#{email_id}/attachments/#{attachment_id}/download")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Lists rules used to store or forward matching inbound email.
|
|
50
|
+
def list_routes(**params)
|
|
51
|
+
client.get("/inbound-settings/routes#{build_query_string(params)}")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def route_items(**params)
|
|
55
|
+
list_routes(**params).fetch('items')
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Retrieves an inbound routing rule by id.
|
|
59
|
+
def get_route(id)
|
|
60
|
+
client.get("/inbound-settings/routes/#{id}")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Creates an inbound routing rule.
|
|
64
|
+
def create_route(**params)
|
|
65
|
+
client.post('/inbound-settings/routes', wire_keys(params))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Updates an inbound routing rule while preserving omitted fields.
|
|
69
|
+
def update_route(id, **params)
|
|
70
|
+
client.put("/inbound-settings/routes/#{id}", wire_keys(params))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Deletes an inbound routing rule without deleting stored email.
|
|
74
|
+
def delete_route(id)
|
|
75
|
+
client.delete("/inbound-settings/routes/#{id}")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Lists blocked addresses with optional offset or cursor pagination.
|
|
79
|
+
def list_blocklist(**params)
|
|
80
|
+
client.get("/inbound-settings/blocklist#{build_query_string(params)}")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def blocklist_items(**params)
|
|
84
|
+
list_blocklist(**params).fetch('items')
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Adds an address to the inbound blocklist.
|
|
88
|
+
def add_blocklist_entry(**params)
|
|
89
|
+
client.post('/inbound-settings/blocklist', wire_keys(params))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Removes an address from the inbound blocklist.
|
|
93
|
+
def remove_blocklist_entry(id)
|
|
94
|
+
client.delete("/inbound-settings/blocklist/#{id}")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Returns inbound quota settings and current usage counters.
|
|
98
|
+
def get_quota
|
|
99
|
+
client.get('/inbound-settings/quota')
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Updates the inbound monthly limit while preserving omitted fields.
|
|
103
|
+
def update_quota(**params)
|
|
104
|
+
client.put('/inbound-settings/quota', wire_keys(params))
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Clears a quota pause without resetting current-period usage.
|
|
108
|
+
def reset_quota
|
|
109
|
+
client.post('/inbound-settings/quota/reset', {})
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Returns effective inbound security settings for a domain.
|
|
113
|
+
def get_security_config(domain_id)
|
|
114
|
+
client.get("/inbound-settings/security/#{domain_id}")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Updates inbound security settings for a domain.
|
|
118
|
+
def update_security_config(domain_id, **params)
|
|
119
|
+
client.put("/inbound-settings/security/#{domain_id}", wire_keys(params))
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Sevk
|
|
6
|
+
module Resources
|
|
7
|
+
# Outbound resource. Read and adjust the project's monthly outbound
|
|
8
|
+
# sending cap, and pause / resume transactional sending.
|
|
9
|
+
#
|
|
10
|
+
# Pause affects +POST /emails+, +POST /emails/bulk+, and SMTP relay.
|
|
11
|
+
# Broadcasts run on a separate balance check and are NOT affected.
|
|
12
|
+
#
|
|
13
|
+
# Access via +sevk.outbound+. Requires an API key with +outbound:read+
|
|
14
|
+
# for +get_quota+, or +outbound:write+ for the others.
|
|
15
|
+
class Outbound < Base
|
|
16
|
+
# Get outbound quota settings and current usage counters.
|
|
17
|
+
# Requires +outbound:read+.
|
|
18
|
+
def get_quota
|
|
19
|
+
client.get('/outbound-settings/quota')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Set the user-side monthly outbound cap. Pass +monthlyEmailLimit:+
|
|
23
|
+
# as a positive Integer to cap, or +nil+ to clear the user-set cap
|
|
24
|
+
# (admin ceiling still applies). Raising the cap above current
|
|
25
|
+
# usage clears a +monthly_cap_exhausted+ pause; does not clear
|
|
26
|
+
# +manual_suspend+.
|
|
27
|
+
# Requires +outbound:write+.
|
|
28
|
+
def update_quota(**params)
|
|
29
|
+
client.put('/outbound-settings/quota', wire_keys(params))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Preserve the monthly counter and clear any pause (including
|
|
33
|
+
# +manual_suspend+ and +balance_exhausted+). The user-set cap is
|
|
34
|
+
# preserved.
|
|
35
|
+
# Requires +outbound:write+.
|
|
36
|
+
def reset_quota
|
|
37
|
+
client.post('/outbound-settings/quota/reset', {})
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Reset custom-limit usage while preserving the cap and monthly usage.
|
|
41
|
+
# Requires +outbound:write+.
|
|
42
|
+
def reset_limit
|
|
43
|
+
client.post('/outbound-settings/quota/reset-limit', {})
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Pause outbound transactional sending with
|
|
47
|
+
# +pausedReason: manual_suspend+. Manual pauses do NOT auto-clear;
|
|
48
|
+
# call +reset_quota+ to resume. Does not affect broadcasts.
|
|
49
|
+
# Requires +outbound:write+.
|
|
50
|
+
def pause_quota
|
|
51
|
+
client.post('/outbound-settings/quota/pause', {})
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -1,42 +1,56 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
3
|
+
require_relative 'base'
|
|
4
4
|
|
|
5
5
|
module Sevk
|
|
6
6
|
module Resources
|
|
7
7
|
class Segments < Base
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
end
|
|
8
|
+
# Lists segments belonging to an audience with optional filtering.
|
|
9
|
+
def list(audience_id, page: nil, limit: nil, after: nil, before: nil, search: nil)
|
|
10
|
+
params = { page: page, limit: limit, after: after, before: before, search: search }
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
client.get("/audiences/#{audience_id}/segments/#{segment_id}")
|
|
12
|
+
client.get("/audiences/#{audience_id}/segments#{build_query_string(params)}")
|
|
15
13
|
end
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
# Creates a saved contact segment under an audience.
|
|
16
|
+
def create(audience_id, name:, rules:, operator: 'AND', limit_to_topics: nil, groups: nil)
|
|
18
17
|
body = { name: name, rules: rules, operator: operator }
|
|
18
|
+
body[:limitToTopics] = limit_to_topics unless limit_to_topics.nil?
|
|
19
|
+
body[:groups] = groups unless groups.nil?
|
|
20
|
+
|
|
19
21
|
client.post("/audiences/#{audience_id}/segments", body)
|
|
20
22
|
end
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
body[:rules] = rules if rules
|
|
26
|
-
body[:operator] = operator if operator
|
|
27
|
-
client.put("/audiences/#{audience_id}/segments/#{segment_id}", body)
|
|
24
|
+
# Evaluates segment rules and returns matches without saving a segment.
|
|
25
|
+
def preview(audience_id, data)
|
|
26
|
+
client.post("/audiences/#{audience_id}/segments/preview", wire_keys(data))
|
|
28
27
|
end
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
# Retrieves a saved segment by id.
|
|
30
|
+
def get(segment_id)
|
|
31
|
+
client.get("/segments/#{segment_id}")
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
# Updates a segment while preserving omitted fields.
|
|
35
|
+
def update(segment_id, name: nil, rules: nil, operator: nil, limit_to_topics: nil, groups: nil)
|
|
36
|
+
body = {}
|
|
37
|
+
body[:name] = name unless name.nil?
|
|
38
|
+
body[:rules] = rules unless rules.nil?
|
|
39
|
+
body[:operator] = operator unless operator.nil?
|
|
40
|
+
body[:limitToTopics] = limit_to_topics unless limit_to_topics.nil?
|
|
41
|
+
body[:groups] = groups unless groups.nil?
|
|
42
|
+
|
|
43
|
+
client.put("/segments/#{segment_id}", body)
|
|
36
44
|
end
|
|
37
45
|
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
# Deletes a segment without deleting any contacts.
|
|
47
|
+
def delete(segment_id)
|
|
48
|
+
client.delete("/segments/#{segment_id}")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Recalculates and returns the current matching contact count.
|
|
52
|
+
def calculate(segment_id)
|
|
53
|
+
client.get("/segments/#{segment_id}/calculate")
|
|
40
54
|
end
|
|
41
55
|
end
|
|
42
56
|
end
|
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
3
|
+
require_relative 'base'
|
|
4
4
|
|
|
5
5
|
module Sevk
|
|
6
6
|
module Resources
|
|
7
7
|
class Subscriptions < Base
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
body
|
|
11
|
-
body[:
|
|
12
|
-
|
|
8
|
+
# Subscribes a contact and optionally adds it to an audience.
|
|
9
|
+
def subscribe(email:, audience_id: nil, data: nil)
|
|
10
|
+
body = { email: email }
|
|
11
|
+
body[:audienceId] = audience_id unless audience_id.nil?
|
|
12
|
+
body[:data] = data unless data.nil?
|
|
13
|
+
|
|
14
|
+
client.post('/subscriptions/subscribe', body)
|
|
13
15
|
end
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
# Global opt-out. Provide at least one of email or contact_id (if both are
|
|
18
|
+
# given, contact_id wins). broadcast_id only attributes the opt-out to a
|
|
19
|
+
# broadcast (activity + counters); it does NOT scope the unsubscribe.
|
|
20
|
+
def unsubscribe(email: nil, contact_id: nil, broadcast_id: nil)
|
|
21
|
+
body = {}
|
|
22
|
+
body[:email] = email unless email.nil?
|
|
23
|
+
body[:contactId] = contact_id unless contact_id.nil?
|
|
24
|
+
body[:broadcastId] = broadcast_id unless broadcast_id.nil?
|
|
25
|
+
|
|
26
|
+
client.post('/subscriptions/unsubscribe', body)
|
|
20
27
|
end
|
|
21
28
|
end
|
|
22
29
|
end
|
|
@@ -1,34 +1,46 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
3
|
+
require_relative 'base'
|
|
4
4
|
|
|
5
5
|
module Sevk
|
|
6
6
|
module Resources
|
|
7
7
|
class Templates < Base
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
# Lists reusable email templates with optional pagination and search.
|
|
9
|
+
def list(page: nil, limit: nil, after: nil, before: nil, search: nil)
|
|
10
|
+
params = { page: page, limit: limit, after: after, before: before, search: search }
|
|
11
|
+
|
|
10
12
|
client.get("/templates#{build_query_string(params)}")
|
|
11
13
|
end
|
|
12
14
|
|
|
15
|
+
# Retrieves a template by id.
|
|
13
16
|
def get(id)
|
|
14
17
|
client.get("/templates/#{id}")
|
|
15
18
|
end
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
# Creates a reusable email template from markup and optional styles.
|
|
21
|
+
def create(title:, content:, style: nil)
|
|
22
|
+
body = { title: title, content: content }
|
|
23
|
+
body[:style] = style unless style.nil?
|
|
24
|
+
|
|
25
|
+
client.post('/templates', body)
|
|
19
26
|
end
|
|
20
27
|
|
|
21
|
-
|
|
28
|
+
# Updates a template while preserving omitted fields.
|
|
29
|
+
def update(id, title: nil, content: nil, style: nil)
|
|
22
30
|
body = {}
|
|
23
|
-
body[:title] = title
|
|
24
|
-
body[:content] = content
|
|
31
|
+
body[:title] = title unless title.nil?
|
|
32
|
+
body[:content] = content unless content.nil?
|
|
33
|
+
body[:style] = style unless style.nil?
|
|
34
|
+
|
|
25
35
|
client.put("/templates/#{id}", body)
|
|
26
36
|
end
|
|
27
37
|
|
|
38
|
+
# Deletes a template without affecting email already sent from it.
|
|
28
39
|
def delete(id)
|
|
29
40
|
client.delete("/templates/#{id}")
|
|
30
41
|
end
|
|
31
42
|
|
|
43
|
+
# Creates an independent copy of a template for further editing.
|
|
32
44
|
def duplicate(id)
|
|
33
45
|
client.post("/templates/#{id}/duplicate", {})
|
|
34
46
|
end
|
|
@@ -1,46 +1,57 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
3
|
+
require_relative 'base'
|
|
4
4
|
|
|
5
5
|
module Sevk
|
|
6
6
|
module Resources
|
|
7
7
|
class Topics < Base
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
end
|
|
8
|
+
# Lists topics belonging to an audience with optional filtering.
|
|
9
|
+
def list(audience_id, page: nil, limit: nil, after: nil, before: nil, search: nil)
|
|
10
|
+
params = { page: page, limit: limit, after: after, before: before, search: search }
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
client.get("/audiences/#{audience_id}/topics/#{topic_id}")
|
|
12
|
+
client.get("/audiences/#{audience_id}/topics#{build_query_string(params)}")
|
|
15
13
|
end
|
|
16
14
|
|
|
15
|
+
# Creates a subscription topic under an audience.
|
|
17
16
|
def create(audience_id, name:, description: nil)
|
|
18
17
|
body = { name: name }
|
|
19
|
-
body[:description] = description
|
|
18
|
+
body[:description] = description unless description.nil?
|
|
19
|
+
|
|
20
20
|
client.post("/audiences/#{audience_id}/topics", body)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
# Retrieves a topic by id.
|
|
24
|
+
def get(topic_id)
|
|
25
|
+
client.get("/topics/#{topic_id}")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Updates a topic while preserving omitted fields.
|
|
29
|
+
def update(topic_id, name: nil, description: nil)
|
|
24
30
|
body = {}
|
|
25
|
-
body[:name] = name
|
|
26
|
-
body[:description] = description
|
|
27
|
-
|
|
31
|
+
body[:name] = name unless name.nil?
|
|
32
|
+
body[:description] = description unless description.nil?
|
|
33
|
+
|
|
34
|
+
client.put("/topics/#{topic_id}", body)
|
|
28
35
|
end
|
|
29
36
|
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
# Deletes a topic without deleting its contacts or parent audience.
|
|
38
|
+
def delete(topic_id)
|
|
39
|
+
client.delete("/topics/#{topic_id}")
|
|
32
40
|
end
|
|
33
41
|
|
|
34
|
-
|
|
35
|
-
|
|
42
|
+
# Adds contacts to a topic, ignoring memberships that already exist.
|
|
43
|
+
def add_contacts(topic_id, contact_ids:)
|
|
44
|
+
client.post("/topics/#{topic_id}/contacts", { contactIds: contact_ids })
|
|
36
45
|
end
|
|
37
46
|
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
# Removes a contact from the topic without deleting the contact.
|
|
48
|
+
def remove_contact(topic_id, contact_id)
|
|
49
|
+
client.delete("/topics/#{topic_id}/contacts/#{contact_id}")
|
|
40
50
|
end
|
|
41
51
|
|
|
42
|
-
|
|
43
|
-
|
|
52
|
+
# Lists contacts by page, cursor, search text, or subscription state.
|
|
53
|
+
def list_contacts(topic_id, params = {})
|
|
54
|
+
client.get("/topics/#{topic_id}/contacts#{build_query_string(params)}")
|
|
44
55
|
end
|
|
45
56
|
end
|
|
46
57
|
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Sevk
|
|
6
|
+
module Resources
|
|
7
|
+
# Reads project limits, resource counts, and current email usage.
|
|
8
|
+
class Usage < Base
|
|
9
|
+
# Returns current limits, resource counts, and inbound and outbound usage.
|
|
10
|
+
def get
|
|
11
|
+
client.get('/limits')
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -1,37 +1,51 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
3
|
+
require_relative 'base'
|
|
4
4
|
|
|
5
5
|
module Sevk
|
|
6
6
|
module Resources
|
|
7
7
|
class Webhooks < Base
|
|
8
|
-
def
|
|
9
|
-
|
|
8
|
+
def rotate(id)
|
|
9
|
+
client.post("/webhooks/#{id}/rotate", {})
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Lists configured webhooks and the available event catalog.
|
|
13
|
+
def list(page: nil, limit: nil, after: nil, before: nil)
|
|
14
|
+
params = { page: page, limit: limit, after: after, before: before }
|
|
15
|
+
|
|
10
16
|
client.get("/webhooks#{build_query_string(params)}")
|
|
11
17
|
end
|
|
12
18
|
|
|
19
|
+
# Retrieves a webhook and its recent delivery state. Its secret is the
|
|
20
|
+
# masked placeholder (••••••••), not a usable verification secret.
|
|
13
21
|
def get(id)
|
|
14
22
|
client.get("/webhooks/#{id}")
|
|
15
23
|
end
|
|
16
24
|
|
|
25
|
+
# Creates a webhook subscription. The returned Hash always contains a non-nil
|
|
26
|
+
# :secret/"secret" value prefixed with whsec_; this usable secret appears only once.
|
|
17
27
|
def create(params)
|
|
18
|
-
client.post(
|
|
28
|
+
client.post('/webhooks', wire_keys(params))
|
|
19
29
|
end
|
|
20
30
|
|
|
31
|
+
# Updates a webhook while preserving omitted fields.
|
|
21
32
|
def update(id, params)
|
|
22
|
-
client.put("/webhooks/#{id}", params)
|
|
33
|
+
client.put("/webhooks/#{id}", wire_keys(params))
|
|
23
34
|
end
|
|
24
35
|
|
|
36
|
+
# Deletes a webhook and stops future deliveries to its URL.
|
|
25
37
|
def delete(id)
|
|
26
38
|
client.delete("/webhooks/#{id}")
|
|
27
39
|
end
|
|
28
40
|
|
|
41
|
+
# Sends a test event to a webhook and returns the delivery result.
|
|
29
42
|
def test(id)
|
|
30
43
|
client.post("/webhooks/#{id}/test", {})
|
|
31
44
|
end
|
|
32
45
|
|
|
33
|
-
|
|
34
|
-
|
|
46
|
+
# Lists event names that can be assigned to a webhook.
|
|
47
|
+
def events
|
|
48
|
+
client.get('/webhooks/events')
|
|
35
49
|
end
|
|
36
50
|
end
|
|
37
51
|
end
|
data/lib/sevk/version.rb
CHANGED