Skip to content

Commit d15737b

Browse files
committed
Add Mailto link generation
1 parent d7adac3 commit d15737b

File tree

2 files changed

+291
-1
lines changed

2 files changed

+291
-1
lines changed

lib/supermail.rb

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require_relative "supermail/version"
44
require "action_mailer"
5+
require "erb"
56

67
module Supermail
78
class Error < StandardError; end
@@ -18,11 +19,40 @@ class Base
1819
def to = nil
1920
def from = nil
2021
def subject = nil
22+
def cc = []
23+
def bcc = []
2124
def body = ""
2225

26+
# Generate a mailto: URL with appropriate escaping.
27+
def mailto = MailTo.href(to:, from:, cc:, bcc:, subject:, body:)
28+
alias :mail_to :mailto
29+
2330
private def action_mailer_base_mail
24-
ActionMailer::Base.mail(to:, from:, subject:, body:)
31+
ActionMailer::Base.mail(to:, from:, cc:, bcc:, subject:, body:)
2532
end
2633
end
2734
end
35+
36+
module MailTo
37+
extend self
38+
39+
def href(to:, **params)
40+
q = query(**params)
41+
q.empty? ? "mailto:#{to}" : "mailto:#{to}?#{q}"
42+
end
43+
44+
def query(**params)
45+
params
46+
.compact # drop nils
47+
.reject { |k, v| v.is_a?(Array) && v.empty? } # drop empty arrays
48+
.map { |k, v| "#{k}=#{mailto_escape(v)}" }
49+
.join("&")
50+
end
51+
52+
private
53+
54+
def mailto_escape(str)
55+
ERB::Util.url_encode(str.to_s).tr("+", "%20")
56+
end
57+
end
2858
end

