haskimail-rails 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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +15 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +267 -0
  5. data/lib/generators/haskimail/install/install_generator.rb +32 -0
  6. data/lib/generators/haskimail/install/templates/haskimail.rb +27 -0
  7. data/lib/haskimail/attachment.rb +84 -0
  8. data/lib/haskimail/clients.rb +78 -0
  9. data/lib/haskimail/configuration.rb +89 -0
  10. data/lib/haskimail/connection.rb +268 -0
  11. data/lib/haskimail/errors.rb +130 -0
  12. data/lib/haskimail/paginator.rb +48 -0
  13. data/lib/haskimail/rails/delivery_method.rb +56 -0
  14. data/lib/haskimail/rails/mail_converter.rb +130 -0
  15. data/lib/haskimail/rails/mailer_helpers.rb +42 -0
  16. data/lib/haskimail/rails/message_options.rb +88 -0
  17. data/lib/haskimail/rails/railtie.rb +28 -0
  18. data/lib/haskimail/resources/account_templates.rb +53 -0
  19. data/lib/haskimail/resources/base.rb +75 -0
  20. data/lib/haskimail/resources/bounces.rb +43 -0
  21. data/lib/haskimail/resources/domains.rb +42 -0
  22. data/lib/haskimail/resources/emails.rb +149 -0
  23. data/lib/haskimail/resources/inbound_rules.rb +33 -0
  24. data/lib/haskimail/resources/message_streams.rb +46 -0
  25. data/lib/haskimail/resources/messages.rb +135 -0
  26. data/lib/haskimail/resources/servers.rb +89 -0
  27. data/lib/haskimail/resources/stats.rb +89 -0
  28. data/lib/haskimail/resources/suppressions.rb +39 -0
  29. data/lib/haskimail/resources/templates.rb +68 -0
  30. data/lib/haskimail/resources/webhooks.rb +80 -0
  31. data/lib/haskimail/response_handler.rb +68 -0
  32. data/lib/haskimail/secret.rb +40 -0
  33. data/lib/haskimail/serializer.rb +75 -0
  34. data/lib/haskimail/util.rb +110 -0
  35. data/lib/haskimail/version.rb +5 -0
  36. data/lib/haskimail/webhooks/endpoint.rb +146 -0
  37. data/lib/haskimail/webhooks/event.rb +103 -0
  38. data/lib/haskimail/webhooks/inbound_message.rb +100 -0
  39. data/lib/haskimail/webhooks.rb +87 -0
  40. data/lib/haskimail-rails.rb +19 -0
  41. data/lib/haskimail.rb +58 -0
  42. metadata +132 -0
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Haskimail
4
+ module Rails
5
+ # Haskimail-specific settings attached to a Mail::Message (`message.haskimail`).
6
+ class MessageOptions
7
+ ATTRIBUTES = %i[tag metadata message_stream track_opens track_links template template_model].freeze
8
+
9
+ attr_reader(*ATTRIBUTES)
10
+ # Response of the last successful delivery ({ message_id:, submitted_at:, ... }).
11
+ attr_accessor :response
12
+
13
+ def assign(options)
14
+ Util.assert_known_keys!(options.keys, ATTRIBUTES, "haskimail")
15
+ options.each { |key, value| public_send(:"#{key}=", value) }
16
+ self
17
+ end
18
+
19
+ def tag=(value)
20
+ @tag = value&.to_s
21
+ end
22
+
23
+ def metadata=(value)
24
+ raise ArgumentError, "metadata must be a Hash" unless value.nil? || value.is_a?(Hash)
25
+
26
+ @metadata = value
27
+ end
28
+
29
+ def message_stream=(value)
30
+ @message_stream = value&.to_s
31
+ end
32
+
33
+ def track_opens=(value)
34
+ raise ArgumentError, "track_opens must be true, false or nil" unless [true, false, nil].include?(value)
35
+
36
+ @track_opens = value
37
+ end
38
+
39
+ def track_links=(value)
40
+ raise ArgumentError, "track_links must be true, false or nil" unless [true, false, nil].include?(value)
41
+
42
+ @track_links = value
43
+ end
44
+
45
+ # @param value [Integer, String] template ID (Integer) or alias/name (String)
46
+ def template=(value)
47
+ unless value.nil? || value.is_a?(Integer) || (value.is_a?(String) && !value.strip.empty?)
48
+ raise ArgumentError, "template must be an Integer ID or a String alias"
49
+ end
50
+
51
+ @template = value
52
+ end
53
+
54
+ def template_model=(value)
55
+ raise ArgumentError, "template_model must be a Hash" unless value.nil? || value.is_a?(Hash)
56
+
57
+ @template_model = value
58
+ end
59
+
60
+ def template?
61
+ !template.nil?
62
+ end
63
+
64
+ def message_id
65
+ response && response[:message_id]
66
+ end
67
+
68
+ def to_api
69
+ payload = {
70
+ "Tag" => tag, "Metadata" => metadata, "MessageStream" => message_stream,
71
+ "TrackOpens" => track_opens, "TrackLinks" => track_links
72
+ }
73
+ if template?
74
+ payload[template.is_a?(Integer) ? "TemplateId" : "TemplateAlias"] = template
75
+ payload["TemplateModel"] = template_model || {}
76
+ end
77
+ payload.compact
78
+ end
79
+ end
80
+
81
+ # Adds `message.haskimail` to Mail::Message.
82
+ module MailExtension
83
+ def haskimail
84
+ @haskimail ||= MessageOptions.new
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Haskimail
4
+ module Rails
5
+ # Wires Haskimail into a Rails application.
6
+ #
7
+ # # config/application.rb (optional; credentials/ENV work too)
8
+ # config.haskimail.server_token = Rails.application.credentials.dig(:haskimail, :server_token)
9
+ # config.haskimail.default_message_stream = "2081"
10
+ class Railtie < ::Rails::Railtie
11
+ config.haskimail = ActiveSupport::OrderedOptions.new
12
+
13
+ initializer "haskimail.configure" do |app|
14
+ settings = app.config.haskimail.to_h
15
+ unknown = settings.keys.map(&:to_sym) - Configuration::SETTINGS
16
+ raise ConfigurationError, "Unknown config.haskimail option(s): #{unknown.join(', ')}" if unknown.any?
17
+
18
+ Haskimail.configure do |config|
19
+ settings.each { |key, value| config.public_send(:"#{key}=", value) }
20
+ end
21
+ end
22
+
23
+ generators do
24
+ require_relative "../../generators/haskimail/install/install_generator"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Haskimail
4
+ module Resources
5
+ # Account-level template operations (account token).
6
+ class AccountTemplates < Base
7
+ # Moves (not copies!) all templates from one server to another.
8
+ # Call with perform_changes: false first to preview what would be moved.
9
+ # @return [Hash] { total_count:, templates: [{ template_id:, name: }] }
10
+ def push(source_server_id:, destination_server_id:, perform_changes:)
11
+ require_boolean!(:perform_changes, perform_changes)
12
+ require_value!(:source_server_id, source_server_id)
13
+ require_value!(:destination_server_id, destination_server_id)
14
+ if source_server_id.to_s == destination_server_id.to_s
15
+ raise ArgumentError, "source and destination servers must differ"
16
+ end
17
+
18
+ api_put(path(:templates, :push), {
19
+ "SourceServerID" => source_server_id,
20
+ "DestinationServerID" => destination_server_id,
21
+ "PerformChanges" => perform_changes
22
+ })
23
+ end
24
+
25
+ # Creates a template on any server of the account.
26
+ def create(associated_server_id:, name:, subject: nil, html_body: nil, text_body: nil)
27
+ server_templates.create(name: name, subject: subject, html_body: html_body,
28
+ text_body: text_body, associated_server_id: associated_server_id)
29
+ end
30
+
31
+ # Templates of all servers in the account.
32
+ # @return [Hash] { total_count:, templates: [{ template_id:, name: }] }
33
+ def list(count: Templates::DEFAULT_PAGE_SIZE, offset: 0)
34
+ server_templates.list(count: count, offset: offset)
35
+ end
36
+
37
+ def each(page_size: Templates::DEFAULT_PAGE_SIZE, &)
38
+ server_templates.each(page_size: page_size, &)
39
+ end
40
+
41
+ # Renders a template without saving or sending anything (see Templates#validate).
42
+ def validate(**options)
43
+ server_templates.validate(**options)
44
+ end
45
+
46
+ private
47
+
48
+ def server_templates
49
+ @server_templates ||= Templates.new(connection)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Haskimail
4
+ module Resources
5
+ # Common plumbing for API resources: path building, serialization, validation.
6
+ class Base
7
+ def initialize(connection)
8
+ @connection = connection
9
+ end
10
+
11
+ def inspect
12
+ "#<#{self.class.name}>"
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :connection
18
+
19
+ def path(*segments)
20
+ segments.map do |segment|
21
+ segment.is_a?(Symbol) ? segment.to_s : Util.escape_path_segment(segment)
22
+ end.unshift("").join("/")
23
+ end
24
+
25
+ def api_get(api_path, query = nil)
26
+ Serializer.from_api(connection.get(api_path, query: query))
27
+ end
28
+
29
+ def api_post(api_path, body = nil, idempotent: false)
30
+ Serializer.from_api(connection.post(api_path, body && Serializer.to_api(body), idempotent: idempotent))
31
+ end
32
+
33
+ def api_put(api_path, body = nil, idempotent: true)
34
+ Serializer.from_api(connection.put(api_path, body && Serializer.to_api(body), idempotent: idempotent))
35
+ end
36
+
37
+ def api_patch(api_path, body = nil)
38
+ Serializer.from_api(connection.patch(api_path, body && Serializer.to_api(body)))
39
+ end
40
+
41
+ def api_delete(api_path)
42
+ Serializer.from_api(connection.delete(api_path))
43
+ end
44
+
45
+ # Returns an Enumerator, or iterates right away when a block is given.
46
+ def iterate(items_key, page_size, max_window, consumer, max_page_size: nil, &)
47
+ Util.assert_range!(:page_size, page_size, 1..(max_page_size || Float::INFINITY))
48
+ paginator = Paginator.new(items_key: items_key, page_size: page_size, max_window: max_window, &)
49
+ consumer ? paginator.each(&consumer) : paginator.each
50
+ end
51
+
52
+ def pagination_query(count, offset, max_count: nil, max_window: nil)
53
+ Util.assert_range!(:count, count, 1..(max_count || Float::INFINITY))
54
+ Util.assert_range!(:offset, offset, 0..Float::INFINITY)
55
+ if max_window && count + offset > max_window
56
+ raise ArgumentError, "count + offset must not exceed #{max_window} (API limit)"
57
+ end
58
+
59
+ { "count" => count, "offset" => offset }
60
+ end
61
+
62
+ def require_value!(name, value)
63
+ raise ArgumentError, "#{name} is required" if Util.blank?(value)
64
+
65
+ value
66
+ end
67
+
68
+ def require_boolean!(name, value)
69
+ raise ArgumentError, "#{name} must be true or false" unless [true, false].include?(value)
70
+
71
+ value
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Haskimail
4
+ module Resources
5
+ # Bounces: https://haskimail.ru/developer/api/bounce-api
6
+ class Bounces < Base
7
+ DEFAULT_PAGE_SIZE = 500
8
+ # List names and their delivery-stats aliases (both are accepted by the API).
9
+ TYPES = %w[HardBounce SoftBounce BlkBounce SpmBounce BulkBounce SpamBlock].freeze
10
+
11
+ FILTERS = {
12
+ type: "type", email_filter: "emailFilter", tag: "tag", message_id: "messageID",
13
+ message_stream: "messagestream", from_date: "fromdate", to_date: "todate"
14
+ }.freeze
15
+
16
+ # Bounce counts by type for the last 45 days.
17
+ # @return [Hash] { bounces: [{ name: "All", count: 253 }, { type: "HardBounce", ... }] }
18
+ def delivery_stats
19
+ api_get(path(:deliverystats))
20
+ end
21
+
22
+ # @return [Hash] { total_count:, bounces: [...] }
23
+ def list(count: DEFAULT_PAGE_SIZE, offset: 0, **filters)
24
+ Util.assert_known_keys!(filters.keys, FILTERS.keys, "bounce filter")
25
+ if filters[:type] && !TYPES.include?(filters[:type].to_s)
26
+ raise ArgumentError, "type must be one of: #{TYPES.join(', ')}"
27
+ end
28
+
29
+ query = pagination_query(count, offset)
30
+ filters.each do |key, value|
31
+ next if value.nil?
32
+
33
+ query[FILTERS.fetch(key)] = %i[from_date to_date].include?(key) ? Util.format_date(value) : value.to_s
34
+ end
35
+ api_get(path(:bounces), query)
36
+ end
37
+
38
+ def each(page_size: DEFAULT_PAGE_SIZE, **filters, &block)
39
+ iterate(:bounces, page_size, nil, block) { |count, offset| list(count: count, offset: offset, **filters) }
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Haskimail
4
+ module Resources
5
+ # Sender domains (account token): https://haskimail.ru/developer/api/domains-api
6
+ class Domains < Base
7
+ DEFAULT_PAGE_SIZE = 500
8
+
9
+ # @return [Hash] { total_count:, domains: [{ id:, name:, dkim_verified:, spf_verified:, ... }] }
10
+ def list(count: DEFAULT_PAGE_SIZE, offset: 0)
11
+ api_get(path(:domains), pagination_query(count, offset))
12
+ end
13
+
14
+ def each(page_size: DEFAULT_PAGE_SIZE, &block)
15
+ iterate(:domains, page_size, nil, block) { |count, offset| list(count: count, offset: offset) }
16
+ end
17
+
18
+ # @return [Hash] domain with :spf_text_value and :dkim_text_value DNS records
19
+ def get(domain_id)
20
+ api_get(path(:domains, domain_id))
21
+ end
22
+
23
+ def create(name)
24
+ require_value!(:name, name)
25
+ api_post(path(:domains), { "Name" => name.to_s.strip })
26
+ end
27
+
28
+ # Deletes the domain and its confirmed sender addresses. Irreversible.
29
+ def delete(domain_id)
30
+ api_delete(path(:domains, domain_id))
31
+ end
32
+
33
+ def verify_dkim(domain_id)
34
+ api_put(path(:domains, domain_id, :verifyDkim))
35
+ end
36
+
37
+ def verify_spf(domain_id)
38
+ api_put(path(:domains, domain_id, :verifyspf))
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Haskimail
4
+ module Resources
5
+ # Sending email: https://haskimail.ru/developer/api/email-api
6
+ # and template sending: https://haskimail.ru/developer/api/templates-api
7
+ class Emails < Base
8
+ MAX_BATCH_SIZE = 500
9
+ ADDRESS_FIELDS = %w[To Cc Bcc ReplyTo].freeze
10
+
11
+ # @example
12
+ # client.emails.deliver(
13
+ # from: "Магазин <shop@example.com>", to: "ivan@example.com",
14
+ # subject: "Заказ №1042", html_body: "<p>Спасибо!</p>",
15
+ # tag: "orders", metadata: { order_id: "1042" }, message_stream: "2081"
16
+ # )
17
+ # @return [Hash] { to:, submitted_at:, message_id:, error_code: 0, message: "OK" }
18
+ def deliver(message)
19
+ send_one("/email", build_message(message, template: false))
20
+ end
21
+
22
+ # @param messages [Array<Hash>] 1..500 messages; may mix plain and template messages
23
+ # @return [Array<Hash>] one result per message, in order. Check :error_code of each item:
24
+ # the API returns HTTP 200 even when some messages are rejected.
25
+ def deliver_batch(messages)
26
+ payload = build_batch(messages) { |message| build_message(message, template: nil) }
27
+ Serializer.from_api(connection.post("/email/batch", payload))
28
+ end
29
+
30
+ # @example
31
+ # client.emails.deliver_with_template(
32
+ # template_alias: "order-created", template_model: { "{{name}}" => "Иван" },
33
+ # from: "shop@example.com", to: "ivan@example.com"
34
+ # )
35
+ def deliver_with_template(message)
36
+ send_one("/email/withTemplate", build_message(message, template: true))
37
+ end
38
+
39
+ # @return [Array<Hash>] one result per message, in order (check :error_code of each)
40
+ def deliver_batch_with_templates(messages)
41
+ payload = build_batch(messages) { |message| build_message(message, template: true) }
42
+ Serializer.from_api(connection.post("/email/batchWithTemplates", { "Messages" => payload }))
43
+ end
44
+
45
+ private
46
+
47
+ def send_one(api_path, payload)
48
+ Serializer.from_api(connection.post(api_path, payload))
49
+ end
50
+
51
+ def build_batch(messages, &)
52
+ list = messages.respond_to?(:to_ary) ? messages.to_ary : nil
53
+ raise ArgumentError, "messages must be an Array" unless list
54
+ raise ArgumentError, "messages must not be empty" if list.empty?
55
+ if list.size > MAX_BATCH_SIZE
56
+ raise ArgumentError, "A batch may contain at most #{MAX_BATCH_SIZE} messages, got #{list.size}"
57
+ end
58
+
59
+ list.map(&)
60
+ end
61
+
62
+ # @param template [Boolean, nil] true: template required, false: body required, nil: either
63
+ def build_message(message, template:)
64
+ raise ArgumentError, "message must be a Hash, got #{message.class}" unless message.is_a?(Hash)
65
+
66
+ header_keys = message.keys.select { |key| Serializer.api_key(key) == "Headers" }
67
+ raise ArgumentError, "Duplicate field Headers (given as #{header_keys.inspect})" if header_keys.size > 1
68
+
69
+ # Header names are literal ("X-Order-Id"), so they bypass key transformation.
70
+ payload = Serializer.to_api(message.except(*header_keys))
71
+ payload["Headers"] = normalize_headers(message[header_keys.first]) if header_keys.any?
72
+ normalize!(payload)
73
+ default_stream = connection.config.default_message_stream
74
+ payload["MessageStream"] = default_stream.to_s if default_stream && Util.blank?(payload["MessageStream"])
75
+ validate_message!(payload, template)
76
+ payload.compact
77
+ end
78
+
79
+ def normalize!(payload)
80
+ payload["Attachments"] = normalize_attachments(payload["Attachments"]) if payload.key?("Attachments")
81
+ ADDRESS_FIELDS.each do |field|
82
+ payload[field] = payload[field].join(", ") if payload[field].is_a?(Array)
83
+ end
84
+ end
85
+
86
+ def validate_message!(payload, template)
87
+ %w[From To].each do |field|
88
+ raise ArgumentError, "#{field} is required" if Util.blank?(payload[field])
89
+ end
90
+
91
+ uses_template = !Util.blank?(payload["TemplateId"]) || !Util.blank?(payload["TemplateAlias"])
92
+ if template == true && !uses_template
93
+ raise ArgumentError, "template_id or template_alias is required"
94
+ elsif template == false && uses_template
95
+ raise ArgumentError, "Use deliver_with_template to send a template"
96
+ elsif !uses_template && Util.blank?(payload["HtmlBody"]) && Util.blank?(payload["TextBody"])
97
+ raise ArgumentError, "html_body or text_body is required"
98
+ end
99
+ end
100
+
101
+ # Accepts [{name:, value:}] or a plain Hash {"X-Order-Id" => "1042"}.
102
+ def normalize_headers(headers)
103
+ list = if headers.is_a?(Hash)
104
+ headers.map do |name, value|
105
+ { "Name" => name.to_s, "Value" => value.to_s }
106
+ end
107
+ else
108
+ headers
109
+ end
110
+ Array(list).map do |header|
111
+ raise ArgumentError, "Each header must be a Hash with name and value" unless header.is_a?(Hash)
112
+
113
+ header = Serializer.to_api(header)
114
+ name = header["Name"].to_s
115
+ value = header["Value"].to_s
116
+ if name.match?(/[\r\n:\0]/) || name.strip.empty? || value.match?(/[\r\n\0]/)
117
+ raise ArgumentError, "Invalid header #{name.inspect}"
118
+ end
119
+
120
+ { "Name" => name, "Value" => value }
121
+ end
122
+ end
123
+
124
+ # Hash attachments must already carry Base64 content; use Haskimail::Attachment for raw bytes.
125
+ def normalize_attachments(attachments)
126
+ Array(attachments).map do |attachment|
127
+ next attachment.to_h if attachment.is_a?(Attachment)
128
+ raise ArgumentError, "Each attachment must be a Hash or Haskimail::Attachment" unless attachment.is_a?(Hash)
129
+
130
+ if Util.blank?(attachment["Name"]) || Util.blank?(attachment["Content"])
131
+ raise ArgumentError, "Each attachment needs name and content"
132
+ end
133
+
134
+ Attachment.assert_allowed!(attachment["Name"])
135
+ assert_base64!(attachment)
136
+ content_id = Attachment.normalize_content_id(attachment["ContentID"])
137
+ content_id ? attachment.merge("ContentID" => content_id) : attachment.except("ContentID")
138
+ end
139
+ end
140
+
141
+ def assert_base64!(attachment)
142
+ Base64.strict_decode64(attachment["Content"].to_s)
143
+ rescue ArgumentError
144
+ raise ArgumentError, "Attachment #{attachment['Name'].inspect}: Content must be strict Base64 " \
145
+ "(use Haskimail::Attachment to pass raw bytes)"
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Haskimail
4
+ module Resources
5
+ # Inbound block rules: https://haskimail.ru/developer/api/inbound-rules-triggers-api
6
+ class InboundRules < Base
7
+ DEFAULT_PAGE_SIZE = 100
8
+ MAX_PAGE_SIZE = 500
9
+
10
+ # @return [Hash] { total_count:, inbound_rules: [{ id:, rule: }] }
11
+ def list(count: DEFAULT_PAGE_SIZE, offset: 0)
12
+ api_get(path(:triggers, :inboundrules), pagination_query(count, offset, max_count: MAX_PAGE_SIZE))
13
+ end
14
+
15
+ def each(page_size: DEFAULT_PAGE_SIZE, &block)
16
+ iterate(:inbound_rules, page_size, nil, block, max_page_size: MAX_PAGE_SIZE) do |count, offset|
17
+ list(count: count, offset: offset)
18
+ end
19
+ end
20
+
21
+ # @param rule [String] an email address ("spam@example.com") or a domain ("example.org")
22
+ # @return [Hash] { id:, rule: }
23
+ def create(rule)
24
+ require_value!(:rule, rule)
25
+ api_post(path(:triggers, :inboundrules), { "Rule" => rule.to_s.strip })
26
+ end
27
+
28
+ def delete(rule_id)
29
+ api_delete(path(:triggers, :inboundrules, rule_id))
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Haskimail
4
+ module Resources
5
+ # Message streams (channels): https://haskimail.ru/developer/api/message-streams-api
6
+ class MessageStreams < Base
7
+ CREATABLE_TYPES = %w[Transactional Broadcasts].freeze
8
+
9
+ # @return [Hash] { total_count:, message_streams: [...] } (archived streams included)
10
+ def list
11
+ api_get(path(:"message-streams"))
12
+ end
13
+
14
+ def get(stream_id)
15
+ api_get(path(:"message-streams", stream_id))
16
+ end
17
+
18
+ # @param message_stream_type ["Transactional", "Broadcasts"] (case-insensitive)
19
+ # @param server_id [Integer, nil] required only when called with an account token
20
+ def create(name:, message_stream_type:, server_id: nil)
21
+ require_value!(:name, name)
22
+ type = CREATABLE_TYPES.find { |candidate| candidate.casecmp?(message_stream_type.to_s) }
23
+ raise ArgumentError, "message_stream_type must be one of: #{CREATABLE_TYPES.join(', ')}" unless type
24
+
25
+ api_post(path(:"message-streams"),
26
+ Util.compact("ServerID" => server_id, "Name" => name, "MessageStreamType" => type))
27
+ end
28
+
29
+ # Only the name can be changed.
30
+ def update(stream_id, name:)
31
+ require_value!(:name, name)
32
+ api_patch(path(:"message-streams", stream_id), { "Name" => name })
33
+ end
34
+
35
+ # Stops sending through the stream. It is purged 45 days later unless unarchived.
36
+ # @return [Hash] { id:, server_id:, archived_at:, expected_purge_date: }
37
+ def archive(stream_id)
38
+ Serializer.from_api(connection.post(path(:"message-streams", stream_id, :archive), nil, idempotent: true))
39
+ end
40
+
41
+ def unarchive(stream_id)
42
+ Serializer.from_api(connection.post(path(:"message-streams", stream_id, :unarchive), nil, idempotent: true))
43
+ end
44
+ end
45
+ end
46
+ end