nextmail 0.0.1 → 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.
@@ -43,6 +43,21 @@ module Nextmail
43
43
  deliver(outbound, repeatable.nil? ? Protocol::RETRY[:METHODS].include?(verb) : repeatable, accept, binary)
44
44
  end
45
45
 
46
+ def stream(path, query: nil, headers: nil, accept: nil, api_key: nil, timeout: nil, idle_timeout: nil, &)
47
+ raise ArgumentError, Defaults::MESSAGES[:STREAM_BLOCK_REQUIRED] unless block_given?
48
+
49
+ outbound = HttpRequest.new(
50
+ method: Protocol::HTTP_METHODS[:GET],
51
+ url: Endpoint.build_url(@base_url, path, query),
52
+ headers: headers_for(body: nil, raw: nil, content_type: nil, api_key:, inbox_token: nil, anonymous: false,
53
+ idempotent: false, idempotency_key: nil, accept:, extra: headers),
54
+ body: nil,
55
+ timeout: seconds(timeout.nil? ? @timeout : timeout_option(timeout))
56
+ )
57
+
58
+ open_stream(outbound, idle_timeout.nil? ? nil : seconds(timeout_option(idle_timeout)), &)
59
+ end
60
+
46
61
  def inspect
47
62
  "#<#{self.class.name} base_url=#{@base_url.inspect}>"
48
63
  end
@@ -81,20 +96,116 @@ module Nextmail
81
96
  response = @adapter.call(outbound)
82
97
  HttpResponse.new(status: Integer(response.status), headers: header_table(response.headers), body: response.body.to_s)
83
98
  rescue Timeout::Error => error
84
- NetworkError.new(format(Defaults::MESSAGES[:TIMED_OUT], seconds: outbound.timeout), original: error, timeout: true)
99
+ timed_out(outbound.timeout, error)
85
100
  rescue *PROGRAMMING_ERRORS
86
101
  raise
87
102
  rescue => error
103
+ unreachable(error)
104
+ end
105
+
106
+ def timed_out(seconds, error)
107
+ NetworkError.new(format(Defaults::MESSAGES[:TIMED_OUT], seconds:), original: error, timeout: true)
108
+ end
109
+
110
+ def unreachable(error)
88
111
  NetworkError.new(format(Defaults::MESSAGES[:UNREACHABLE], base_url: @base_url, reason: error.message),
89
112
  original: error)
90
113
  end
91
114
 
115
+ def open_stream(outbound, idle)
116
+ refusal = nil
117
+ result = nil
118
+ inside = false
119
+
120
+ begin
121
+ receive_stream(outbound, idle) do |response|
122
+ if response.ok?
123
+ inside = true
124
+ result = yield response.with(body: guarded_chunks(response.body, idle))
125
+ inside = false
126
+ else
127
+ refusal = refused(response)
128
+ end
129
+ end
130
+ rescue Timeout::Error => error
131
+ raise if inside
132
+
133
+ raise timed_out(outbound.timeout, error)
134
+ rescue *PROGRAMMING_ERRORS
135
+ raise
136
+ rescue => error
137
+ raise if inside
138
+
139
+ raise unreachable(error)
140
+ end
141
+
142
+ raise api_error(refusal, retry_after(refusal.header(Protocol::HEADER_KEYS[:RETRY_AFTER]))) unless refusal.nil?
143
+
144
+ result
145
+ end
146
+
147
+ def receive_stream(outbound, idle)
148
+ unless @adapter.respond_to?(:stream)
149
+ whole = @adapter.call(outbound)
150
+ return yield HttpStream.new(status: Integer(whole.status), headers: header_table(whole.headers),
151
+ body: [whole.body.to_s])
152
+ end
153
+
154
+ @adapter.stream(outbound, idle_timeout: idle) do |response|
155
+ yield HttpStream.new(status: Integer(response.status), headers: header_table(response.headers),
156
+ body: response.body)
157
+ end
158
+ end
159
+
160
+ def refused(response)
161
+ body = begin
162
+ response.read
163
+ rescue *PROGRAMMING_ERRORS
164
+ raise
165
+ rescue
166
+ ""
167
+ end
168
+
169
+ HttpResponse.new(status: response.status, headers: response.headers, body:)
170
+ end
171
+
172
+ def guarded_chunks(body, idle)
173
+ Enumerator.new do |chunks|
174
+ inside = false
175
+
176
+ begin
177
+ body.each do |chunk|
178
+ inside = true
179
+ chunks << chunk
180
+ inside = false
181
+ end
182
+ rescue *PROGRAMMING_ERRORS
183
+ raise
184
+ rescue => error
185
+ raise if inside
186
+
187
+ raise stream_failure(error, idle)
188
+ end
189
+ end
190
+ end
191
+
192
+ def stream_failure(error, idle)
193
+ timeout = error.is_a?(Timeout::Error)
194
+ if timeout && !idle.nil?
195
+ return NetworkError.new(format(Defaults::MESSAGES[:STREAM_IDLE], seconds: idle), original: error, timeout:)
196
+ end
197
+
198
+ NetworkError.new(format(Defaults::MESSAGES[:STREAM_BROKEN], base_url: @base_url, reason: error.message),
199
+ original: error, timeout:)
200
+ end
201
+
92
202
  def header_table(headers)
93
203
  (headers || {}).to_h { |name, value| [name.to_s.downcase, value.is_a?(Array) ? value.join(", ") : value.to_s] }