spec/mailto_spec.rb

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
# frozen_string_literal: true
2+
3+
require "spec_helper"
4+
5+
RSpec.describe Supermail::MailTo do
6+
describe ".href" do
7+
context "with minimal parameters" do
8+
it "generates a simple mailto URL" do
9+
result = described_class.href(to: "[email protected]")
10+
expect(result).to eq("mailto:[email protected]")
11+
end
12+
end
13+
14+
context "with all parameters" do
15+
let(:params) do
16+
{
17+
18+
19+
20+
21+
subject: "Test Subject",
22+
body: "Test body content"
23+
}
24+
end
25+
26+
it "generates a complete mailto URL with query parameters" do
27+
result = described_class.href(**params)
28+
expected_query = [
29+
"from=sender%40example.com",
30+
"cc=cc%40example.com",
31+
"bcc=bcc%40example.com",
32+
"subject=Test%20Subject",
33+
"body=Test%20body%20content"
34+
].join("&")
35+
36+
expect(result).to eq("mailto:[email protected]?#{expected_query}")
37+
end
38+
end
39+
40+
context "with nil parameters" do
41+
it "excludes nil parameters from the query string" do
42+
result = described_class.href(
43+
44+
from: nil,
45+
subject: "Hello",
46+
cc: nil
47+
)
48+
49+
expect(result).to eq("mailto:[email protected]?subject=Hello")
50+
end
51+
end
52+
53+
context "with empty arrays" do
54+
it "excludes empty arrays from the query string" do
55+
result = described_class.href(
56+
57+
cc: []
58+
)
59+
60+
expect(result).to eq("mailto:[email protected]")
61+
end
62+
end
63+
64+
context "with special characters" do
65+
it "properly escapes email addresses and content" do
66+
result = described_class.href(
67+
68+
subject: "Hello & Welcome!",
69+
body: "Line 1\nLine 2"
70+
)
71+
72+
expected_query = [
73+
"subject=Hello%20%26%20Welcome%21",
74+
"body=Line%201%0ALine%202"
75+
].join("&")
76+
77+
expect(result).to eq("mailto:[email protected]?#{expected_query}")
78+
end
79+
end
80+
81+
context "with unicode characters" do
82+
it "properly handles unicode content" do
83+
result = described_class.href(
84+
85+
subject: "Héllo Wørld! 🎉"
86+
)
87+
88+
expect(result).to include("mailto:[email protected]?subject=")
89+
expect(result).to include("H%C3%A9llo")
90+
expect(result).to include("W%C3%B8rld")
91+
end
92+
end
93+
94+
context "with multiple CC recipients" do
95+
it "handles array of CC recipients" do
96+
result = described_class.href(
97+
98+
99+
)
100+
101+
expect(result).to eq("mailto:[email protected]?cc=%5B%22cc1%40example.com%22%2C%20%22cc2%40example.com%22%5D")
102+
end
103+
end
104+
105+
context "with multiple BCC recipients" do
106+
it "handles array of BCC recipients" do
107+
result = described_class.href(
108+
109+
110+
)
111+
112+
expect(result).to eq("mailto:[email protected]?bcc=%5B%22bcc1%40example.com%22%2C%20%22bcc2%40example.com%22%5D")
113+
end
114+
end
115+
end
116+
117+
describe ".query" do
118+
context "with simple parameters" do
119+
it "generates a proper query string" do
120+
params = { subject: "Hello", body: "World" }
121+
result = described_class.query(**params)
122+
123+
expect(result).to eq("subject=Hello&body=World")
124+
end
125+
end
126+
127+
context "with no parameters" do
128+
it "returns an empty string" do
129+
result = described_class.query
130+
expect(result).to eq("")
131+
end
132+
end
133+
134+
context "with nil values" do
135+
it "excludes nil values from the query string" do
136+
params = { subject: "Hello", from: nil, body: "World" }
137+
result = described_class.query(**params)
138+
139+
expect(result).to eq("subject=Hello&body=World")
140+
end
141+
end
142+
143+
context "with special characters" do
144+
it "properly escapes parameter values" do
145+
params = { subject: "Hello & Goodbye", body: "Line 1\nLine 2" }
146+
result = described_class.query(**params)
147+
148+
expect(result).to eq("subject=Hello%20%26%20Goodbye&body=Line%201%0ALine%202")
149+
end
150+
end
151+
152+
context "with mixed parameter types" do
153+
it "handles strings, arrays, and nil values" do
154+
params = {
155+
subject: "Test",
156+
157+
from: nil,
158+
body: ""
159+
}
160+
result = described_class.query(**params)
161+
162+
expect(result).to include("subject=Test")
163+
expect(result).to include("cc=%5B%22test1%40example.com%22%2C%20%22test2%40example.com%22%5D")
164+
expect(result).to include("body=")
165+
expect(result).not_to include("from=")
166+
end
167+
end
168+
169+
context "with empty arrays" do
170+
it "excludes empty arrays from query string" do
171+
params = {
172+
subject: "Test",
173+
cc: [],
174+
bcc: [],
175+
176+
}
177+
result = described_class.query(**params)
178+
179+
expect(result).to eq("subject=Test&from=test%40example.com")
180+
expect(result).not_to include("cc=")
181+
expect(result).not_to include("bcc=")
182+
end
183+
end
184+
185+
context "with mixed empty arrays and nil values" do
186+
it "excludes both empty arrays and nil values from query string" do
187+
params = {
188+
subject: "Test",
189+
cc: [],
190+
bcc: nil,
191+
192+
body: ""
193+
}
194+
result = described_class.query(**params)
195+
196+
expect(result).to eq("subject=Test&from=test%40example.com&body=")
197+
expect(result).not_to include("cc=")
198+
expect(result).not_to include("bcc=")
199+
end
200+
end
201+
end
202+
203+
describe ".mailto_escape" do
204+
it "is private" do
205+
expect(described_class.private_methods).to include(:mailto_escape)
206+
end
207+
208+
context "accessing via send" do
209+
it "properly escapes strings" do
210+
result = described_class.send(:mailto_escape, "Hello World")
211+
expect(result).to eq("Hello%20World")
212+
end
213+
214+
it "converts plus signs to %20" do
215+
result = described_class.send(:mailto_escape, "[email protected]")
216+
expect(result).to eq("test%2Btag%40example.com")
217+
end
218+
219+
it "handles newlines" do
220+
result = described_class.send(:mailto_escape, "Line 1\nLine 2")
221+
expect(result).to eq("Line%201%0ALine%202")
222+
end
223+
224+
it "handles ampersands" do
225+
result = described_class.send(:mailto_escape, "Hello & Goodbye")
226+
expect(result).to eq("Hello%20%26%20Goodbye")
227+
end
228+
229+
it "converts non-string objects to strings" do
230+
result = described_class.send(:mailto_escape, 123)
231+
expect(result).to eq("123")
232+
end
233+
234+
it "handles special email characters" do
235+
result = described_class.send(:mailto_escape, "[email protected]")
236+
expect(result).to eq("user%2Btag%40example.com")
237+
end
238+
239+
it "handles carriage returns and line feeds" do
240+
result = described_class.send(:mailto_escape, "Line 1\r\nLine 2")
241+
expect(result).to eq("Line%201%0D%0ALine%202")
242+
end
243+
244+
it "handles unicode characters" do
245+
result = described_class.send(:mailto_escape, "Héllo 🎉")
246+
expect(result).to include("H%C3%A9llo")
247+
end
248+
249+
it "handles percent signs" do
250+
result = described_class.send(:mailto_escape, "50% off")
251+
expect(result).to eq("50%25%20off")
252+
end
253+
254+
it "handles empty strings" do
255+
result = described_class.send(:mailto_escape, "")
256+
expect(result).to eq("")
257+
end
258+
end
259+
end
260+
end

0 commit comments

Comments
 (0)