sevk 0.1.0 → 1.0.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/README.md +0 -4
- data/lib/sevk/client.rb +13 -0
- data/lib/sevk/markup/renderer.rb +460 -32
- data/lib/sevk/resources/audiences.rb +8 -0
- data/lib/sevk/resources/broadcasts.rb +44 -0
- data/lib/sevk/resources/contacts.rb +12 -0
- data/lib/sevk/resources/domains.rb +24 -0
- data/lib/sevk/resources/emails.rb +32 -1
- data/lib/sevk/resources/events.rb +18 -0
- data/lib/sevk/resources/segments.rb +8 -0
- data/lib/sevk/resources/topics.rb +12 -0
- data/lib/sevk/resources/webhooks.rb +38 -0
- data/lib/sevk/version.rb +1 -1
- data/lib/sevk.rb +46 -0
- data/sevk.gemspec +1 -0
- metadata +17 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 62e0a240f8ad60264efdd2a0384dbcd25eb7f35f6bd2ca608ffa0206e0f0b376
|
|
4
|
+
data.tar.gz: 6179e81e538df7a45124341b773df25999483de8a48b70fdb1b37f28a650c35d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 51ce594479adaaa2ee9aa02121eb8013251c8f02b23eca08b81bf4cf9441f5871faa6084d4c3e9808019fc84b4a24d279b8104163d4c07e929d54062f9d7f312
|
|
7
|
+
data.tar.gz: 4418c136224bf8452e88adef21165a28f81d8d72d0da38109fe0dcf36241b81578c848418a3fd7a08a4ef00d477ced5f6023b01638742d345abab17212c8ee69
|
data/README.md
CHANGED
data/lib/sevk/client.rb
CHANGED
|
@@ -45,6 +45,19 @@ module Sevk
|
|
|
45
45
|
@emails ||= Resources::Emails.new(self)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
def webhooks
|
|
49
|
+
@webhooks ||= Resources::Webhooks.new(self)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def events
|
|
53
|
+
@events ||= Resources::Events.new(self)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Get project usage and limits
|
|
57
|
+
def get_usage
|
|
58
|
+
get("/limits")
|
|
59
|
+
end
|
|
60
|
+
|
|
48
61
|
def get(path, params = {})
|
|
49
62
|
request(:get, path, params)
|
|
50
63
|
end
|
data/lib/sevk/markup/renderer.rb
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rouge"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
3
6
|
module Sevk
|
|
4
7
|
module Markup
|
|
5
8
|
# Font configuration
|
|
@@ -15,13 +18,15 @@ module Sevk
|
|
|
15
18
|
|
|
16
19
|
# Head settings for email generation
|
|
17
20
|
class EmailHeadSettings
|
|
18
|
-
attr_accessor :title, :preview_text, :styles, :fonts
|
|
21
|
+
attr_accessor :title, :preview_text, :styles, :fonts, :lang, :dir
|
|
19
22
|
|
|
20
23
|
def initialize
|
|
21
24
|
@title = ""
|
|
22
25
|
@preview_text = ""
|
|
23
26
|
@styles = ""
|
|
24
27
|
@fonts = []
|
|
28
|
+
@lang = ""
|
|
29
|
+
@dir = ""
|
|
25
30
|
end
|
|
26
31
|
end
|
|
27
32
|
|
|
@@ -46,17 +51,29 @@ module Sevk
|
|
|
46
51
|
# Parse head settings from markup
|
|
47
52
|
parse_head_settings(markup)
|
|
48
53
|
|
|
54
|
+
# Extract clean body content (strips <mail>/<head> wrapper tags)
|
|
55
|
+
body_content = extract_body_content(markup)
|
|
56
|
+
|
|
49
57
|
# Normalize markup
|
|
50
|
-
|
|
58
|
+
normalized = normalize_markup(body_content)
|
|
51
59
|
|
|
52
60
|
# Process markup using regex
|
|
53
|
-
processed = process_markup(
|
|
61
|
+
processed = process_markup(normalized)
|
|
54
62
|
|
|
55
63
|
generate_html(processed)
|
|
56
64
|
end
|
|
57
65
|
|
|
58
66
|
private
|
|
59
67
|
|
|
68
|
+
def extract_body_content(markup)
|
|
69
|
+
if markup.include?("<mail") || markup.include?("<email")
|
|
70
|
+
if (match = markup.match(/<body[^>]*>([\s\S]*?)<\/body>/i))
|
|
71
|
+
return match[1].strip
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
markup
|
|
75
|
+
end
|
|
76
|
+
|
|
60
77
|
def normalize_markup(content)
|
|
61
78
|
result = content
|
|
62
79
|
|
|
@@ -74,6 +91,17 @@ module Sevk
|
|
|
74
91
|
end
|
|
75
92
|
|
|
76
93
|
def parse_head_settings(markup)
|
|
94
|
+
# Parse lang and dir from <mail> or <email> root tag
|
|
95
|
+
if (root_match = markup.match(/<(?:email|mail)([^>]*)>/i))
|
|
96
|
+
root_attrs = root_match[1]
|
|
97
|
+
if (lang_match = root_attrs.match(/lang=["']([^"']*)["']/i))
|
|
98
|
+
@head_settings.lang = lang_match[1]
|
|
99
|
+
end
|
|
100
|
+
if (dir_match = root_attrs.match(/dir=["']([^"']*)["']/i))
|
|
101
|
+
@head_settings.dir = dir_match[1]
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
77
105
|
# Extract title
|
|
78
106
|
if (match = markup.match(/<title[^>]*>([\s\S]*?)<\/title>/i))
|
|
79
107
|
@head_settings.title = match[1].strip
|
|
@@ -98,6 +126,23 @@ module Sevk
|
|
|
98
126
|
def process_markup(content)
|
|
99
127
|
result = content
|
|
100
128
|
|
|
129
|
+
# Convert <link> to <sevk-link>
|
|
130
|
+
if result.include?("<link")
|
|
131
|
+
result = result.gsub(/<link\s+href=/i, "<sevk-link href=")
|
|
132
|
+
result = result.gsub("</link>", "</sevk-link>")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Process block tags (before other tags)
|
|
136
|
+
result = process_tag(result, "block") do |attrs, inner|
|
|
137
|
+
process_block_tag(attrs, inner)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Process self-closing block tags
|
|
141
|
+
result = result.gsub(/<block([^>]*)\/?\s*>/i) do
|
|
142
|
+
attrs = parse_attributes(Regexp.last_match(1) || "")
|
|
143
|
+
process_block_tag(attrs)
|
|
144
|
+
end
|
|
145
|
+
|
|
101
146
|
# Process section tags
|
|
102
147
|
result = process_tag(result, "section") do |attrs, inner|
|
|
103
148
|
style = extract_all_style_attributes(attrs)
|
|
@@ -111,32 +156,88 @@ module Sevk
|
|
|
111
156
|
</table>)
|
|
112
157
|
end
|
|
113
158
|
|
|
159
|
+
# Process column tags (before rows, so row can count them)
|
|
160
|
+
result = process_tag(result, "column") do |attrs, inner|
|
|
161
|
+
style = extract_all_style_attributes(attrs)
|
|
162
|
+
style["vertical-align"] ||= "top"
|
|
163
|
+
style_str = style_to_string(style)
|
|
164
|
+
%(<td class="sevk-column" style="#{style_str}">#{inner}</td>)
|
|
165
|
+
end
|
|
166
|
+
|
|
114
167
|
# Process row tags
|
|
168
|
+
row_counter = 0
|
|
115
169
|
result = process_tag(result, "row") do |attrs, inner|
|
|
170
|
+
gap = attrs["gap"] || "0"
|
|
116
171
|
style = extract_all_style_attributes(attrs)
|
|
172
|
+
style.delete("gap")
|
|
117
173
|
style_str = style_to_string(style)
|
|
118
|
-
|
|
174
|
+
gap_px = gap.gsub("px", "")
|
|
175
|
+
gap_num = gap_px.to_i
|
|
176
|
+
row_id = "sevk-row-#{row_counter}"
|
|
177
|
+
row_counter += 1
|
|
178
|
+
|
|
179
|
+
# Assign equal widths to columns if more than one
|
|
180
|
+
processed_inner = inner
|
|
181
|
+
column_count = inner.scan(/class="sevk-column"/).length
|
|
182
|
+
if column_count > 1
|
|
183
|
+
equal_width = "#{(100 / column_count).to_i}%"
|
|
184
|
+
processed_inner = processed_inner.gsub(/<td class="sevk-column" style="([^"]*)"/) do
|
|
185
|
+
existing_style = Regexp.last_match(1)
|
|
186
|
+
if existing_style.include?("width:")
|
|
187
|
+
%(<td class="sevk-column" style="#{existing_style}")
|
|
188
|
+
else
|
|
189
|
+
%(<td class="sevk-column" style="width:#{equal_width};#{existing_style}")
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
gap_style = gap_num > 0 ? %(<style>@media only screen and (max-width:479px){.#{row_id} > tbody > tr > td{margin-bottom:#{gap_px}px !important;padding-left:0 !important;padding-right:0 !important;}.#{row_id} > tbody > tr > td:last-child{margin-bottom:0 !important;}}</style>) : ""
|
|
195
|
+
%(#{gap_style}<table class="sevk-row-table #{row_id}" align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="#{style_str}">
|
|
119
196
|
<tbody style="width:100%">
|
|
120
|
-
<tr style="width:100%">#{
|
|
197
|
+
<tr style="width:100%">#{processed_inner}</tr>
|
|
121
198
|
</tbody>
|
|
122
199
|
</table>)
|
|
123
200
|
end
|
|
124
201
|
|
|
125
|
-
# Process column tags
|
|
126
|
-
result = process_tag(result, "column") do |attrs, inner|
|
|
127
|
-
style = extract_all_style_attributes(attrs)
|
|
128
|
-
style_str = style_to_string(style)
|
|
129
|
-
%(<td style="#{style_str}">#{inner}</td>)
|
|
130
|
-
end
|
|
131
|
-
|
|
132
202
|
# Process container tags
|
|
133
203
|
result = process_tag(result, "container") do |attrs, inner|
|
|
134
204
|
style = extract_all_style_attributes(attrs)
|
|
135
|
-
|
|
136
|
-
|
|
205
|
+
td_style = {}
|
|
206
|
+
table_style = {}
|
|
207
|
+
|
|
208
|
+
# Visual styles go on <td>, layout styles stay on <table>
|
|
209
|
+
visual_keys = %w[
|
|
210
|
+
background-color background-image background-size background-position background-repeat
|
|
211
|
+
border border-top border-right border-bottom border-left border-color border-width border-style
|
|
212
|
+
border-radius border-top-left-radius border-top-right-radius border-bottom-left-radius border-bottom-right-radius
|
|
213
|
+
padding padding-top padding-right padding-bottom padding-left
|
|
214
|
+
]
|
|
215
|
+
style.each do |key, value|
|
|
216
|
+
if visual_keys.include?(key)
|
|
217
|
+
td_style[key] = value
|
|
218
|
+
else
|
|
219
|
+
table_style[key] = value
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Add border-collapse: separate when border-radius is used
|
|
224
|
+
has_border_radius = td_style["border-radius"] || td_style["border-top-left-radius"] ||
|
|
225
|
+
td_style["border-top-right-radius"] || td_style["border-bottom-left-radius"] ||
|
|
226
|
+
td_style["border-bottom-right-radius"]
|
|
227
|
+
table_style["border-collapse"] = "separate" if has_border_radius
|
|
228
|
+
|
|
229
|
+
# Make fixed widths responsive: width becomes max-width, width set to 100%
|
|
230
|
+
if table_style["width"] && table_style["width"] != "100%" && table_style["width"] != "auto"
|
|
231
|
+
table_style["max-width"] ||= table_style["width"]
|
|
232
|
+
table_style["width"] = "100%"
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
table_style_str = style_to_string(table_style)
|
|
236
|
+
td_style_str = style_to_string(td_style)
|
|
237
|
+
%(<table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="#{table_style_str}">
|
|
137
238
|
<tbody>
|
|
138
239
|
<tr style="width:100%">
|
|
139
|
-
<td>#{inner}</td>
|
|
240
|
+
<td style="#{td_style_str}">#{inner}</td>
|
|
140
241
|
</tr>
|
|
141
242
|
</tbody>
|
|
142
243
|
</table>)
|
|
@@ -146,6 +247,7 @@ module Sevk
|
|
|
146
247
|
result = process_tag(result, "heading") do |attrs, inner|
|
|
147
248
|
level = attrs["level"] || "1"
|
|
148
249
|
style = extract_all_style_attributes(attrs)
|
|
250
|
+
style['margin'] ||= '0'
|
|
149
251
|
style_str = style_to_string(style)
|
|
150
252
|
%(<h#{level} style="#{style_str}">#{inner}</h#{level}>)
|
|
151
253
|
end
|
|
@@ -153,6 +255,7 @@ module Sevk
|
|
|
153
255
|
# Process paragraph tags
|
|
154
256
|
result = process_tag(result, "paragraph") do |attrs, inner|
|
|
155
257
|
style = extract_all_style_attributes(attrs)
|
|
258
|
+
style['margin'] ||= '0'
|
|
156
259
|
style_str = style_to_string(style)
|
|
157
260
|
%(<p style="#{style_str}">#{inner}</p>)
|
|
158
261
|
end
|
|
@@ -178,13 +281,15 @@ module Sevk
|
|
|
178
281
|
height = attrs["height"]
|
|
179
282
|
|
|
180
283
|
style = extract_all_style_attributes(attrs)
|
|
284
|
+
style["vertical-align"] ||= "middle"
|
|
285
|
+
style["max-width"] ||= "100%"
|
|
181
286
|
style["outline"] ||= "none"
|
|
182
287
|
style["border"] ||= "none"
|
|
183
288
|
style["text-decoration"] ||= "none"
|
|
184
289
|
|
|
185
290
|
style_str = style_to_string(style)
|
|
186
|
-
width_attr = width ? %( width="#{width}") : ""
|
|
187
|
-
height_attr = height ? %( height="#{height}") : ""
|
|
291
|
+
width_attr = width ? %( width="#{width.to_s.gsub('px', '')}") : ""
|
|
292
|
+
height_attr = height ? %( height="#{height.to_s.gsub('px', '')}") : ""
|
|
188
293
|
|
|
189
294
|
%(<img src="#{src}" alt="#{alt}"#{width_attr}#{height_attr} style="#{style_str}" />)
|
|
190
295
|
end
|
|
@@ -199,6 +304,9 @@ module Sevk
|
|
|
199
304
|
%(<hr style="#{style_str}"#{class_str} />)
|
|
200
305
|
end
|
|
201
306
|
|
|
307
|
+
# Remove stray </divider> closing tags
|
|
308
|
+
result = result.gsub(%r{</divider>}i, "")
|
|
309
|
+
|
|
202
310
|
# Process link tags
|
|
203
311
|
result = process_tag(result, "sevk-link") do |attrs, inner|
|
|
204
312
|
href = attrs["href"] || "#"
|
|
@@ -213,6 +321,7 @@ module Sevk
|
|
|
213
321
|
list_type = attrs["type"] || "unordered"
|
|
214
322
|
tag = list_type == "ordered" ? "ol" : "ul"
|
|
215
323
|
style = extract_all_style_attributes(attrs)
|
|
324
|
+
style['margin'] ||= '0'
|
|
216
325
|
style["list-style-type"] = attrs["list-style-type"] if attrs["list-style-type"]
|
|
217
326
|
style_str = style_to_string(style)
|
|
218
327
|
class_attr = attrs["class"] || attrs["className"]
|
|
@@ -231,12 +340,13 @@ module Sevk
|
|
|
231
340
|
|
|
232
341
|
# Process codeblock tags
|
|
233
342
|
result = process_tag(result, "codeblock") do |attrs, inner|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
343
|
+
process_codeblock(attrs, inner)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Clean up stray Sevk closing tags
|
|
347
|
+
stray_closing_tags = %w[container section row column heading paragraph text button sevk-link]
|
|
348
|
+
stray_closing_tags.each do |tag|
|
|
349
|
+
result = result.gsub(%r{</#{tag}>}i, "")
|
|
240
350
|
end
|
|
241
351
|
|
|
242
352
|
# Clean up wrapper tags
|
|
@@ -292,6 +402,280 @@ module Sevk
|
|
|
292
402
|
%(<a href="#{href}" target="_blank" style="#{style_str}"><!--[if mso]><i style="mso-font-width:#{(pl_font_width * 100).round}%;mso-text-raise:#{text_raise}" hidden>#{left_mso_spaces}</i><![endif]--><span style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:#{px_to_pt(padding_bottom)}">#{inner}</span><!--[if mso]><i style="mso-font-width:#{(pr_font_width * 100).round}%" hidden>#{right_mso_spaces}​</i><![endif]--></a>)
|
|
293
403
|
end
|
|
294
404
|
|
|
405
|
+
# One Dark theme token-to-inline-style mapping
|
|
406
|
+
ONE_DARK_THEME = {
|
|
407
|
+
"Comment" => { "color" => "#5c6370", "font-style" => "italic" },
|
|
408
|
+
"Comment.Single" => { "color" => "#5c6370", "font-style" => "italic" },
|
|
409
|
+
"Comment.Multiline" => { "color" => "#5c6370", "font-style" => "italic" },
|
|
410
|
+
"Comment.Preproc" => { "color" => "#5c6370", "font-style" => "italic" },
|
|
411
|
+
"Comment.Doc" => { "color" => "#5c6370", "font-style" => "italic" },
|
|
412
|
+
"Comment.Special" => { "color" => "#5c6370", "font-style" => "italic" },
|
|
413
|
+
"Keyword" => { "color" => "#c678dd" },
|
|
414
|
+
"Keyword.Constant" => { "color" => "#c678dd" },
|
|
415
|
+
"Keyword.Declaration" => { "color" => "#c678dd" },
|
|
416
|
+
"Keyword.Namespace" => { "color" => "#c678dd" },
|
|
417
|
+
"Keyword.Pseudo" => { "color" => "#c678dd" },
|
|
418
|
+
"Keyword.Reserved" => { "color" => "#c678dd" },
|
|
419
|
+
"Keyword.Type" => { "color" => "#c678dd" },
|
|
420
|
+
"Literal.String" => { "color" => "#98c379" },
|
|
421
|
+
"Literal.String.Affix" => { "color" => "#98c379" },
|
|
422
|
+
"Literal.String.Backtick" => { "color" => "#98c379" },
|
|
423
|
+
"Literal.String.Char" => { "color" => "#98c379" },
|
|
424
|
+
"Literal.String.Doc" => { "color" => "#98c379" },
|
|
425
|
+
"Literal.String.Double" => { "color" => "#98c379" },
|
|
426
|
+
"Literal.String.Escape" => { "color" => "#d19a66" },
|
|
427
|
+
"Literal.String.Heredoc" => { "color" => "#98c379" },
|
|
428
|
+
"Literal.String.Interpol" => { "color" => "#98c379" },
|
|
429
|
+
"Literal.String.Other" => { "color" => "#98c379" },
|
|
430
|
+
"Literal.String.Regex" => { "color" => "#98c379" },
|
|
431
|
+
"Literal.String.Single" => { "color" => "#98c379" },
|
|
432
|
+
"Literal.String.Symbol" => { "color" => "#e06c75" },
|
|
433
|
+
"Literal.Number" => { "color" => "#d19a66" },
|
|
434
|
+
"Literal.Number.Bin" => { "color" => "#d19a66" },
|
|
435
|
+
"Literal.Number.Float" => { "color" => "#d19a66" },
|
|
436
|
+
"Literal.Number.Hex" => { "color" => "#d19a66" },
|
|
437
|
+
"Literal.Number.Integer" => { "color" => "#d19a66" },
|
|
438
|
+
"Literal.Number.Oct" => { "color" => "#d19a66" },
|
|
439
|
+
"Name.Function" => { "color" => "#61afef" },
|
|
440
|
+
"Name.Function.Magic" => { "color" => "#61afef" },
|
|
441
|
+
"Name.Builtin" => { "color" => "#61afef" },
|
|
442
|
+
"Name.Builtin.Pseudo" => { "color" => "#e06c75" },
|
|
443
|
+
"Name.Variable" => { "color" => "#e06c75" },
|
|
444
|
+
"Name.Variable.Class" => { "color" => "#e06c75" },
|
|
445
|
+
"Name.Variable.Global" => { "color" => "#e06c75" },
|
|
446
|
+
"Name.Variable.Instance" => { "color" => "#e06c75" },
|
|
447
|
+
"Name.Variable.Magic" => { "color" => "#e06c75" },
|
|
448
|
+
"Name.Class" => { "color" => "#d19a66" },
|
|
449
|
+
"Name.Constant" => { "color" => "#d19a66" },
|
|
450
|
+
"Name.Decorator" => { "color" => "#61afef" },
|
|
451
|
+
"Name.Attribute" => { "color" => "#d19a66" },
|
|
452
|
+
"Name.Tag" => { "color" => "#e06c75" },
|
|
453
|
+
"Name.Namespace" => { "color" => "#d19a66" },
|
|
454
|
+
"Operator" => { "color" => "#61afef" },
|
|
455
|
+
"Operator.Word" => { "color" => "#c678dd" },
|
|
456
|
+
"Punctuation" => { "color" => "#abb2bf" },
|
|
457
|
+
"Generic.Deleted" => { "color" => "#e06c75" },
|
|
458
|
+
"Generic.Inserted" => { "color" => "#98c379" },
|
|
459
|
+
"Generic.Heading" => { "color" => "#61afef", "font-weight" => "bold" },
|
|
460
|
+
"Generic.Subheading" => { "color" => "#61afef" },
|
|
461
|
+
"Generic.Emph" => { "font-style" => "italic" },
|
|
462
|
+
"Generic.Strong" => { "font-weight" => "bold" },
|
|
463
|
+
}.freeze
|
|
464
|
+
|
|
465
|
+
def process_codeblock(attrs, inner)
|
|
466
|
+
language = attrs["language"] || "text"
|
|
467
|
+
custom_style = extract_all_style_attributes(attrs)
|
|
468
|
+
|
|
469
|
+
code = inner.strip
|
|
470
|
+
|
|
471
|
+
# Try to find a Rouge lexer for the language
|
|
472
|
+
lexer = begin
|
|
473
|
+
Rouge::Lexer.find(language)&.new
|
|
474
|
+
rescue StandardError
|
|
475
|
+
nil
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Base pre styles (One Dark)
|
|
479
|
+
base_style = {
|
|
480
|
+
"background-color" => "#282c34",
|
|
481
|
+
"color" => "#abb2bf",
|
|
482
|
+
"font-family" => "'Fira Code', 'Fira Mono', Menlo, Consolas, 'DejaVu Sans Mono', monospace",
|
|
483
|
+
"font-size" => "13px",
|
|
484
|
+
"direction" => "ltr",
|
|
485
|
+
"text-align" => "left",
|
|
486
|
+
"white-space" => "pre",
|
|
487
|
+
"word-spacing" => "normal",
|
|
488
|
+
"word-break" => "normal",
|
|
489
|
+
"line-height" => "1.5",
|
|
490
|
+
"tab-size" => "2",
|
|
491
|
+
"hyphens" => "none",
|
|
492
|
+
"padding" => "1em",
|
|
493
|
+
"margin" => "0.5em 0",
|
|
494
|
+
"overflow" => "auto",
|
|
495
|
+
"border-radius" => "0.3em",
|
|
496
|
+
"width" => "100%",
|
|
497
|
+
"box-sizing" => "border-box"
|
|
498
|
+
}
|
|
499
|
+
base_style.merge!(custom_style)
|
|
500
|
+
|
|
501
|
+
if lexer.nil?
|
|
502
|
+
# Fallback: plain pre/code with no highlighting
|
|
503
|
+
style_str = style_to_string(base_style)
|
|
504
|
+
escaped = code.gsub("&", "&").gsub("<", "<").gsub(">", ">")
|
|
505
|
+
return %(<pre style="#{style_str}"><code>#{escaped}</code></pre>)
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# Tokenize and highlight
|
|
509
|
+
tokens = lexer.lex(code)
|
|
510
|
+
lines = build_highlighted_lines(tokens)
|
|
511
|
+
|
|
512
|
+
lines_html = lines.map do |line_tokens|
|
|
513
|
+
spans = line_tokens.map { |tok_type, tok_text| render_rouge_token(tok_type, tok_text) }.join
|
|
514
|
+
%(<p style="margin:0;min-height:1em">#{spans}</p>)
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
style_str = style_to_string(base_style)
|
|
518
|
+
%(<pre style="#{style_str}"><code>#{lines_html.join}</code></pre>)
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def build_highlighted_lines(tokens)
|
|
522
|
+
lines = [[]]
|
|
523
|
+
tokens.each do |tok_type, tok_text|
|
|
524
|
+
parts = tok_text.split(/(\n)/, -1)
|
|
525
|
+
parts.each_with_index do |part, i|
|
|
526
|
+
if part == "\n"
|
|
527
|
+
lines << []
|
|
528
|
+
elsif !part.empty?
|
|
529
|
+
lines.last << [tok_type, part]
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
lines
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def render_rouge_token(tok_type, text)
|
|
537
|
+
escaped = text.gsub("&", "&").gsub("<", "<").gsub(">", ">")
|
|
538
|
+
# Replace spaces with non-breaking space + zero-width joiner + zero-width space for email safety
|
|
539
|
+
escaped = escaped.gsub(" ", "\u00A0\u200D\u200B")
|
|
540
|
+
|
|
541
|
+
styles = resolve_token_styles(tok_type)
|
|
542
|
+
if styles.empty?
|
|
543
|
+
escaped
|
|
544
|
+
else
|
|
545
|
+
style_str = styles.map { |k, v| "#{k}:#{v}" }.join(";")
|
|
546
|
+
%(<span style="#{style_str}">#{escaped}</span>)
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def resolve_token_styles(tok_type)
|
|
551
|
+
# Walk up the token hierarchy to find a matching theme entry
|
|
552
|
+
# e.g., Token::Literal::String::Double -> "Literal.String.Double" -> "Literal.String" -> ...
|
|
553
|
+
qualname = tok_type.qualname
|
|
554
|
+
loop do
|
|
555
|
+
return ONE_DARK_THEME[qualname].dup if ONE_DARK_THEME.key?(qualname)
|
|
556
|
+
|
|
557
|
+
dot_index = qualname.rindex(".")
|
|
558
|
+
break unless dot_index
|
|
559
|
+
|
|
560
|
+
qualname = qualname[0...dot_index]
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
# Check base token name
|
|
564
|
+
return ONE_DARK_THEME[qualname].dup if ONE_DARK_THEME.key?(qualname)
|
|
565
|
+
|
|
566
|
+
{}
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def truthy?(val)
|
|
570
|
+
return false if val.nil?
|
|
571
|
+
return false if val == false
|
|
572
|
+
return false if val == 0
|
|
573
|
+
return false if val.is_a?(String) && val.empty?
|
|
574
|
+
return false if val.is_a?(Array) && val.empty?
|
|
575
|
+
|
|
576
|
+
true
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
def evaluate_condition(expr, config)
|
|
580
|
+
trimmed = expr.strip
|
|
581
|
+
|
|
582
|
+
# OR: split on ||, return true if any part is true
|
|
583
|
+
if trimmed.include?('||')
|
|
584
|
+
return trimmed.split('||').any? { |part| evaluate_condition(part, config) }
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
# AND: split on &&, return true if all parts are true
|
|
588
|
+
if trimmed.include?('&&')
|
|
589
|
+
return trimmed.split('&&').all? { |part| evaluate_condition(part, config) }
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
# Equality: key == "value"
|
|
593
|
+
if (eq_match = trimmed.match(/^(\w+)\s*==\s*"([^"]*)"$/))
|
|
594
|
+
return (config[eq_match[1]] || '').to_s == eq_match[2]
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
# Inequality: key != "value"
|
|
598
|
+
if (neq_match = trimmed.match(/^(\w+)\s*!=\s*"([^"]*)"$/))
|
|
599
|
+
return (config[neq_match[1]] || '').to_s != neq_match[2]
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
# Simple truthy check
|
|
603
|
+
truthy?(config[trimmed])
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
def render_template(template, config)
|
|
607
|
+
result = template.dup
|
|
608
|
+
|
|
609
|
+
# Process #each loops
|
|
610
|
+
result = result.gsub(/\{%#each\s+(\w+)\s+as\s+(\w+)%\}([\s\S]*?)\{%\/each%\}/) do
|
|
611
|
+
key = Regexp.last_match(1)
|
|
612
|
+
alias_name = Regexp.last_match(2)
|
|
613
|
+
body = Regexp.last_match(3)
|
|
614
|
+
arr = config[key]
|
|
615
|
+
if arr.is_a?(Array)
|
|
616
|
+
arr.map do |item|
|
|
617
|
+
iteration = body.dup
|
|
618
|
+
if item.is_a?(Hash)
|
|
619
|
+
item.each do |prop, val|
|
|
620
|
+
iteration = iteration.gsub(/\{%#{Regexp.escape(alias_name)}\.#{Regexp.escape(prop)}(?:\s*\?\?\s*([^%]*?))?\s*%\}/) do
|
|
621
|
+
fallback = Regexp.last_match(1)
|
|
622
|
+
truthy?(val) ? val.to_s : (fallback ? fallback.strip : '')
|
|
623
|
+
end
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
# Replace bare alias reference
|
|
627
|
+
iteration = iteration.gsub(/\{%#{Regexp.escape(alias_name)}(?:\s*\?\?\s*([^%]*?))?\s*%\}/) do
|
|
628
|
+
fallback = Regexp.last_match(1)
|
|
629
|
+
truthy?(item) ? item.to_s : (fallback ? fallback.strip : '')
|
|
630
|
+
end
|
|
631
|
+
iteration
|
|
632
|
+
end.join
|
|
633
|
+
else
|
|
634
|
+
''
|
|
635
|
+
end
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
# Process nested #if conditionals (innermost first)
|
|
639
|
+
loop do
|
|
640
|
+
matched = false
|
|
641
|
+
result = result.gsub(/\{%#if\s+([^%]+)%\}((?:(?!\{%#if\s)[\s\S])*?)\{%\/if%\}/) do
|
|
642
|
+
matched = true
|
|
643
|
+
condition = Regexp.last_match(1)
|
|
644
|
+
body = Regexp.last_match(2)
|
|
645
|
+
if body.include?('{%else%}')
|
|
646
|
+
parts = body.split('{%else%}', 2)
|
|
647
|
+
evaluate_condition(condition, config) ? parts[0] : parts[1]
|
|
648
|
+
else
|
|
649
|
+
evaluate_condition(condition, config) ? body : ''
|
|
650
|
+
end
|
|
651
|
+
end
|
|
652
|
+
break unless matched
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
# Process variable injection with fallback
|
|
656
|
+
result = result.gsub(/\{%(\w+)(?:\s*\?\?\s*([^%]*?))?\s*%\}/) do
|
|
657
|
+
key = Regexp.last_match(1)
|
|
658
|
+
fallback = Regexp.last_match(2)
|
|
659
|
+
val = config[key]
|
|
660
|
+
truthy?(val) ? val.to_s : (fallback ? fallback.strip : '')
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
result
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
def process_block_tag(attrs, inner = '')
|
|
667
|
+
template = inner.strip.empty? ? (attrs['template'] || '') : inner.strip
|
|
668
|
+
return '' if template.empty?
|
|
669
|
+
|
|
670
|
+
config_str = (attrs['config'] || '{}').gsub("'", '"').gsub('"', '"').gsub('&', '&')
|
|
671
|
+
config = begin
|
|
672
|
+
JSON.parse(config_str)
|
|
673
|
+
rescue StandardError
|
|
674
|
+
{}
|
|
675
|
+
end
|
|
676
|
+
render_template(template, config)
|
|
677
|
+
end
|
|
678
|
+
|
|
295
679
|
def parse_padding(style)
|
|
296
680
|
if style["padding"]
|
|
297
681
|
parts = style["padding"].split
|
|
@@ -369,14 +753,15 @@ module Sevk
|
|
|
369
753
|
inner_start = match.end(0)
|
|
370
754
|
attrs_str = match[1]
|
|
371
755
|
|
|
372
|
-
# Find the next close tag after this opening tag
|
|
373
|
-
|
|
374
|
-
next unless
|
|
756
|
+
# Find the next close tag after this opening tag (case-insensitive)
|
|
757
|
+
close_match = result.match(Regexp.new("<\\/#{tag_name}>", Regexp::IGNORECASE), inner_start)
|
|
758
|
+
next unless close_match
|
|
375
759
|
|
|
760
|
+
close_pos = close_match.begin(0)
|
|
376
761
|
inner = result[inner_start...close_pos]
|
|
377
762
|
|
|
378
|
-
# Check if there's another opening tag inside
|
|
379
|
-
next if inner.
|
|
763
|
+
# Check if there's another opening tag inside (case-insensitive)
|
|
764
|
+
next if inner.match?(Regexp.new("<#{tag_name}", Regexp::IGNORECASE))
|
|
380
765
|
|
|
381
766
|
# This is an innermost tag, process it
|
|
382
767
|
attrs = parse_attributes(attrs_str)
|
|
@@ -396,8 +781,8 @@ module Sevk
|
|
|
396
781
|
|
|
397
782
|
def parse_attributes(attrs_str)
|
|
398
783
|
attrs = {}
|
|
399
|
-
attrs_str.scan(/([\w-]+)=["'
|
|
400
|
-
attrs[key] =
|
|
784
|
+
attrs_str.scan(/([\w-]+)=(?:"([^"]*)"|'([^']*)')/) do |key, dq_val, sq_val|
|
|
785
|
+
attrs[key] = dq_val.nil? || dq_val.empty? ? (sq_val || '') : dq_val
|
|
401
786
|
end
|
|
402
787
|
attrs
|
|
403
788
|
end
|
|
@@ -423,6 +808,8 @@ module Sevk
|
|
|
423
808
|
style["width"] = attrs["width"] if attrs["width"]
|
|
424
809
|
style["height"] = attrs["height"] if attrs["height"]
|
|
425
810
|
style["max-width"] = attrs["max-width"] if attrs["max-width"]
|
|
811
|
+
style["max-height"] = attrs["max-height"] if attrs["max-height"]
|
|
812
|
+
style["min-width"] = attrs["min-width"] if attrs["min-width"]
|
|
426
813
|
style["min-height"] = attrs["min-height"] if attrs["min-height"]
|
|
427
814
|
|
|
428
815
|
# Spacing - Padding
|
|
@@ -468,6 +855,18 @@ module Sevk
|
|
|
468
855
|
style["border-bottom-right-radius"] = attrs["border-bottom-right-radius"] if attrs["border-bottom-right-radius"]
|
|
469
856
|
end
|
|
470
857
|
|
|
858
|
+
# Background image
|
|
859
|
+
if attrs["background-image"]
|
|
860
|
+
style["background-image"] = "url('#{attrs["background-image"]}')"
|
|
861
|
+
style["background-size"] = attrs["background-size"] || "cover"
|
|
862
|
+
style["background-position"] = attrs["background-position"] || "center"
|
|
863
|
+
style["background-repeat"] = attrs["background-repeat"] || "no-repeat"
|
|
864
|
+
else
|
|
865
|
+
style["background-size"] = attrs["background-size"] if attrs["background-size"]
|
|
866
|
+
style["background-position"] = attrs["background-position"] if attrs["background-position"]
|
|
867
|
+
style["background-repeat"] = attrs["background-repeat"] if attrs["background-repeat"]
|
|
868
|
+
end
|
|
869
|
+
|
|
471
870
|
style
|
|
472
871
|
end
|
|
473
872
|
|
|
@@ -476,6 +875,9 @@ module Sevk
|
|
|
476
875
|
end
|
|
477
876
|
|
|
478
877
|
def generate_html(content)
|
|
878
|
+
lang = @head_settings.lang.empty? ? "en" : @head_settings.lang
|
|
879
|
+
dir = @head_settings.dir.empty? ? "ltr" : @head_settings.dir
|
|
880
|
+
|
|
479
881
|
title = @head_settings.title.empty? ? "" : "<title>#{@head_settings.title}</title>"
|
|
480
882
|
|
|
481
883
|
font_links = @head_settings.fonts.map do |font|
|
|
@@ -491,17 +893,43 @@ module Sevk
|
|
|
491
893
|
|
|
492
894
|
<<~HTML
|
|
493
895
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
494
|
-
<html lang="
|
|
896
|
+
<html lang="#{lang}" dir="#{dir}" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
|
495
897
|
<head>
|
|
496
898
|
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
|
|
497
899
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
900
|
+
<meta name="x-apple-disable-message-reformatting"/>
|
|
901
|
+
<meta content="IE=edge" http-equiv="X-UA-Compatible"/>
|
|
902
|
+
<meta name="format-detection" content="telephone=no,address=no,email=no,date=no,url=no"/>
|
|
903
|
+
<!--[if mso]>
|
|
904
|
+
<noscript>
|
|
905
|
+
<xml>
|
|
906
|
+
<o:OfficeDocumentSettings>
|
|
907
|
+
<o:AllowPNG/>
|
|
908
|
+
<o:PixelsPerInch>96</o:PixelsPerInch>
|
|
909
|
+
</o:OfficeDocumentSettings>
|
|
910
|
+
</xml>
|
|
911
|
+
</noscript>
|
|
912
|
+
<![endif]-->
|
|
913
|
+
<style type="text/css">
|
|
914
|
+
#outlook a { padding: 0; }
|
|
915
|
+
body { margin: 0; padding: 0; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
|
|
916
|
+
table, td { border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
|
|
917
|
+
.sevk-row-table { border-collapse: separate !important; }
|
|
918
|
+
img { border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; }
|
|
919
|
+
@media only screen and (max-width: 479px) {
|
|
920
|
+
.sevk-row-table { width: 100% !important; }
|
|
921
|
+
.sevk-column { display: block !important; width: 100% !important; max-width: 100% !important; }
|
|
922
|
+
}
|
|
923
|
+
</style>
|
|
498
924
|
#{title}
|
|
499
925
|
#{font_links}
|
|
500
926
|
#{styles}
|
|
501
927
|
</head>
|
|
502
|
-
<body style="margin:0;padding:0;font-family:ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
928
|
+
<body style="margin:0;padding:0;word-spacing:normal;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;font-family:ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;">
|
|
929
|
+
<div aria-roledescription="email" role="article">
|
|
503
930
|
#{preview_text}
|
|
504
931
|
#{content}
|
|
932
|
+
</div>
|
|
505
933
|
</body>
|
|
506
934
|
</html>
|
|
507
935
|
HTML
|
|
@@ -36,6 +36,14 @@ module Sevk
|
|
|
36
36
|
def add_contacts(audience_id, contact_ids)
|
|
37
37
|
client.post("/audiences/#{audience_id}/contacts", { contactIds: contact_ids })
|
|
38
38
|
end
|
|
39
|
+
|
|
40
|
+
def list_contacts(audience_id, params = {})
|
|
41
|
+
client.get("/audiences/#{audience_id}/contacts#{build_query_string(params)}")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def remove_contact(audience_id, contact_id)
|
|
45
|
+
client.delete("/audiences/#{audience_id}/contacts/#{contact_id}")
|
|
46
|
+
end
|
|
39
47
|
end
|
|
40
48
|
end
|
|
41
49
|
end
|
|
@@ -13,6 +13,50 @@ module Sevk
|
|
|
13
13
|
def get(id)
|
|
14
14
|
client.get("/broadcasts/#{id}")
|
|
15
15
|
end
|
|
16
|
+
|
|
17
|
+
def create(params)
|
|
18
|
+
client.post("/broadcasts", params)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def update(id, params)
|
|
22
|
+
client.put("/broadcasts/#{id}", params)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def delete(id)
|
|
26
|
+
client.delete("/broadcasts/#{id}")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def send(id)
|
|
30
|
+
client.post("/broadcasts/#{id}/send")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def cancel(id)
|
|
34
|
+
client.post("/broadcasts/#{id}/cancel")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def send_test(id, params)
|
|
38
|
+
client.post("/broadcasts/#{id}/test", params)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def get_analytics(id)
|
|
42
|
+
client.get("/broadcasts/#{id}/analytics")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def get_status(id)
|
|
46
|
+
client.get("/broadcasts/#{id}/status")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def get_emails(id, params = {})
|
|
50
|
+
client.get("/broadcasts/#{id}/emails#{build_query_string(params)}")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def estimate_cost(id)
|
|
54
|
+
client.get("/broadcasts/#{id}/estimate-cost")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def list_active
|
|
58
|
+
client.get("/broadcasts/active")
|
|
59
|
+
end
|
|
16
60
|
end
|
|
17
61
|
end
|
|
18
62
|
end
|
|
@@ -35,6 +35,18 @@ module Sevk
|
|
|
35
35
|
def delete(id)
|
|
36
36
|
client.delete("/contacts/#{id}")
|
|
37
37
|
end
|
|
38
|
+
|
|
39
|
+
def bulk_update(updates)
|
|
40
|
+
client.put("/contacts/bulk-update", updates)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def import_csv(params)
|
|
44
|
+
client.post("/contacts/import", params)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def get_events(id)
|
|
48
|
+
client.get("/contacts/#{id}/events")
|
|
49
|
+
end
|
|
38
50
|
end
|
|
39
51
|
end
|
|
40
52
|
end
|
|
@@ -13,6 +13,30 @@ module Sevk
|
|
|
13
13
|
def get(id)
|
|
14
14
|
client.get("/domains/#{id}")
|
|
15
15
|
end
|
|
16
|
+
|
|
17
|
+
def create(params)
|
|
18
|
+
client.post("/domains", params)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def update(id, params)
|
|
22
|
+
client.put("/domains/#{id}", params)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def delete(id)
|
|
26
|
+
client.delete("/domains/#{id}")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def verify(id)
|
|
30
|
+
client.post("/domains/#{id}/verify", {})
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def get_dns_records(id)
|
|
34
|
+
client.get("/domains/#{id}/dns-records")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def get_regions
|
|
38
|
+
client.get("/domains/regions")
|
|
39
|
+
end
|
|
16
40
|
end
|
|
17
41
|
end
|
|
18
42
|
end
|
|
@@ -5,7 +5,21 @@ require_relative "base"
|
|
|
5
5
|
module Sevk
|
|
6
6
|
module Resources
|
|
7
7
|
class Emails < Base
|
|
8
|
-
|
|
8
|
+
# Send an email with optional attachments
|
|
9
|
+
#
|
|
10
|
+
# @param to [String, Array<String>] Recipient email(s)
|
|
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
23
|
body = {
|
|
10
24
|
to: to,
|
|
11
25
|
subject: subject,
|
|
@@ -17,8 +31,25 @@ module Sevk
|
|
|
17
31
|
body[:text] = text if text
|
|
18
32
|
body[:headers] = headers if headers
|
|
19
33
|
body[:tags] = tags if tags
|
|
34
|
+
body[:attachments] = attachments if attachments
|
|
20
35
|
client.post("emails", body)
|
|
21
36
|
end
|
|
37
|
+
|
|
38
|
+
# Send multiple emails in bulk
|
|
39
|
+
#
|
|
40
|
+
# @param emails [Array<Hash>] Array of email parameter hashes (max 100)
|
|
41
|
+
# @return [Hash] Response with :success, :failed, :ids, :errors
|
|
42
|
+
def send_bulk(emails)
|
|
43
|
+
client.post("emails/bulk", { emails: emails })
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Get an email by ID
|
|
47
|
+
#
|
|
48
|
+
# @param id [String] Email ID
|
|
49
|
+
# @return [Hash] Email details
|
|
50
|
+
def get(id)
|
|
51
|
+
client.get("/emails/#{id}")
|
|
52
|
+
end
|
|
22
53
|
end
|
|
23
54
|
end
|
|
24
55
|
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Sevk
|
|
6
|
+
module Resources
|
|
7
|
+
class Events < Base
|
|
8
|
+
def list(page: nil, limit: nil, type: nil, contact_id: nil)
|
|
9
|
+
params = { page: page, limit: limit, type: type, contact_id: contact_id }
|
|
10
|
+
client.get("/events#{build_query_string(params)}")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def stats
|
|
14
|
+
client.get("/events/stats")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -30,6 +30,14 @@ module Sevk
|
|
|
30
30
|
def delete(audience_id, segment_id)
|
|
31
31
|
client.delete("/audiences/#{audience_id}/segments/#{segment_id}")
|
|
32
32
|
end
|
|
33
|
+
|
|
34
|
+
def calculate(audience_id, segment_id)
|
|
35
|
+
client.get("/audiences/#{audience_id}/segments/#{segment_id}/calculate")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def preview(audience_id, data)
|
|
39
|
+
client.post("/audiences/#{audience_id}/segments/preview", data)
|
|
40
|
+
end
|
|
33
41
|
end
|
|
34
42
|
end
|
|
35
43
|
end
|
|
@@ -30,6 +30,18 @@ module Sevk
|
|
|
30
30
|
def delete(audience_id, topic_id)
|
|
31
31
|
client.delete("/audiences/#{audience_id}/topics/#{topic_id}")
|
|
32
32
|
end
|
|
33
|
+
|
|
34
|
+
def add_contacts(audience_id, topic_id, contact_ids:)
|
|
35
|
+
client.post("/audiences/#{audience_id}/topics/#{topic_id}/contacts", { contactIds: contact_ids })
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def remove_contact(audience_id, topic_id, contact_id)
|
|
39
|
+
client.delete("/audiences/#{audience_id}/topics/#{topic_id}/contacts/#{contact_id}")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def list_contacts(audience_id, topic_id, params = {})
|
|
43
|
+
client.get("/audiences/#{audience_id}/topics/#{topic_id}/contacts#{build_query_string(params)}")
|
|
44
|
+
end
|
|
33
45
|
end
|
|
34
46
|
end
|
|
35
47
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Sevk
|
|
6
|
+
module Resources
|
|
7
|
+
class Webhooks < Base
|
|
8
|
+
def list(page: nil, limit: nil)
|
|
9
|
+
params = { page: page, limit: limit }
|
|
10
|
+
client.get("/webhooks#{build_query_string(params)}")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def get(id)
|
|
14
|
+
client.get("/webhooks/#{id}")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def create(params)
|
|
18
|
+
client.post("/webhooks", params)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def update(id, params)
|
|
22
|
+
client.put("/webhooks/#{id}", params)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def delete(id)
|
|
26
|
+
client.delete("/webhooks/#{id}")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def test(id)
|
|
30
|
+
client.post("/webhooks/#{id}/test", {})
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def list_events
|
|
34
|
+
client.get("/webhooks/events")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/sevk/version.rb
CHANGED
data/lib/sevk.rb
CHANGED
|
@@ -17,6 +17,8 @@ require_relative "sevk/resources/topics"
|
|
|
17
17
|
require_relative "sevk/resources/segments"
|
|
18
18
|
require_relative "sevk/resources/subscriptions"
|
|
19
19
|
require_relative "sevk/resources/emails"
|
|
20
|
+
require_relative "sevk/resources/webhooks"
|
|
21
|
+
require_relative "sevk/resources/events"
|
|
20
22
|
require_relative "sevk/markup/renderer"
|
|
21
23
|
|
|
22
24
|
module Sevk
|
|
@@ -210,4 +212,48 @@ module Sevk
|
|
|
210
212
|
end
|
|
211
213
|
end
|
|
212
214
|
end
|
|
215
|
+
|
|
216
|
+
module Webhooks
|
|
217
|
+
class << self
|
|
218
|
+
def list(params = {})
|
|
219
|
+
Sevk.client.webhooks.list(**params)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def get(id)
|
|
223
|
+
Sevk.client.webhooks.get(id)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def create(params)
|
|
227
|
+
Sevk.client.webhooks.create(params)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def update(id, params)
|
|
231
|
+
Sevk.client.webhooks.update(id, params)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def delete(id)
|
|
235
|
+
Sevk.client.webhooks.delete(id)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def test(id)
|
|
239
|
+
Sevk.client.webhooks.test(id)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def list_events
|
|
243
|
+
Sevk.client.webhooks.list_events
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
module Events
|
|
249
|
+
class << self
|
|
250
|
+
def list(params = {})
|
|
251
|
+
Sevk.client.events.list(**params)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def stats
|
|
255
|
+
Sevk.client.events.stats
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
213
259
|
end
|
data/sevk.gemspec
CHANGED
|
@@ -31,6 +31,7 @@ Gem::Specification.new do |spec|
|
|
|
31
31
|
spec.add_dependency "faraday", "~> 2.0"
|
|
32
32
|
spec.add_dependency "faraday-retry", "~> 2.0"
|
|
33
33
|
spec.add_dependency "nokogiri", "~> 1.15"
|
|
34
|
+
spec.add_dependency "rouge", "~> 4.0"
|
|
34
35
|
|
|
35
36
|
spec.add_development_dependency "bundler", "~> 2.0"
|
|
36
37
|
spec.add_development_dependency "rake", "~> 13.0"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sevk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sevk
|
|
@@ -51,6 +51,20 @@ dependencies:
|
|
|
51
51
|
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '1.15'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rouge
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '4.0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '4.0'
|
|
54
68
|
- !ruby/object:Gem::Dependency
|
|
55
69
|
name: bundler
|
|
56
70
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -141,10 +155,12 @@ files:
|
|
|
141
155
|
- lib/sevk/resources/contacts.rb
|
|
142
156
|
- lib/sevk/resources/domains.rb
|
|
143
157
|
- lib/sevk/resources/emails.rb
|
|
158
|
+
- lib/sevk/resources/events.rb
|
|
144
159
|
- lib/sevk/resources/segments.rb
|
|
145
160
|
- lib/sevk/resources/subscriptions.rb
|
|
146
161
|
- lib/sevk/resources/templates.rb
|
|
147
162
|
- lib/sevk/resources/topics.rb
|
|
163
|
+
- lib/sevk/resources/webhooks.rb
|
|
148
164
|
- lib/sevk/version.rb
|
|
149
165
|
- sevk.gemspec
|
|
150
166
|
homepage: https://sevk.io
|