mailkite 0.11.0 → 0.13.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6150a49b6aeb81a4a2f47110dca45c08d0369de7492de5f09a93ba962aae6e22
4
- data.tar.gz: 7432bb5749a2e701d4a08cbaf92e0a386853e9953c7b4db4cfb23834f480c96d
3
+ metadata.gz: 1271ec234bd17506cc9aa35f15262d7708bb827786df2da37065457b90e0f106
4
+ data.tar.gz: 490ce824dc5ff8097190a893db918f906945c4239822387a9e39f10e4fa85f29
5
5
  SHA512:
6
- metadata.gz: e8651f3331f64a40afca30a2b3191d40deb9a9cf71393e8d9df8a1ae9273ad5d73828dc67b38a3f928d80b063ed32c03e337f418a7df1646eb0806dcbb70862c
7
- data.tar.gz: d8989b2cc0b1581990b0b4c0c1622e65fc89b9acec5ad26e4de734b583354fd777f779072d696d84cf267457e9a415c2b57c470872417c138caec224e6e13272
6
+ metadata.gz: 621ff2176d573d2df21262f249e018b5e0acce0ab79d296ecc40ee9e2e5d031c60009bd7514ea292fa9bb4a76183ceecbb4d573837e6388b3194e8b4231bd167
7
+ data.tar.gz: 3515d606332f77ba60b3304f0d7945cf3e4cfaf77799ea719b9a3d6b20c9f3736f7bb12152020b3d5d31a35ce4a84157e3c5e4418467015d5d5861fc43262e84
@@ -0,0 +1,150 @@
1
+ # Docs: docs/integrations/rails-delivery-method.md
2
+ #
3
+ # ActionMailer / Mail delivery method: deliver Mail::Message objects through
4
+ # the MailKite API (POST /v1/send) instead of SMTP.
5
+ #
6
+ # # Rails — the Railtie registers :mailkite automatically:
7
+ # config.action_mailer.delivery_method = :mailkite
8
+ # config.action_mailer.mailkite_settings = { api_key: ENV["MAILKITE_API_KEY"] }
9
+ #
10
+ # # Plain Mail (no Rails):
11
+ # Mail.defaults { delivery_method Mailkite::DeliveryMethod, api_key: ENV["MAILKITE_API_KEY"] }
12
+ #
13
+ # The `mail` gem is NOT a runtime dependency of this gem — it is required
14
+ # lazily here, on instantiation, so `require "mailkite"` stays zero-dependency.
15
+
16
+ module Mailkite
17
+ class DeliveryMethod
18
+ # Raised when a Mail::Message uses a feature the send API has no
19
+ # equivalent for (custom headers, inline cid: attachments, …). Refusing
20
+ # loudly beats silently dropping part of the message.
21
+ class UnsupportedFeatureError < StandardError; end
22
+
23
+ # Top-level headers the conversion consumes, plus the structural headers
24
+ # that only describe the MIME document (the API composes its own MIME).
25
+ # Anything else raises UnsupportedFeatureError — POST /v1/send takes no
26
+ # custom headers.
27
+ HANDLED_HEADERS = %w[
28
+ from to cc bcc subject reply-to in-reply-to
29
+ date message-id mime-version content-type content-transfer-encoding
30
+ ].freeze
31
+
32
+ attr_reader :settings
33
+
34
+ # settings: api_key (falls back to ENV["MAILKITE_API_KEY"]), optional
35
+ # base_url, or bring your own `client` — anything with `send(message)`
36
+ # (e.g. a Mailkite::Client, or a stub in tests). Takes precedence over
37
+ # api_key.
38
+ def initialize(settings = {})
39
+ require "mail" # lazy — only delivery-method users need the mail gem
40
+ @settings = settings
41
+ @client = settings[:client]
42
+ return if @client
43
+
44
+ api_key = settings[:api_key] || ENV["MAILKITE_API_KEY"]
45
+ if api_key.nil? || api_key.empty?
46
+ raise ArgumentError, "MailKite API key missing — pass api_key: in mailkite_settings or set MAILKITE_API_KEY"
47
+ end
48
+ @client = Client.new(api_key, settings[:base_url] || DEFAULT_BASE_URL)
49
+ end
50
+
51
+ # Deliver a Mail::Message via POST /v1/send. Returns the API response hash
52
+ # ({ "id" => …, "status" => … }); API failures raise Mailkite::Error.
53
+ def deliver!(mail)
54
+ @client.send(payload_for(mail))
55
+ end
56
+
57
+ private
58
+
59
+ # Convert a Mail::Message into a send-request body
60
+ # (sdks/spec/schemas/send-request.json). Raises on anything the API can't
61
+ # express rather than delivering a lossy message.
62
+ def payload_for(mail)
63
+ reject_custom_headers(mail)
64
+
65
+ from = addresses(mail[:from]).first
66
+ raise ArgumentError, "mail is missing a from address" if from.nil?
67
+ to = addresses(mail[:to])
68
+ raise ArgumentError, "mail is missing to recipients" if to.empty?
69
+
70
+ payload = { "from" => from, "to" => to }
71
+ cc = addresses(mail[:cc])
72
+ payload["cc"] = cc unless cc.empty?
73
+ bcc = addresses(mail[:bcc])
74
+ payload["bcc"] = bcc unless bcc.empty?
75
+ payload["subject"] = mail.subject unless mail.subject.nil?
76
+
77
+ reply_to = addresses(mail[:reply_to])
78
+ payload["replyTo"] = reply_to.join(", ") unless reply_to.empty?
79
+ in_reply_to = Array(mail.in_reply_to)
80
+ unless in_reply_to.empty?
81
+ payload["inReplyTo"] = in_reply_to.map { |id| id.start_with?("<") ? id : "<#{id}>" }.join(" ")
82
+ end
83
+
84
+ payload.merge(body_fields(mail)).merge(attachment_fields(mail))
85
+ end
86
+
87
+ # html/text from the message body: html_part/text_part when multipart,
88
+ # else the body under its own mime type. Any leftover MIME part that is
89
+ # neither the body nor an attachment has no API equivalent → raise.
90
+ def body_fields(mail)
91
+ fields = {}
92
+ if mail.multipart?
93
+ html = mail.html_part
94
+ text = mail.text_part
95
+ fields["html"] = html.decoded if html
96
+ fields["text"] = text.decoded if text
97
+ consumed = [html, text].compact + mail.attachments.to_a
98
+ leftover = mail.all_parts.reject { |p| p.multipart? || consumed.any? { |c| c.equal?(p) } }
99
+ unless leftover.empty?
100
+ raise UnsupportedFeatureError,
101
+ "the MailKite send API cannot express the #{leftover.first.mime_type} MIME part — only html/text bodies and attachments are supported"
102
+ end
103
+ else
104
+ body = mail.body.decoded
105
+ unless body.empty?
106
+ fields[mail.mime_type == "text/html" ? "html" : "text"] = body
107
+ end
108
+ end
109
+ fields
110
+ end
111
+
112
+ # Attachments become { filename, content: base64, contentType }. Inline
113
+ # (cid:) parts raise — the API has no Content-ID support, so embedded
114
+ # images would render broken.
115
+ def attachment_fields(mail)
116
+ return {} if mail.attachments.empty?
117
+
118
+ list = mail.attachments.map do |part|
119
+ if part.inline?
120
+ raise UnsupportedFeatureError,
121
+ "inline (cid:) attachments are not supported by the MailKite send API — attach the file normally or host the image at a URL"
122
+ end
123
+ {
124
+ "filename" => part.filename,
125
+ "content" => Base64.strict_encode64(part.body.decoded),
126
+ "contentType" => part.mime_type,
127
+ }
128
+ end
129
+ { "attachments" => list }
130
+ end
131
+
132
+ # POST /v1/send takes no custom headers (the schema is
133
+ # additionalProperties: false) — refuse them instead of silently dropping.
134
+ def reject_custom_headers(mail)
135
+ mail.header.fields.each do |field|
136
+ name = field.name.to_s.downcase
137
+ next if HANDLED_HEADERS.include?(name)
138
+
139
+ raise UnsupportedFeatureError,
140
+ "the MailKite send API has no support for the #{field.name} header — remove it, or deliver this message over SMTP relay instead"
141
+ end
142
+ end
143
+
144
+ # Formatted addresses for a header field (display names kept — the API
145
+ # parses RFC 5322 mailboxes); [] when the field is absent.
146
+ def addresses(field)
147
+ field.nil? ? [] : Array(field.formatted)
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,16 @@
1
+ # Docs: docs/integrations/rails-delivery-method.md
2
+ #
3
+ # Rails glue: registers :mailkite as an ActionMailer delivery method, so
4
+ # config.action_mailer.delivery_method = :mailkite
5
+ # just works. Loaded from lib/mailkite.rb only when Rails is present; outside
6
+ # Rails, register Mailkite::DeliveryMethod on `mail` directly.
7
+
8
+ module Mailkite
9
+ class Railtie < ::Rails::Railtie
10
+ initializer "mailkite.add_delivery_method", before: "action_mailer.set_configs" do
11
+ ActiveSupport.on_load(:action_mailer) do
12
+ ActionMailer::Base.add_delivery_method :mailkite, Mailkite::DeliveryMethod
13
+ end
14
+ end
15
+ end
16
+ end
data/lib/mailkite.rb CHANGED
@@ -15,7 +15,7 @@ require "base64"
15
15
  require "cgi"
