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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +15 -0
- data/LICENSE.txt +21 -0
- data/README.md +267 -0
- data/lib/generators/haskimail/install/install_generator.rb +32 -0
- data/lib/generators/haskimail/install/templates/haskimail.rb +27 -0
- data/lib/haskimail/attachment.rb +84 -0
- data/lib/haskimail/clients.rb +78 -0
- data/lib/haskimail/configuration.rb +89 -0
- data/lib/haskimail/connection.rb +268 -0
- data/lib/haskimail/errors.rb +130 -0
- data/lib/haskimail/paginator.rb +48 -0
- data/lib/haskimail/rails/delivery_method.rb +56 -0
- data/lib/haskimail/rails/mail_converter.rb +130 -0
- data/lib/haskimail/rails/mailer_helpers.rb +42 -0
- data/lib/haskimail/rails/message_options.rb +88 -0
- data/lib/haskimail/rails/railtie.rb +28 -0
- data/lib/haskimail/resources/account_templates.rb +53 -0
- data/lib/haskimail/resources/base.rb +75 -0
- data/lib/haskimail/resources/bounces.rb +43 -0
- data/lib/haskimail/resources/domains.rb +42 -0
- data/lib/haskimail/resources/emails.rb +149 -0
- data/lib/haskimail/resources/inbound_rules.rb +33 -0
- data/lib/haskimail/resources/message_streams.rb +46 -0
- data/lib/haskimail/resources/messages.rb +135 -0
- data/lib/haskimail/resources/servers.rb +89 -0
- data/lib/haskimail/resources/stats.rb +89 -0
- data/lib/haskimail/resources/suppressions.rb +39 -0
- data/lib/haskimail/resources/templates.rb +68 -0
- data/lib/haskimail/resources/webhooks.rb +80 -0
- data/lib/haskimail/response_handler.rb +68 -0
- data/lib/haskimail/secret.rb +40 -0
- data/lib/haskimail/serializer.rb +75 -0
- data/lib/haskimail/util.rb +110 -0
- data/lib/haskimail/version.rb +5 -0
- data/lib/haskimail/webhooks/endpoint.rb +146 -0
- data/lib/haskimail/webhooks/event.rb +103 -0
- data/lib/haskimail/webhooks/inbound_message.rb +100 -0
- data/lib/haskimail/webhooks.rb +87 -0
- data/lib/haskimail-rails.rb +19 -0
- data/lib/haskimail.rb +58 -0
- 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
|