94
204
  end
95
205
 
96
- def headers_for(body:, raw:, content_type:, api_key:, inbox_token:, anonymous:, idempotent:, idempotency_key:, accept:)
97
- headers = @headers.merge(
206
+ def headers_for(body:, raw:, content_type:, api_key:, inbox_token:, anonymous:, idempotent:, idempotency_key:, accept:,
207
+ extra: nil)
208
+ headers = @headers.merge(extra_headers(extra)).merge(
98
209
  Protocol::HEADER_KEYS[:ACCEPT] => accept || Protocol::CONTENT_TYPES[:JSON],
99
210
  Protocol::HEADER_KEYS[:USER_AGENT] => @user_agent
100
211
  )
@@ -141,6 +252,10 @@ module Nextmail
141
252
  "#{Protocol::IDEMPOTENCY_KEY_PREFIX}#{SecureRandom.uuid}"
142
253
  end
143
254
 
255
+ def extra_headers(extra)
256
+ (extra || {}).to_h { |name, value| [header_name(name), header_value(value)] }
257
+ end
258
+
144
259
  def header_name(name)
145
260
  text = name.to_s
146
261
  raise ArgumentError, Defaults::MESSAGES[:HEADER_SHAPE] unless HEADER_NAME.match?(text)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nextmail
4
- VERSION = "0.0.1"
4
+ VERSION = "0.1.0"
5
5
  end
data/lib/nextmail/wire.rb CHANGED
@@ -27,6 +27,20 @@ module Nextmail
27
27
  end
28
28
  end
29
29
 
30
+ def email_raw(body)
31
+ recipients(body).tap do |wire|
32
+ wire["raw"] = raw(wire["raw"]) if wire.key?("raw")
33
+ wire["followUpAt"] = instant(wire["followUpAt"]) if wire.key?("followUpAt")
34
+ end
35
+ end
36
+
37
+ def raw(value)
38
+ return [RawBody.read(value)].pack("m0") unless value.is_a?(String) && value.encoding != Encoding::BINARY
39
+ return value if BASE64_TEXT.match?(value.delete(BASE64_WHITESPACE))
40
+
41
+ raise ArgumentError, Defaults::MESSAGES[:RAW_MESSAGE_SHAPE]
42
+ end
43
+
30
44
  def with_attachments(body)
31
45
  body.dup.tap do |wire|
32
46
  next unless wire.key?("attachments")
data/lib/nextmail.rb CHANGED
@@ -23,8 +23,10 @@ require_relative "nextmail/raw_body"
23
23
  require_relative "nextmail/wire"
24
24
  require_relative "nextmail/image_crop"
25
25
  require_relative "nextmail/http"
26
+ require_relative "nextmail/event_stream"
26
27
  require_relative "nextmail/update_notice"
27
28
  require_relative "nextmail/transport"
29
+ require_relative "nextmail/live_event_stream"
28
30
  require_relative "nextmail/results"
29
31
  require_relative "nextmail/pagination"
30
32
  require_relative "nextmail/webhook"
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nextmail
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nextmail
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-10-25 00:00:00.000000000 Z
10
+ date: 2026-10-30 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: Send email and broadcasts, work with threads, drafts, labels, templates,
13
13
  rules, webhooks, tracking, contacts, audiences, sign-up forms, domains, suppressions,
@@ -26,9 +26,11 @@ files:
26
26
  - lib/nextmail/defaults.rb
27
27
  - lib/nextmail/endpoint.rb
28
28
  - lib/nextmail/errors.rb
29
+ - lib/nextmail/event_stream.rb
29
30
  - lib/nextmail/http.rb
30
31
  - lib/nextmail/image_crop.rb
31
32
  - lib/nextmail/languages.rb
33
+ - lib/nextmail/live_event_stream.rb
32
34
  - lib/nextmail/pagination.rb
33
35
  - lib/nextmail/protocol.rb
34
36
  - lib/nextmail/raw_body.rb
@@ -64,6 +66,7 @@ files:
64
66
  - lib/nextmail/resources/knowledge.rb
65
67
  - lib/nextmail/resources/labels.rb
66
68
  - lib/nextmail/resources/languages.rb
69
+ - lib/nextmail/resources/live_events.rb
67
70
  - lib/nextmail/resources/me.rb
68
71
  - lib/nextmail/resources/members.rb
69
72
  - lib/nextmail/resources/out_of_office.rb
@@ -75,6 +78,8 @@ files:
75
78
  - lib/nextmail/resources/senders.rb
76
79
  - lib/nextmail/resources/sending.rb
77
80
  - lib/nextmail/resources/settings.rb
81
+ - lib/nextmail/resources/share_links.rb
82
+ - lib/nextmail/resources/shared.rb
78
83
  - lib/nextmail/resources/signatures.rb
79
84
  - lib/nextmail/resources/subscriptions.rb
80
85
  - lib/nextmail/resources/support.rb
@@ -85,6 +90,7 @@ files:
85
90
  - lib/nextmail/resources/threads.rb
86
91
  - lib/nextmail/resources/tools.rb
87
92
  - lib/nextmail/resources/tracking.rb
93
+ - lib/nextmail/resources/traffic.rb
88
94
  - lib/nextmail/resources/webhooks.rb
89
95
  - lib/nextmail/resources/workspaces.rb
90
96
  - lib/nextmail/results.rb