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 +4 -4
- data/lib/mailkite/delivery_method.rb +150 -0
- data/lib/mailkite/railtie.rb +16 -0
- data/lib/mailkite.rb +26 -5
- metadata +19 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1271ec234bd17506cc9aa35f15262d7708bb827786df2da37065457b90e0f106
|
|
4
|
+
data.tar.gz: 490ce824dc5ff8097190a893db918f906945c4239822387a9e39f10e4fa85f29
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
-
|
|
155
|
-
|
|
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 #{
|
|
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 #{
|
|
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.
|
|
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-
|
|
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
|