sevk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,516 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sevk
4
+ module Markup
5
+ # Font configuration
6
+ class FontConfig
7
+ attr_accessor :id, :name, :url
8
+
9
+ def initialize(id: "", name: "", url: "")
10
+ @id = id
11
+ @name = name
12
+ @url = url
13
+ end
14
+ end
15
+
16
+ # Head settings for email generation
17
+ class EmailHeadSettings
18
+ attr_accessor :title, :preview_text, :styles, :fonts
19
+
20
+ def initialize
21
+ @title = ""
22
+ @preview_text = ""
23
+ @styles = ""
24
+ @fonts = []
25
+ end
26
+ end
27
+
28
+ # Parsed email content
29
+ class ParsedEmailContent
30
+ attr_accessor :body, :head_settings
31
+
32
+ def initialize
33
+ @body = ""
34
+ @head_settings = EmailHeadSettings.new
35
+ end
36
+ end
37
+
38
+ # Sevk Markup Renderer
39
+ # Converts Sevk markup to email-compatible HTML using regex-based parsing (like Node.js)
40
+ class Renderer
41
+ def initialize
42
+ @head_settings = EmailHeadSettings.new
43
+ end
44
+
45
+ def render(markup)
46
+ # Parse head settings from markup
47
+ parse_head_settings(markup)
48
+
49
+ # Normalize markup
50
+ markup = normalize_markup(markup)
51
+
52
+ # Process markup using regex
53
+ processed = process_markup(markup)
54
+
55
+ generate_html(processed)
56
+ end
57
+
58
+ private
59
+
60
+ def normalize_markup(content)
61
+ result = content
62
+
63
+ # Replace <link> with <sevk-link>
64
+ if result.include?("<link")
65
+ result = result.gsub(/<link\s+href=/i, "<sevk-link href=")
66
+ result = result.gsub("</link>", "</sevk-link>")
67
+ end
68
+
69
+ unless result.include?("<sevk-email") || result.include?("<email") || result.include?("<mail")
70
+ result = "<mail><body>#{result}</body></mail>"
71
+ end
72
+
73
+ result
74
+ end
75
+
76
+ def parse_head_settings(markup)
77
+ # Extract title
78
+ if (match = markup.match(/<title[^>]*>([\s\S]*?)<\/title>/i))
79
+ @head_settings.title = match[1].strip
80
+ end
81
+
82
+ # Extract preview
83
+ if (match = markup.match(/<preview[^>]*>([\s\S]*?)<\/preview>/i))
84
+ @head_settings.preview_text = match[1].strip
85
+ end
86
+
87
+ # Extract styles
88
+ if (match = markup.match(/<style[^>]*>([\s\S]*?)<\/style>/i))
89
+ @head_settings.styles = match[1].strip
90
+ end
91
+
92
+ # Extract fonts
93
+ markup.scan(/<font[^>]*name=["']([^"']*)["'][^>]*url=["']([^"']*)["'][^>]*\/?>/i).each_with_index do |(name, url), i|
94
+ @head_settings.fonts << FontConfig.new(id: "font-#{i}", name: name, url: url)
95
+ end
96
+ end
97
+
98
+ def process_markup(content)
99
+ result = content
100
+
101
+ # Process section tags
102
+ result = process_tag(result, "section") do |attrs, inner|
103
+ style = extract_all_style_attributes(attrs)
104
+ style_str = style_to_string(style)
105
+ %(<table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="#{style_str}">
106
+ <tbody>
107
+ <tr>
108
+ <td>#{inner}</td>
109
+ </tr>
110
+ </tbody>
111
+ </table>)
112
+ end
113
+
114
+ # Process row tags
115
+ result = process_tag(result, "row") do |attrs, inner|
116
+ style = extract_all_style_attributes(attrs)
117
+ style_str = style_to_string(style)
118
+ %(<table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="#{style_str}">
119
+ <tbody style="width:100%">
120
+ <tr style="width:100%">#{inner}</tr>
121
+ </tbody>
122
+ </table>)
123
+ end
124
+
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
+ # Process container tags
133
+ result = process_tag(result, "container") do |attrs, inner|
134
+ style = extract_all_style_attributes(attrs)
135
+ style_str = style_to_string(style)
136
+ %(<table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="#{style_str}">
137
+ <tbody>
138
+ <tr style="width:100%">
139
+ <td>#{inner}</td>
140
+ </tr>
141
+ </tbody>
142
+ </table>)
143
+ end
144
+
145
+ # Process heading tags
146
+ result = process_tag(result, "heading") do |attrs, inner|
147
+ level = attrs["level"] || "1"
148
+ style = extract_all_style_attributes(attrs)
149
+ style_str = style_to_string(style)
150
+ %(<h#{level} style="#{style_str}">#{inner}</h#{level}>)
151
+ end
152
+
153
+ # Process paragraph tags
154
+ result = process_tag(result, "paragraph") do |attrs, inner|
155
+ style = extract_all_style_attributes(attrs)
156
+ style_str = style_to_string(style)
157
+ %(<p style="#{style_str}">#{inner}</p>)
158
+ end
159
+
160
+ # Process text tags
161
+ result = process_tag(result, "text") do |attrs, inner|
162
+ style = extract_all_style_attributes(attrs)
163
+ style_str = style_to_string(style)
164
+ %(<span style="#{style_str}">#{inner}</span>)
165
+ end
166
+
167
+ # Process button tags with MSO compatibility
168
+ result = process_tag(result, "button") do |attrs, inner|
169
+ process_button(attrs, inner)
170
+ end
171
+
172
+ # Process image tags
173
+ result = result.gsub(/<image([^>]*)\/?>/i) do
174
+ attrs = parse_attributes(Regexp.last_match(1) || "")
175
+ src = attrs["src"] || ""
176
+ alt = attrs["alt"] || ""
177
+ width = attrs["width"]
178
+ height = attrs["height"]
179
+
180
+ style = extract_all_style_attributes(attrs)
181
+ style["outline"] ||= "none"
182
+ style["border"] ||= "none"
183
+ style["text-decoration"] ||= "none"
184
+
185
+ style_str = style_to_string(style)
186
+ width_attr = width ? %( width="#{width}") : ""
187
+ height_attr = height ? %( height="#{height}") : ""
188
+
189
+ %(<img src="#{src}" alt="#{alt}"#{width_attr}#{height_attr} style="#{style_str}" />)
190
+ end
191
+
192
+ # Process divider tags
193
+ result = result.gsub(/<divider([^>]*)\/?>/i) do
194
+ attrs = parse_attributes(Regexp.last_match(1) || "")
195
+ style = extract_all_style_attributes(attrs)
196
+ style_str = style_to_string(style)
197
+ class_attr = attrs["class"] || attrs["className"]
198
+ class_str = class_attr ? %( class="#{class_attr}") : ""
199
+ %(<hr style="#{style_str}"#{class_str} />)
200
+ end
201
+
202
+ # Process link tags
203
+ result = process_tag(result, "sevk-link") do |attrs, inner|
204
+ href = attrs["href"] || "#"
205
+ target = attrs["target"] || "_blank"
206
+ style = extract_all_style_attributes(attrs)
207
+ style_str = style_to_string(style)
208
+ %(<a href="#{href}" target="#{target}" style="#{style_str}">#{inner}</a>)
209
+ end
210
+
211
+ # Process list tags
212
+ result = process_tag(result, "list") do |attrs, inner|
213
+ list_type = attrs["type"] || "unordered"
214
+ tag = list_type == "ordered" ? "ol" : "ul"
215
+ style = extract_all_style_attributes(attrs)
216
+ style["list-style-type"] = attrs["list-style-type"] if attrs["list-style-type"]
217
+ style_str = style_to_string(style)
218
+ class_attr = attrs["class"] || attrs["className"]
219
+ class_str = class_attr ? %( class="#{class_attr}") : ""
220
+ %(<#{tag} style="#{style_str}"#{class_str}>#{inner}</#{tag}>)
221
+ end
222
+
223
+ # Process list item tags
224
+ result = process_tag(result, "li") do |attrs, inner|
225
+ style = extract_all_style_attributes(attrs)
226
+ style_str = style_to_string(style)
227
+ class_attr = attrs["class"] || attrs["className"]
228
+ class_str = class_attr ? %( class="#{class_attr}") : ""
229
+ %(<li style="#{style_str}"#{class_str}>#{inner}</li>)
230
+ end
231
+
232
+ # Process codeblock tags
233
+ result = process_tag(result, "codeblock") do |attrs, inner|
234
+ style = extract_all_style_attributes(attrs)
235
+ style["width"] ||= "100%"
236
+ style["box-sizing"] ||= "border-box"
237
+ style_str = style_to_string(style)
238
+ escaped = inner.gsub("<", "&lt;").gsub(">", "&gt;")
239
+ %(<pre style="#{style_str}"><code>#{escaped}</code></pre>)
240
+ end
241
+
242
+ # Clean up wrapper tags
243
+ wrapper_patterns = [
244
+ /<sevk-email[^>]*>/i, /<\/sevk-email>/i,
245
+ /<sevk-body[^>]*>/i, /<\/sevk-body>/i,
246
+ /<email[^>]*>/i, /<\/email>/i,
247
+ /<mail[^>]*>/i, /<\/mail>/i,
248
+ /<body[^>]*>/i, /<\/body>/i
249
+ ]
250
+ wrapper_patterns.each do |pattern|
251
+ result = result.gsub(pattern, "")
252
+ end
253
+
254
+ result.strip
255
+ end
256
+
257
+ def process_button(attrs, inner)
258
+ href = attrs["href"] || "#"
259
+ style = extract_all_style_attributes(attrs)
260
+
261
+ # Parse padding
262
+ padding_top, padding_right, padding_bottom, padding_left = parse_padding(style)
263
+
264
+ y = padding_top + padding_bottom
265
+ text_raise = px_to_pt(y)
266
+
267
+ pl_font_width, pl_space_count = compute_font_width_and_space_count(padding_left)
268
+ pr_font_width, pr_space_count = compute_font_width_and_space_count(padding_right)
269
+
270
+ button_style = {
271
+ "line-height" => "100%",
272
+ "text-decoration" => "none",
273
+ "display" => "inline-block",
274
+ "max-width" => "100%",
275
+ "mso-padding-alt" => "0px"
276
+ }
277
+
278
+ # Merge with extracted styles
279
+ button_style.merge!(style)
280
+
281
+ # Override padding with parsed values
282
+ button_style["padding-top"] = "#{padding_top}px"
283
+ button_style["padding-right"] = "#{padding_right}px"
284
+ button_style["padding-bottom"] = "#{padding_bottom}px"
285
+ button_style["padding-left"] = "#{padding_left}px"
286
+
287
+ style_str = style_to_string(button_style)
288
+
289
+ left_mso_spaces = "&#8202;" * pl_space_count
290
+ right_mso_spaces = "&#8202;" * pr_space_count
291
+
292
+ %(<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}&#8203;</i><![endif]--></a>)
293
+ end
294
+
295
+ def parse_padding(style)
296
+ if style["padding"]
297
+ parts = style["padding"].split
298
+ case parts.length
299
+ when 1
300
+ val = parse_px(parts[0])
301
+ [val, val, val, val]
302
+ when 2
303
+ vertical = parse_px(parts[0])
304
+ horizontal = parse_px(parts[1])
305
+ [vertical, horizontal, vertical, horizontal]
306
+ when 4
307
+ [parse_px(parts[0]), parse_px(parts[1]), parse_px(parts[2]), parse_px(parts[3])]
308
+ else
309
+ [0, 0, 0, 0]
310
+ end
311
+ else
312
+ [
313
+ parse_px(style["padding-top"] || "0"),
314
+ parse_px(style["padding-right"] || "0"),
315
+ parse_px(style["padding-bottom"] || "0"),
316
+ parse_px(style["padding-left"] || "0")
317
+ ]
318
+ end
319
+ end
320
+
321
+ def parse_px(s)
322
+ s.to_s.gsub("px", "").to_i
323
+ end
324
+
325
+ def px_to_pt(px)
326
+ (px * 3) / 4
327
+ end
328
+
329
+ def compute_font_width_and_space_count(expected_width)
330
+ return [0, 0] if expected_width == 0
331
+
332
+ smallest_space_count = 0
333
+ max_font_width = 5.0
334
+
335
+ loop do
336
+ required_font_width = if smallest_space_count > 0
337
+ expected_width.to_f / smallest_space_count / 2.0
338
+ else
339
+ Float::INFINITY
340
+ end
341
+
342
+ return [required_font_width, smallest_space_count] if required_font_width <= max_font_width
343
+
344
+ smallest_space_count += 1
345
+ end
346
+ end
347
+
348
+ def process_tag(content, tag_name)
349
+ result = content
350
+ open_pattern = /<#{tag_name}([^>]*)>/i
351
+ close_tag = "</#{tag_name}>"
352
+ open_tag_start = "<#{tag_name}"
353
+
354
+ max_iterations = 10_000
355
+ iterations = 0
356
+
357
+ while iterations < max_iterations
358
+ iterations += 1
359
+
360
+ # Find all opening tags
361
+ matches = result.to_enum(:scan, open_pattern).map { Regexp.last_match }
362
+ break if matches.empty?
363
+
364
+ processed = false
365
+
366
+ # Find the innermost tag (one that has no nested same tags)
367
+ matches.reverse_each do |match|
368
+ start = match.begin(0)
369
+ inner_start = match.end(0)
370
+ attrs_str = match[1]
371
+
372
+ # Find the next close tag after this opening tag
373
+ close_pos = result.downcase.index(close_tag.downcase, inner_start)
374
+ next unless close_pos
375
+
376
+ inner = result[inner_start...close_pos]
377
+
378
+ # Check if there's another opening tag inside
379
+ next if inner.downcase.include?(open_tag_start.downcase)
380
+
381
+ # This is an innermost tag, process it
382
+ attrs = parse_attributes(attrs_str)
383
+ replacement = yield(attrs, inner)
384
+ end_pos = close_pos + close_tag.length
385
+
386
+ result = result[0...start] + replacement + result[end_pos..]
387
+ processed = true
388
+ break
389
+ end
390
+
391
+ break unless processed
392
+ end
393
+
394
+ result
395
+ end
396
+
397
+ def parse_attributes(attrs_str)
398
+ attrs = {}
399
+ attrs_str.scan(/([\w-]+)=["']([^"']*)["']/) do |key, value|
400
+ attrs[key] = value
401
+ end
402
+ attrs
403
+ end
404
+
405
+ def extract_all_style_attributes(attrs)
406
+ style = {}
407
+
408
+ # Typography attributes
409
+ if attrs["text-color"]
410
+ style["color"] = attrs["text-color"]
411
+ elsif attrs["color"]
412
+ style["color"] = attrs["color"]
413
+ end
414
+ style["background-color"] = attrs["background-color"] if attrs["background-color"]
415
+ style["font-size"] = attrs["font-size"] if attrs["font-size"]
416
+ style["font-family"] = attrs["font-family"] if attrs["font-family"]
417
+ style["font-weight"] = attrs["font-weight"] if attrs["font-weight"]
418
+ style["line-height"] = attrs["line-height"] if attrs["line-height"]
419
+ style["text-align"] = attrs["text-align"] if attrs["text-align"]
420
+ style["text-decoration"] = attrs["text-decoration"] if attrs["text-decoration"]
421
+
422
+ # Dimensions
423
+ style["width"] = attrs["width"] if attrs["width"]
424
+ style["height"] = attrs["height"] if attrs["height"]
425
+ style["max-width"] = attrs["max-width"] if attrs["max-width"]
426
+ style["min-height"] = attrs["min-height"] if attrs["min-height"]
427
+
428
+ # Spacing - Padding
429
+ if attrs["padding"]
430
+ style["padding"] = attrs["padding"]
431
+ else
432
+ style["padding-top"] = attrs["padding-top"] if attrs["padding-top"]
433
+ style["padding-right"] = attrs["padding-right"] if attrs["padding-right"]
434
+ style["padding-bottom"] = attrs["padding-bottom"] if attrs["padding-bottom"]
435
+ style["padding-left"] = attrs["padding-left"] if attrs["padding-left"]
436
+ end
437
+
438
+ # Spacing - Margin
439
+ if attrs["margin"]
440
+ style["margin"] = attrs["margin"]
441
+ else
442
+ style["margin-top"] = attrs["margin-top"] if attrs["margin-top"]
443
+ style["margin-right"] = attrs["margin-right"] if attrs["margin-right"]
444
+ style["margin-bottom"] = attrs["margin-bottom"] if attrs["margin-bottom"]
445
+ style["margin-left"] = attrs["margin-left"] if attrs["margin-left"]
446
+ end
447
+
448
+ # Borders
449
+ if attrs["border"]
450
+ style["border"] = attrs["border"]
451
+ else
452
+ style["border-top"] = attrs["border-top"] if attrs["border-top"]
453
+ style["border-right"] = attrs["border-right"] if attrs["border-right"]
454
+ style["border-bottom"] = attrs["border-bottom"] if attrs["border-bottom"]
455
+ style["border-left"] = attrs["border-left"] if attrs["border-left"]
456
+ style["border-color"] = attrs["border-color"] if attrs["border-color"]
457
+ style["border-width"] = attrs["border-width"] if attrs["border-width"]
458
+ style["border-style"] = attrs["border-style"] if attrs["border-style"]
459
+ end
460
+
461
+ # Border Radius
462
+ if attrs["border-radius"]
463
+ style["border-radius"] = attrs["border-radius"]
464
+ else
465
+ style["border-top-left-radius"] = attrs["border-top-left-radius"] if attrs["border-top-left-radius"]
466
+ style["border-top-right-radius"] = attrs["border-top-right-radius"] if attrs["border-top-right-radius"]
467
+ style["border-bottom-left-radius"] = attrs["border-bottom-left-radius"] if attrs["border-bottom-left-radius"]
468
+ style["border-bottom-right-radius"] = attrs["border-bottom-right-radius"] if attrs["border-bottom-right-radius"]
469
+ end
470
+
471
+ style
472
+ end
473
+
474
+ def style_to_string(style)
475
+ style.map { |k, v| "#{k}:#{v}" }.join(";")
476
+ end
477
+
478
+ def generate_html(content)
479
+ title = @head_settings.title.empty? ? "" : "<title>#{@head_settings.title}</title>"
480
+
481
+ font_links = @head_settings.fonts.map do |font|
482
+ %(<link href="#{CGI.escapeHTML(font.url)}" rel="stylesheet" type="text/css" />)
483
+ end.join("\n")
484
+
485
+ styles = @head_settings.styles.empty? ? "" : %(<style type="text/css">#{@head_settings.styles}</style>)
486
+
487
+ preview_text = ""
488
+ unless @head_settings.preview_text.empty?
489
+ preview_text = %(<div style="display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;">#{CGI.escapeHTML(@head_settings.preview_text)}</div>)
490
+ end
491
+
492
+ <<~HTML
493
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
494
+ <html lang="en" dir="ltr">
495
+ <head>
496
+ <meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
497
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
498
+ #{title}
499
+ #{font_links}
500
+ #{styles}
501
+ </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;background-color:#ffffff">
503
+ #{preview_text}
504
+ #{content}
505
+ </body>
506
+ </html>
507
+ HTML
508
+ end
509
+ end
510
+
511
+ # Module-level render method
512
+ def self.render(markup)
513
+ Renderer.new.render(markup)
514
+ end
515
+ end
516
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Sevk
6
+ module Resources
7
+ class Audiences < Base
8
+ def list(page: nil, limit: nil)
9
+ params = { page: page, limit: limit }
10
+ client.get("/audiences#{build_query_string(params)}")
11
+ end
12
+
13
+ def get(id)
14
+ client.get("/audiences/#{id}")
15
+ end
16
+
17
+ def create(name:, description: nil, users_can_see: nil)
18
+ body = { name: name }
19
+ body[:description] = description if description
20
+ body[:usersCanSee] = users_can_see if users_can_see
21
+ client.post("/audiences", body)
22
+ end
23
+
24
+ def update(id, name: nil, description: nil, users_can_see: nil)
25
+ body = {}
26
+ body[:name] = name if name
27
+ body[:description] = description if description
28
+ body[:usersCanSee] = users_can_see if users_can_see
29
+ client.put("/audiences/#{id}", body)
30
+ end
31
+
32
+ def delete(id)
33
+ client.delete("/audiences/#{id}")
34
+ end
35
+
36
+ def add_contacts(audience_id, contact_ids)
37
+ client.post("/audiences/#{audience_id}/contacts", { contactIds: contact_ids })
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sevk
4
+ module Resources
5
+ class Base
6
+ attr_reader :client
7
+
8
+ def initialize(client)
9
+ @client = client
10
+ end
11
+
12
+ private
13
+
14
+ def build_query_string(params)
15
+ return "" if params.nil? || params.empty?
16
+
17
+ query = params.compact.map { |k, v| "#{k}=#{v}" }.join("&")
18
+ query.empty? ? "" : "?#{query}"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Sevk
6
+ module Resources
7
+ class Broadcasts < Base
8
+ def list(page: nil, limit: nil, search: nil, status: nil)
9
+ params = { page: page, limit: limit, search: search, status: status }
10
+ client.get("/broadcasts#{build_query_string(params)}")
11
+ end
12
+
13
+ def get(id)
14
+ client.get("/broadcasts/#{id}")
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Sevk
6
+ module Resources
7
+ class Contacts < Base
8
+ def list(page: nil, limit: nil, search: nil)
9
+ params = { page: page, limit: limit, search: search }
10
+ client.get("/contacts#{build_query_string(params)}")
11
+ end
12
+
13
+ def get(id)
14
+ client.get("/contacts/#{id}")
15
+ end
16
+
17
+ def create(email:, first_name: nil, last_name: nil, subscribed: nil, metadata: nil)
18
+ body = { email: email }
19
+ body[:firstName] = first_name if first_name
20
+ body[:lastName] = last_name if last_name
21
+ body[:subscribed] = subscribed unless subscribed.nil?
22
+ body[:metadata] = metadata if metadata
23
+ client.post("/contacts", body)
24
+ end
25
+
26
+ def update(id, first_name: nil, last_name: nil, subscribed: nil, metadata: nil)
27
+ body = {}
28
+ body[:firstName] = first_name if first_name
29
+ body[:lastName] = last_name if last_name
30
+ body[:subscribed] = subscribed unless subscribed.nil?
31
+ body[:metadata] = metadata if metadata
32
+ client.put("/contacts/#{id}", body)
33
+ end
34
+
35
+ def delete(id)
36
+ client.delete("/contacts/#{id}")
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Sevk
6
+ module Resources
7
+ class Domains < Base
8
+ def list(verified: nil)
9
+ params = { verified: verified }
10
+ client.get("/domains#{build_query_string(params)}")
11
+ end
12
+
13
+ def get(id)
14
+ client.get("/domains/#{id}")
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Sevk
6
+ module Resources
7
+ class Emails < Base
8
+ def send(to:, subject:, html:, from:, from_name: nil, reply_to: nil, text: nil, headers: nil, tags: nil)
9
+ body = {
10
+ to: to,
11
+ subject: subject,
12
+ html: html,
13
+ from: from
14
+ }
15
+ body[:fromName] = from_name if from_name
16
+ body[:replyTo] = reply_to if reply_to
17
+ body[:text] = text if text
18
+ body[:headers] = headers if headers
19
+ body[:tags] = tags if tags
20
+ client.post("emails", body)
21
+ end
22
+ end
23
+ end
24
+ end