16
16
 
17
17
  module Mailkite
18
- VERSION = "0.11.0"
18
+ VERSION = "0.13.0"
19
19
  DEFAULT_BASE_URL = "https://api.mailkite.dev"
20
20
  # Reject webhook events older than this (ms) to block replays. Pass 0 to disable.
21
21
  DEFAULT_TOLERANCE_MS = 5 * 60 * 1000
@@ -151,16 +151,28 @@ module Mailkite
151
151
  "DELETE" => Net::HTTP::Delete,
152
152
  }.freeze
153
153
 
154
- def initialize(api_key, base_url = DEFAULT_BASE_URL)
155
- @api_key = api_key
154
+ # Authenticate with a Bearer token. `api_key` (mk_live_…) and the `access_token:` keyword (an
155
+ # OAuth access token) are equivalent — both are Bearer credentials. For short-lived OAuth
156
+ # tokens pass `get_token:` — a callable invoked before each request for a fresh token (it wins):
157
+ # MailKite::Client.new("mk_live_...")
158
+ # MailKite::Client.new(access_token: my_oauth_token)
159
+ # MailKite::Client.new(get_token: -> { current_session.access_token })
160
+ def initialize(api_key = nil, base_url = DEFAULT_BASE_URL, access_token: nil, get_token: nil)
161
+ @api_key = api_key.nil? ? access_token : api_key
162
+ @get_token = get_token
156
163
  @base_url = base_url.sub(%r{/+\z}, "")
