haskimail 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.
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_client"
4
+ require_relative "resources/emails"
5
+ require_relative "resources/templates"
6
+ require_relative "resources/messages"
7
+ require_relative "resources/server_resources"
8
+ require_relative "resources/account_resources"
9
+
10
+ module Haskimail
11
+ # Token that validates send requests without sending anything.
12
+ TEST_TOKEN = "HASKIMAIL_API_TEST"
13
+
14
+ # Client authenticated with a server token (`X-Haskimail-Server-Token`):
15
+ # everything that belongs to one server, including sending email.
16
+ #
17
+ # client = Haskimail::ServerClient.new(ENV.fetch("HASKIMAIL_SERVER_TOKEN"))
18
+ # client.emails.deliver(from: "shop@example.com", to: "ivan@example.com",
19
+ # subject: "Привет", text_body: "Привет!")
20
+ #
21
+ # Pass {Haskimail::TEST_TOKEN} to validate send requests without sending.
22
+ # Instances are immutable and safe to share between threads.
23
+ class ServerClient < BaseClient
24
+ # Header that carries the server token.
25
+ TOKEN_HEADER = "X-Haskimail-Server-Token"
26
+
27
+ # @return [Resources::Emails]
28
+ attr_reader :emails
29
+ # @return [Resources::Templates]
30
+ attr_reader :templates
31
+ # @return [Resources::Messages]
32
+ attr_reader :messages
33
+ # @return [Resources::Opens]
34
+ attr_reader :opens
35
+ # @return [Resources::Clicks]
36
+ attr_reader :clicks
37
+ # @return [Resources::InboundMessages]
38
+ attr_reader :inbound_messages
39
+ # @return [Resources::Bounces]
40
+ attr_reader :bounces
41
+ # @return [Resources::MessageStreams]
42
+ attr_reader :message_streams
43
+ # @return [Resources::Suppressions]
44
+ attr_reader :suppressions
45
+ # @return [Resources::Webhooks]
46
+ attr_reader :webhooks
47
+ # @return [Resources::InboundRules]
48
+ attr_reader :inbound_rules
49
+ # @return [Resources::Stats]
50
+ attr_reader :stats
51
+
52
+ # @param token [String, nil] server token; defaults to ENV["HASKIMAIL_SERVER_TOKEN"]
53
+ def initialize(token = ENV.fetch("HASKIMAIL_SERVER_TOKEN", nil), **options)
54
+ super
55
+ end
56
+
57
+ protected
58
+
59
+ # Name of the authentication header.
60
+ def token_header
61
+ TOKEN_HEADER
62
+ end
63
+
64
+ # Creates the resource objects available with this token.
65
+ def build_resources
66
+ @emails = Resources::Emails.new(self)
67
+ @templates = Resources::Templates.new(self)
68
+ @messages = Resources::Messages.new(self)
69
+ @opens = Resources::Opens.new(self)
70
+ @clicks = Resources::Clicks.new(self)
71
+ @inbound_messages = Resources::InboundMessages.new(self)
72
+ @bounces = Resources::Bounces.new(self)
73
+ @message_streams = Resources::MessageStreams.new(self)
74
+ @suppressions = Resources::Suppressions.new(self)
75
+ @webhooks = Resources::Webhooks.new(self)
76
+ @inbound_rules = Resources::InboundRules.new(self)
77
+ @stats = Resources::Stats.new(self)
78
+ end
79
+ end
80
+
81
+ # Client authenticated with the account token (`X-Haskimail-Account-Token`).
82
+ #
83
+ # Manages servers and domains, moves templates between servers and — as the
84
+ # documentation allows for the account key — reads and manages data of all
85
+ # servers of the account at once: templates, messages, opens, clicks,
86
+ # bounces, message streams, webhooks and statistics.
87
+ #
88
+ # Sending email, inbound messages, inbound rules and suppression lists need a
89
+ # server token: use {ServerClient} for them.
90
+ #
91
+ # account = Haskimail::AccountClient.new(ENV.fetch("HASKIMAIL_ACCOUNT_TOKEN"))
92
+ # account.servers.each { |server| puts server.name }
93
+ class AccountClient < BaseClient
94
+ # Header that carries the account token.
95
+ TOKEN_HEADER = "X-Haskimail-Account-Token"
96
+
97
+ # @return [Resources::Servers]
98
+ attr_reader :servers
99
+ # @return [Resources::Domains]
100
+ attr_reader :domains
101
+ # @return [Resources::AccountTemplates]
102
+ attr_reader :templates
103
+ # @return [Resources::Messages]
104
+ attr_reader :messages
105
+ # @return [Resources::Opens]
106
+ attr_reader :opens
107
+ # @return [Resources::Clicks]
108
+ attr_reader :clicks
109
+ # @return [Resources::Bounces]
110
+ attr_reader :bounces
111
+ # With the account token `create` needs `server_id:`.
112
+ # @return [Resources::MessageStreams]
113
+ attr_reader :message_streams
114
+ # With the account token pass `message_stream:` to `create` explicitly.
115
+ # @return [Resources::Webhooks]
116
+ attr_reader :webhooks
117
+ # @return [Resources::Stats]
118
+ attr_reader :stats
119
+
120
+ # @param token [String, nil] account token; defaults to ENV["HASKIMAIL_ACCOUNT_TOKEN"]
121
+ def initialize(token = ENV.fetch("HASKIMAIL_ACCOUNT_TOKEN", nil), **options)
122
+ super
123
+ end
124
+
125
+ protected
126
+
127
+ # Name of the authentication header.
128
+ def token_header
129
+ TOKEN_HEADER
130
+ end
131
+
132
+ # Creates the resource objects available with this token.
133
+ def build_resources
134
+ @servers = Resources::Servers.new(self)
135
+ @domains = Resources::Domains.new(self)
136
+ @templates = Resources::AccountTemplates.new(self)
137
+ @messages = Resources::Messages.new(self)
138
+ @opens = Resources::Opens.new(self)
139
+ @clicks = Resources::Clicks.new(self)
140
+ @bounces = Resources::Bounces.new(self)
141
+ @message_streams = Resources::MessageStreams.new(self)
142
+ @webhooks = Resources::Webhooks.new(self)
143
+ @stats = Resources::Stats.new(self)
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+ require "uri"
5
+
6
+ require_relative "errors"
7
+
8
+ # Ruby SDK for the Haskimail transactional email API.
9
+ module Haskimail
10
+ # Client settings. A global instance lives in {Haskimail.configuration};
11
+ # every client takes a frozen snapshot of it at construction time, so
12
+ # changing global settings later never affects clients that already exist.
13
+ #
14
+ # Haskimail.configure do |config|
15
+ # config.read_timeout = 15
16
+ # config.max_retries = 3
17
+ # config.logger = Rails.logger
18
+ # end
19
+ class Configuration
20
+ # Production API endpoint.
21
+ DEFAULT_BASE_URL = "https://api.haskimail.ru"
22
+
23
+ # Every setting, in the order shown by #inspect.
24
+ ATTRIBUTES = %i[
25
+ base_url open_timeout read_timeout write_timeout max_retries
26
+ logger proxy ca_file user_agent
27
+ ].freeze
28
+
29
+ # @!attribute [r] base_url
30
+ # @return [String] API endpoint (HTTPS)
31
+ # @!attribute [r] open_timeout
32
+ # @return [Numeric] seconds to establish the connection
33
+ # @!attribute [r] read_timeout
34
+ # @return [Numeric] seconds to wait for each read
35
+ # @!attribute [r] write_timeout
36
+ # @return [Numeric] seconds to wait for each write
37
+ # @!attribute [r] max_retries
38
+ # @return [Integer] automatic retries (0..10)
39
+ # @!attribute [r] logger
40
+ # @return [Logger, nil] request log without tokens or addresses
41
+ # @!attribute [r] proxy
42
+ # @return [URI::HTTP, false, nil] explicit proxy; false = direct; nil = from the environment
43
+ # @!attribute [r] ca_file
44
+ # @return [String, nil] extra trusted CA bundle
45
+ # @!attribute [r] user_agent
46
+ # @return [String, nil] suffix of the User-Agent header
47
+ attr_reader :base_url, :open_timeout, :read_timeout, :write_timeout, :max_retries,
48
+ :logger, :proxy, :ca_file, :user_agent
49
+
50
+ def initialize(**options)
51
+ @base_url = DEFAULT_BASE_URL
52
+ @open_timeout = 5
53
+ @read_timeout = 30
54
+ @write_timeout = 30
55
+ @max_retries = 2
56
+ @logger = nil
57
+ @proxy = nil
58
+ @ca_file = nil
59
+ @user_agent = nil
60
+ update(**options)
61
+ end
62
+
63
+ # Bulk assignment with validation.
64
+ # @return [self]
65
+ def update(**options)
66
+ unknown = options.keys - ATTRIBUTES
67
+ raise ConfigurationError, "Unknown option(s): #{unknown.join(', ')}" unless unknown.empty?
68
+
69
+ # All-or-nothing: validate on a copy, then apply.
70
+ staged = dup
71
+ options.each { |key, value| staged.public_send(:"#{key}=", value) }
72
+ ATTRIBUTES.each { |key| instance_variable_set(:"@#{key}", staged.public_send(key)) }
73
+ self
74
+ end
75
+
76
+ # HTTPS URL without credentials, query or fragment.
77
+ def base_url=(value)
78
+ @base_url = self.class.normalize_base_url(value)
79
+ end
80
+
81
+ # Seconds to establish the connection.
82
+ def open_timeout=(value)
83
+ @open_timeout = positive_number!(:open_timeout, value)
84
+ end
85
+
86
+ # Seconds to wait for each read of the response.
87
+ def read_timeout=(value)
88
+ @read_timeout = positive_number!(:read_timeout, value)
89
+ end
90
+
91
+ # Seconds to wait for each write of the request.
92
+ def write_timeout=(value)
93
+ @write_timeout = positive_number!(:write_timeout, value)
94
+ end
95
+
96
+ # Automatic retries after a failed attempt (0..10), see HTTP::RetryPolicy.
97
+ def max_retries=(value)
98
+ unless value.is_a?(Integer) && value.between?(0, 10)
99
+ raise ConfigurationError, "max_retries must be an Integer between 0 and 10"
100
+ end
101
+
102
+ @max_retries = value
103
+ end
104
+
105
+ # Any object responding to #debug/#info/#warn (Logger, Rails.logger...).
106
+ def logger=(value)
107
+ unless value.nil? || %i[debug info warn].all? { |m| value.respond_to?(m) }
108
+ raise ConfigurationError, "logger must respond to #debug, #info and #warn"
109
+ end
110
+
111
+ @logger = value
112
+ end
113
+
114
+ # Explicit HTTP proxy, e.g. "http://user:pass@proxy.local:3128". Traffic to
115
+ # Haskimail is still end-to-end TLS through the proxy (CONNECT tunnel).
116
+ # When nil, the `https_proxy`, `HTTPS_PROXY` or `http_proxy` environment
117
+ # variable is used (with `no_proxy`), validated the same way; `false`
118
+ # connects directly and ignores the environment.
119
+ def proxy=(value)
120
+ @proxy = value.nil? || value == false ? value : self.class.parse_proxy(value)
121
+ end
122
+
123
+ # Path to a PEM bundle of CAs trusted in addition to the system CAs
124
+ # (e.g. a corporate TLS proxy). Verification itself can never be disabled.
125
+ def ca_file=(value)
126
+ raise ConfigurationError, "ca_file #{value.inspect} does not exist" if value && !File.file?(value.to_s)
127
+
128
+ @ca_file = value&.to_s&.freeze
129
+ end
130
+
131
+ # Suffix appended to the SDK User-Agent, e.g. "my-shop/2.1".
132
+ # Visible ASCII and spaces only (it becomes an HTTP header value).
133
+ def user_agent=(value)
134
+ if !value.nil? && !value.to_s.b.match?(/\A[\x20-\x7e]*\z/n)
135
+ raise ConfigurationError, "user_agent may contain only printable ASCII characters"
136
+ end
137
+
138
+ @user_agent = value.nil? ? nil : value.to_s.dup.freeze
139
+ end
140
+
141
+ # Proxy password is masked; use #proxy for the real value.
142
+ def to_h
143
+ ATTRIBUTES.to_h { |key| [key, key == :proxy ? redacted_proxy : public_send(key)] }
144
+ end
145
+
146
+ # Like #to_h: the proxy password is masked.
147
+ def inspect
148
+ "#<#{self.class.name} #{to_h.map { |key, value| "#{key}=#{value.inspect}" }.join(' ')}>"
149
+ end
150
+ alias to_s inspect
151
+
152
+ # The proxy password must not end up in caches or dumps.
153
+ def marshal_dump
154
+ raise TypeError, "#{self.class.name} may hold credentials and cannot be marshalled"
155
+ end
156
+
157
+ # YAML (Psych) gets the redacted view, like #inspect.
158
+ def encode_with(coder)
159
+ coder.map = to_h.transform_keys(&:to_s).transform_values { |value| value.is_a?(String) ? value : value.inspect }
160
+ end
161
+
162
+ # @api private
163
+ # Validates a proxy URL. Net::HTTP talks to proxies in plain text, so an
164
+ # https:// proxy would silently send its credentials unencrypted: refused.
165
+ # @return [URI::HTTP] frozen
166
+ def self.parse_proxy(value, source: "proxy")
167
+ text = value.to_s.strip
168
+ text = "http://#{text}" unless text.include?("://") # "proxy.local:3128", as in many environments
169
+ uri = URI.parse(text)
170
+ unless uri.scheme == "http" && !uri.host.to_s.empty?
171
+ raise ConfigurationError, "#{source} must be an http:// URL (TLS to the proxy is not supported by Net::HTTP)"
172
+ end
173
+
174
+ uri.freeze
175
+ rescue URI::InvalidURIError
176
+ raise ConfigurationError, "#{source} is not a valid URL"
177
+ end
178
+
179
+ # @api private
180
+ # @return [String] frozen base URL without a trailing slash
181
+ def self.normalize_base_url(value)
182
+ uri = URI.parse(value.to_s)
183
+ raise ConfigurationError, "base_url must use https" unless uri.scheme == "https"
184
+ raise ConfigurationError, "base_url must contain a host" if uri.host.nil? || uri.host.empty?
185
+ raise ConfigurationError, "base_url must not contain credentials" if uri.userinfo
186
+ raise ConfigurationError, "base_url must not contain a query or fragment" if uri.query || uri.fragment
187
+
188
+ uri.to_s.chomp("/").freeze
189
+ rescue URI::InvalidURIError
190
+ raise ConfigurationError, "base_url is not a valid URL"
191
+ end
192
+
193
+ private
194
+
195
+ def redacted_proxy
196
+ proxy = @proxy
197
+ return proxy unless proxy
198
+ return proxy.to_s unless proxy.userinfo
199
+
200
+ # A lone user name may itself be a token, so it is masked too.
201
+ user = proxy.password ? "#{proxy.user}:[FILTERED]" : "[FILTERED]"
202
+ "#{proxy.scheme}://#{user}@#{proxy.host}:#{proxy.port}"
203
+ end
204
+
205
+ def positive_number!(name, value)
206
+ return value if value.is_a?(Numeric) && value.positive?
207
+
208
+ raise ConfigurationError, "#{name} must be a positive number of seconds"
209
+ end
210
+ end
211
+
212
+ # Global defaults live here (not in haskimail.rb) so that every file of the
213
+ # gem can be required on its own.
214
+ @configuration = Configuration.new.freeze
215
+ # Re-entrant: reading the configuration inside a configure block is fine.
216
+ @config_mutex = Monitor.new
217
+
218
+ class << self
219
+ # Global defaults (frozen), snapshotted by every client when it is created.
220
+ # Change them with {configure}.
221
+ # @return [Configuration]
222
+ def configuration
223
+ @config_mutex.synchronize { @configuration }
224
+ end
225
+
226
+ # Changes global defaults atomically: if the block raises, nothing changes.
227
+ #
228
+ # Haskimail.configure { |config| config.max_retries = 3 }
229
+ #
230
+ # @yieldparam config [Configuration]
231
+ def configure
232
+ raise ConfigurationError, "Haskimail.configure needs a block" unless block_given?
233
+
234
+ @config_mutex.synchronize do
235
+ draft = @configuration.dup
236
+ yield draft
237
+ @configuration = draft.freeze
238
+ end
239
+ end
240
+
241
+ # Restores defaults (handy in tests).
242
+ def reset_configuration!
243
+ @config_mutex.synchronize { @configuration = Configuration.new.freeze }
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Haskimail
4
+ # Base class for every error raised by this library.
5
+ # `rescue Haskimail::Error` catches every failure of an API call, of input
6
+ # validation and of webhook parsing. Only calling a Ruby method with the
7
+ # wrong arguments (e.g. a missing keyword) raises Ruby's own ArgumentError.
8
+ class Error < StandardError; end
9
+
10
+ # Invalid client configuration (missing token, non-HTTPS base URL, bad timeout...).
11
+ class ConfigurationError < Error; end
12
+
13
+ # The request was rejected on the client side before anything was sent.
14
+ class ValidationError < Error; end
15
+
16
+ # Webhook payload signature does not match.
17
+ class SignatureVerificationError < Error; end
18
+
19
+ # Webhook body is larger than the configured limit. A subclass of
20
+ # ValidationError, so answer it with a 413 rather than a 403: for inbound
21
+ # email a 403 stops Haskimail's retries and the message is lost.
22
+ class WebhookPayloadTooLargeError < ValidationError; end
23
+
24
+ # Network-level failure: DNS, refused connection, TLS, timeout.
25
+ class ConnectionError < Error
26
+ # @return [Exception, nil] the low-level exception
27
+ attr_reader :original_error
28
+
29
+ def initialize(message = nil, original_error: nil, request_sent: true)
30
+ super(message)
31
+ @original_error = original_error
32
+ @request_sent = request_sent
33
+ end
34
+
35
+ # `false` means the request certainly never reached Haskimail, so even a
36
+ # non-idempotent call (sending an email) is safe to repeat.
37
+ def request_sent?
38
+ @request_sent
39
+ end
40
+ end
41
+
42
+ # Connect, read or write timeout.
43
+ class TimeoutError < ConnectionError; end
44
+
45
+ # TLS handshake or certificate verification failure. Never retried.
46
+ class TlsError < ConnectionError; end
47
+
48
+ # Haskimail answered with an error. Carries the HTTP status and the API
49
+ # `ErrorCode` (see {Haskimail::ErrorCodes}).
50
+ class ApiError < Error
51
+ attr_reader :http_status, :error_code, :api_message, :response_body, :response_headers
52
+
53
+ def initialize(message = nil, http_status: nil, error_code: nil, api_message: nil,
54
+ response_body: nil, response_headers: {})
55
+ super(message)
56
+ @http_status = http_status
57
+ @error_code = error_code
58
+ @api_message = api_message
59
+ @response_body = response_body
60
+ @response_headers = response_headers
61
+ end
62
+
63
+ # Description of {#error_code} from the documentation, if known.
64
+ def error_code_description
65
+ ErrorCodes.describe(error_code)
66
+ end
67
+ end
68
+
69
+ # HTTP 401 — the token is missing, wrong, or of the wrong kind
70
+ # (e.g. a server token used for account-level methods; ErrorCode 10).
71
+ class AuthenticationError < ApiError; end
72
+
73
+ # HTTP 403 — the IP address is not in the account allow-list.
74
+ class ForbiddenError < ApiError; end
75
+
76
+ # HTTP 404 — unknown API method or object.
77
+ class NotFoundError < ApiError; end
78
+
79
+ # HTTP 413 — the batch is larger than 50 MB.
80
+ class PayloadTooLargeError < ApiError; end
81
+
82
+ # HTTP 422 — validation failed; inspect {#error_code}.
83
+ class UnprocessableEntityError < ApiError; end
84
+
85
+ # HTTP 429 — not described in the docs, handled defensively.
86
+ class RateLimitError < ApiError; end
87
+
88
+ # HTTP 5xx — server-side failure. GET/PUT/DELETE are retried automatically;
89
+ # a POST that got 500/502/504 MAY already have been executed (an email may
90
+ # have been sent), so check the message log before repeating it.
91
+ class ServerError < ApiError; end
92
+
93
+ # HTTP 503 — temporary unavailability; the request was not executed.
94
+ class ServiceUnavailableError < ServerError; end
95
+
96
+ # A successful status with a body the SDK cannot interpret (not JSON, or
97
+ # not the documented shape). For send calls the email MAY have been
98
+ # accepted — check the message log before re-sending.
99
+ class UnexpectedResponseError < ApiError; end
100
+
101
+ # Numeric `ErrorCode` values documented at
102
+ # https://haskimail.ru/developer/api/overview#error-codes
103
+ module ErrorCodes
104
+ # Неверный API-ключ.
105
+ INVALID_API_TOKEN = 10
106
+ # Объект не найден или неполный запрос.
107
+ NOT_FOUND_OR_INCOMPLETE_REQUEST = 12
108
+ # Внутренняя ошибка, повторите запрос позже.
109
+ INTERNAL_ERROR = 100
110
+ # Неизвестный метод API.
111
+ UNKNOWN_API_METHOD = 404
112
+
113
+ # Не все обязательные параметры.
114
+ MISSING_REQUIRED_FIELDS = 3
115
+ # Аккаунт не может отправлять.
116
+ ACCOUNT_CANNOT_SEND = 4
117
+ # Некорректный email-адрес.
118
+ INVALID_EMAIL_ADDRESS = 6
119
+ # Действие запрещено.
120
+ ACTION_NOT_ALLOWED = 15
121
+ # Слишком большое вложение.
122
+ ATTACHMENT_TOO_LARGE = 26
123
+ # Недопустимый тип вложения.
124
+ INVALID_ATTACHMENT_TYPE = 33
125
+ # Отправка недоступна.
126
+ SENDING_UNAVAILABLE = 35
127
+ # Временная ошибка отправки, повторите запрос позже.
128
+ TEMPORARY_SENDING_ERROR = 36
129
+ # Некорректные заголовки письма.
130
+ INVALID_HEADERS = 40
131
+ # Адрес отправителя не подтвержден.
132
+ SENDER_SIGNATURE_NOT_CONFIRMED = 52
133
+ # Неверное значение поля или неверный пакет писем.
134
+ INVALID_FIELD_VALUE_OR_BATCH = 300
135
+ # Отправитель не подтвержден (аккаунт на модерации).
136
+ SENDER_NOT_CONFIRMED = 400
137
+ # Неверное поле запроса (тестовый режим).
138
+ INVALID_REQUEST_FIELD = 403
139
+ # Запрещенный тип вложения (тестовый режим).
140
+ FORBIDDEN_ATTACHMENT_TYPE = 411
141
+ # Получатель не подтвержден (аккаунт на модерации).
142
+ RECIPIENT_NOT_CONFIRMED = 412
143
+ # Шаблон не найден.
144
+ TEMPLATE_NOT_FOUND = 1101
145
+ # Канал не найден.
146
+ MESSAGE_STREAM_NOT_FOUND = 1226
147
+ # Через входящий канал нельзя отправлять письма.
148
+ CANNOT_SEND_VIA_INBOUND_STREAM = 1236
149
+
150
+ # Неверные параметры запроса сообщений.
151
+ INVALID_MESSAGES_QUERY = 700
152
+ # Сообщение не найдено.
153
+ MESSAGE_NOT_FOUND = 701
154
+ # Пропустить можно только заблокированное входящее письмо.
155
+ BYPASS_ONLY_BLOCKED = 702
156
+ # Повторить можно только входящее письмо с ошибкой.
157
+ RETRY_ONLY_FAILED = 703
158
+ # Неверные параметры запроса правил.
159
+ INVALID_RULE_REQUEST = 800
160
+ # Такое правило уже есть.
161
+ RULE_ALREADY_EXISTS = 810
162
+ # Правило не найдено.
163
+ RULE_NOT_FOUND = 812
164
+ # Неверное правило.
165
+ INVALID_RULE = 813
166
+
167
+ # Не найден домен или сервер.
168
+ DOMAIN_OR_SERVER_NOT_FOUND = 510
169
+ # Домен уже добавлен.
170
+ DOMAIN_ALREADY_EXISTS = 512
171
+ # Неверный формат домена.
172
+ INVALID_DOMAIN = 516
173
+ # Нечего проверять или неверная модель.
174
+ TEMPLATE_VALIDATION_NOTHING_TO_CHECK = 1120
175
+
176
+ # Некорректный входящий домен.
177
+ INVALID_INBOUND_DOMAIN = 600
178
+ # Входящий домен уже используется.
179
+ INBOUND_DOMAIN_IN_USE = 602
180
+ # Неверный InboundHookUrl.
181
+ INVALID_INBOUND_HOOK_URL = 606
182
+ # У входящего домена нет MX-записи inbound.haskimail.ru.
183
+ INBOUND_DOMAIN_MISSING_MX = 610
184
+ # Неверный InboundSpamThreshold.
185
+ INVALID_INBOUND_SPAM_THRESHOLD = 611
186
+
187
+ # Неверный MessageStreamType.
188
+ INVALID_MESSAGE_STREAM_TYPE = 1221
189
+ # Входящий канал создать нельзя.
190
+ CANNOT_CREATE_INBOUND_STREAM = 1228
191
+ # Входящий канал нельзя архивировать.
192
+ CANNOT_ARCHIVE_INBOUND_STREAM = 1229
193
+
194
+ # Russian description of every documented code.
195
+ DESCRIPTIONS = {
196
+ INVALID_API_TOKEN => "Неверный API-ключ",
197
+ NOT_FOUND_OR_INCOMPLETE_REQUEST => "Объект не найден или неполный запрос",
198
+ INTERNAL_ERROR => "Внутренняя ошибка, повторите запрос позже",
199
+ UNKNOWN_API_METHOD => "Неизвестный метод API",
200
+ MISSING_REQUIRED_FIELDS => "Не все обязательные параметры",
201
+ ACCOUNT_CANNOT_SEND => "Аккаунт не может отправлять",
202
+ INVALID_EMAIL_ADDRESS => "Некорректный email-адрес",
203
+ ACTION_NOT_ALLOWED => "Действие запрещено",
204
+ ATTACHMENT_TOO_LARGE => "Слишком большое вложение",
205
+ INVALID_ATTACHMENT_TYPE => "Недопустимый тип вложения",
206
+ SENDING_UNAVAILABLE => "Отправка недоступна",
207
+ TEMPORARY_SENDING_ERROR => "Временная ошибка отправки, повторите запрос позже",
208
+ INVALID_HEADERS => "Некорректные заголовки письма",
209
+ SENDER_SIGNATURE_NOT_CONFIRMED => "Адрес отправителя не подтвержден",
210
+ INVALID_FIELD_VALUE_OR_BATCH => "Неверное значение поля или неверный пакет писем",
211
+ SENDER_NOT_CONFIRMED => "Отправитель не подтвержден (аккаунт на модерации)",
212
+ INVALID_REQUEST_FIELD => "Неверное поле запроса (тестовый режим)",
213
+ FORBIDDEN_ATTACHMENT_TYPE => "Запрещенный тип вложения (тестовый режим)",
214
+ RECIPIENT_NOT_CONFIRMED => "Получатель не подтвержден (аккаунт на модерации)",
215
+ TEMPLATE_NOT_FOUND => "Шаблон не найден",
216
+ MESSAGE_STREAM_NOT_FOUND => "Канал не найден",
217
+ CANNOT_SEND_VIA_INBOUND_STREAM => "Через входящий канал нельзя отправлять письма",
218
+ INVALID_MESSAGES_QUERY => "Неверные параметры запроса сообщений",
219
+ MESSAGE_NOT_FOUND => "Сообщение не найдено",
220
+ BYPASS_ONLY_BLOCKED => "Пропустить можно только заблокированное входящее письмо",
221
+ RETRY_ONLY_FAILED => "Повторить можно только входящее письмо с ошибкой",
222
+ INVALID_RULE_REQUEST => "Неверные параметры запроса правил",
223
+ RULE_ALREADY_EXISTS => "Такое правило уже есть",
224
+ RULE_NOT_FOUND => "Правило не найдено",
225
+ INVALID_RULE => "Неверное правило",
226
+ DOMAIN_OR_SERVER_NOT_FOUND => "Не найден домен или сервер",
227
+ DOMAIN_ALREADY_EXISTS => "Домен уже добавлен",
228
+ INVALID_DOMAIN => "Неверный формат домена",
229
+ TEMPLATE_VALIDATION_NOTHING_TO_CHECK => "Нечего проверять или неверная модель",
230
+ INVALID_INBOUND_DOMAIN => "Некорректный входящий домен",
231
+ INBOUND_DOMAIN_IN_USE => "Входящий домен уже используется",
232
+ INVALID_INBOUND_HOOK_URL => "Неверный InboundHookUrl",
233
+ INBOUND_DOMAIN_MISSING_MX => "У входящего домена нет MX-записи inbound.haskimail.ru",
234
+ INVALID_INBOUND_SPAM_THRESHOLD => "Неверный InboundSpamThreshold",
235
+ INVALID_MESSAGE_STREAM_TYPE => "Неверный MessageStreamType",
236
+ CANNOT_CREATE_INBOUND_STREAM => "Входящий канал создать нельзя",
237
+ CANNOT_ARCHIVE_INBOUND_STREAM => "Входящий канал нельзя архивировать"
238
+ }.freeze
239
+
240
+ # @param code [Integer, nil]
241
+ # @return [String, nil]
242
+ def self.describe(code)
243
+ DESCRIPTIONS[code]
244
+ end
245
+ end
246
+ end