157
164
  end
158
165
 
166
+ # The Bearer token for one request: a fresh one from get_token if set, else the static key/token.
167
+ def token
168
+ @get_token ? @get_token.call : @api_key
169
+ end
170
+
159
171
  # Low-level request. Every method below is a one-liner on top of this.
160
172
  def request(method, path, body = nil)
161
173
  uri = URI(@base_url + path)
162
174
  req = VERBS.fetch(method).new(uri)
163
- req["Authorization"] = "Bearer #{@api_key}"
175
+ req["Authorization"] = "Bearer #{token}"
164
176
  unless body.nil?
165
177
  req["Content-Type"] = "application/json"
166
178
  req.body = JSON.generate(body)
@@ -178,7 +190,7 @@ module Mailkite
178
190
  qs = query.map { |k, v| "#{CGI.escape(k)}=#{CGI.escape(v.to_s)}" }.join("&")
179
191
  uri = URI("#{@base_url}/v1/attachments?#{qs}")
180
192
  req = Net::HTTP::Post.new(uri)
181
- req["Authorization"] = "Bearer #{@api_key}"
193
+ req["Authorization"] = "Bearer #{token}"
182
194
  req["Content-Type"] = content_type
183
195
  req.body = bytes
184
196
  perform(req, uri)
@@ -337,6 +349,10 @@ module Mailkite
337
349
  request("POST", "/api/routes", body)
338
350
  end
339
351
 
352
+ def deleteRoute(id)
353
+ request("DELETE", "/api/routes/#{id}")
354
+ end
355
+
340
356
  # --- Templates ------------------------------------------------------
341
357
  def listTemplates
342
358
  request("GET", "/api/templates")
@@ -476,3 +492,8 @@ module Mailkite
476
492
  end
477
493
  end
478
494
  end
495
+
496
+ # ActionMailer / Mail integration (config.action_mailer.delivery_method = :mailkite).
497
+ # The `mail` gem is not a runtime dependency — DeliveryMethod requires it lazily.
498
+ require_relative "mailkite/delivery_method"
499
+ require_relative "mailkite/railtie" if defined?(::Rails::Railtie)
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mailkite
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - MailKite
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-29 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2026-07-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: mail
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.7'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.7'
13
27
  description: Send and manage email over your own authenticated domain with MailKite.
14
28
  email:
15
29
  executables: []
@@ -17,6 +31,8 @@ extensions: []
17
31
  extra_rdoc_files: []
18
32
  files:
19
33
  - lib/mailkite.rb
34
+ - lib/mailkite/delivery_method.rb
35
+ - lib/mailkite/railtie.rb
20
36
  homepage: https://mailkite.dev/docs/libraries
21
37
  licenses:
22
38
  - MIT