sms_opener 0.1.1
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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +154 -0
- data/Rakefile +8 -0
- data/app/controllers/sms_opener/messages_controller.rb +105 -0
- data/app/views/sms_opener/messages/index.html.erb +431 -0
- data/config/routes.rb +14 -0
- data/lib/sms_opener/configuration.rb +29 -0
- data/lib/sms_opener/delivery_method.rb +121 -0
- data/lib/sms_opener/engine.rb +9 -0
- data/lib/sms_opener/interceptors/twilio_interceptor.rb +118 -0
- data/lib/sms_opener/message.rb +236 -0
- data/lib/sms_opener/railtie.rb +23 -0
- data/lib/sms_opener/templates/sms.html.erb +350 -0
- data/lib/sms_opener/version.rb +5 -0
- data/lib/sms_opener.rb +46 -0
- metadata +212 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 68eca8fb2a67758906029a23d8f55f5fd2addf948b17a2e64e95840c886813be
|
|
4
|
+
data.tar.gz: 16bcb4329919ba8492105e93fd74da3e136e597ca647f1c31ac912fcfa242667
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1c6d613f42706f7eed39dc383e818cc7b34cac70f71184e038716e5c70eceae49cc46faf460ea7089c1d6e5d8c0676388a334c7bb72e9cfbfff40ad4b6335ec8
|
|
7
|
+
data.tar.gz: ba01a21a56f80dc0f5ca985c37f2fd3928e984dafc11735c5e5f226c441e5c09f333cc5cea8078f9c60b2992b3f5fdf6e419b1b3abe8218d95d82b7c3e001890
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Scott Stubblebine
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# SmsOpener
|
|
2
|
+
|
|
3
|
+
Preview SMS and MMS messages in the browser instead of sending them in Ruby and Rails development environments.
|
|
4
|
+
|
|
5
|
+
Inspired by [`letter_opener`](https://github.com/ryanb/letter_opener) and [`letter_opener_web`](https://github.com/fgrehm/letter_opener_web), **`sms_opener`** provides:
|
|
6
|
+
|
|
7
|
+
1. **Automatic Browser Previews (via [`launchy`](https://github.com/copiousfreetime/launchy))**: Opens a mobile phone preview of every outgoing text message in your default browser as soon as it is sent.
|
|
8
|
+
2. **Mountable Rails Engine Web Inbox (`SmsOpener::Engine`)**: Browse, search, inspect, send test texts, and clear captured SMS/MMS messages at `/sms_opener`.
|
|
9
|
+
3. **Zero-Config `twilio-ruby` Interception**: Automatically intercepts `Twilio::REST::Client#messages.create` in development without making network requests or requiring real Twilio credentials.
|
|
10
|
+
4. **Direct Ruby API & Delivery Adapter**: Call `SmsOpener.deliver(to:, from:, body:)` or `SmsOpener::DeliveryMethod.new.deliver!(...)` from any custom SMS service object or adapter.
|
|
11
|
+
5. **Developer Conveniences**:
|
|
12
|
+
- **1-Click OTP Detection**: Automatically extracts 4–8 digit verification/2FA codes with a one-click copy button.
|
|
13
|
+
- **Clickable Magic Links**: Automatically extracts and links URLs inside SMS bodies.
|
|
14
|
+
- **Segment & Encoding Inspector**: Calculates `GSM-7` vs `UCS-2` encoding, character count, and multi-segment boundaries (`160`/`153` vs `70`/`67`).
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
Add `sms_opener` to your `Gemfile` under the `:development` group:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
group :development do
|
|
24
|
+
gem "sms_opener"
|
|
25
|
+
end
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Then run:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
bundle install
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
### 1. Automatic `twilio-ruby` Interception
|
|
39
|
+
|
|
40
|
+
If your application uses `twilio-ruby`, `sms_opener` automatically hooks into `Twilio::REST::Api::V2010::AccountContext::MessageList#create`.
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
client = Twilio::REST::Client.new(ENV["TWILIO_ACCOUNT_SID"], ENV["TWILIO_AUTH_TOKEN"])
|
|
44
|
+
|
|
45
|
+
message = client.messages.create(
|
|
46
|
+
to: "+15558675309",
|
|
47
|
+
from: "+15550001234",
|
|
48
|
+
body: "Your verification code is 849201"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
message.sid # => "SM..."
|
|
52
|
+
message.status # => "queued"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Instead of hitting Twilio's API, `sms_opener` saves the message to `tmp/sms_opener/`, opens the mobile preview in your browser, and returns a `Twilio::REST::Api::V2010::AccountContext::MessageInstance`.
|
|
56
|
+
|
|
57
|
+
### 2. Direct Ruby API (Custom SMS Services / Adapters)
|
|
58
|
+
|
|
59
|
+
If you use another SMS provider or a custom service object in your app, call `SmsOpener.deliver`:
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
SmsOpener.deliver(
|
|
63
|
+
to: "+15558675309",
|
|
64
|
+
from: "+15550001234",
|
|
65
|
+
body: "Sign in to your account: https://localhost:3000/magic?token=abc123",
|
|
66
|
+
media_urls: ["https://example.com/receipt.png"], # optional MMS attachments
|
|
67
|
+
provider: "custom_sms" # optional provider label
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Or use `SmsOpener::DeliveryMethod` as a pluggable adapter:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
delivery = SmsOpener::DeliveryMethod.new(launch_browser: true)
|
|
75
|
+
delivery.deliver!(to: "+15558675309", from: "+15550001234", body: "Hello world!")
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 3. Mountable Rails Web Inbox (`SmsOpener::Engine`)
|
|
79
|
+
|
|
80
|
+
To browse all captured texts in a web UI (and optionally disable automatic browser tab popups), mount `SmsOpener::Engine` in `config/routes.rb`:
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
Rails.application.routes.draw do
|
|
84
|
+
if Rails.env.development?
|
|
85
|
+
mount SmsOpener::Engine, at: "/sms_opener"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Visit `http://localhost:3000/sms_opener` to:
|
|
91
|
+
- View all captured SMS/MMS messages in a split-pane inbox
|
|
92
|
+
- Copy detected 2FA/OTP codes with one click
|
|
93
|
+
- Search messages by phone number, body, or OTP code
|
|
94
|
+
- Send test SMS messages right from the browser
|
|
95
|
+
- Query `/sms_opener/messages.json` in development or system tests
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Configuration
|
|
100
|
+
|
|
101
|
+
Configure `sms_opener` in `config/environments/development.rb` or an initializer (`config/initializers/sms_opener.rb`):
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
SmsOpener.configure do |config|
|
|
105
|
+
# Directory where captured SMS files are stored (default: Rails.root.join("tmp", "sms_opener"))
|
|
106
|
+
config.location = Rails.root.join("tmp", "sms_opener")
|
|
107
|
+
|
|
108
|
+
# Open each SMS in the browser via Launchy when sent (default: true)
|
|
109
|
+
# Set to false if you prefer checking the /sms_opener web inbox manually
|
|
110
|
+
config.launch_browser = true
|
|
111
|
+
|
|
112
|
+
# Automatically intercept twilio-ruby client.messages.create (default: true)
|
|
113
|
+
config.intercept_twilio = true
|
|
114
|
+
|
|
115
|
+
# Fallback sender number when `from:` is omitted (default: "+15550000000")
|
|
116
|
+
config.default_from = "+15550000000"
|
|
117
|
+
|
|
118
|
+
# Optional URI scheme override for Launchy (e.g., "file")
|
|
119
|
+
config.file_uri_scheme = nil
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Or via Rails environment configuration in `config/environments/development.rb`:
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
Rails.application.configure do
|
|
127
|
+
config.sms_opener.launch_browser = true
|
|
128
|
+
config.sms_opener.intercept_twilio = true
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Inspecting Captured Messages in Console or Tests
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
SmsOpener.messages # => Array of SmsOpener::Message (newest first)
|
|
138
|
+
last_sms = SmsOpener.messages.first
|
|
139
|
+
last_sms.to # => ["+15558675309"]
|
|
140
|
+
last_sms.from # => "+15550001234"
|
|
141
|
+
last_sms.body # => "Your verification code is 849201"
|
|
142
|
+
last_sms.otp_code # => "849201"
|
|
143
|
+
last_sms.urls # => []
|
|
144
|
+
last_sms.encoding # => "GSM-7"
|
|
145
|
+
last_sms.segments # => 1
|
|
146
|
+
|
|
147
|
+
SmsOpener.clear! # Deletes all stored messages
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_controller"
|
|
4
|
+
require "erb"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module SmsOpener
|
|
8
|
+
class MessagesController < ActionController::Base
|
|
9
|
+
protect_from_forgery with: :null_session if respond_to?(:protect_from_forgery)
|
|
10
|
+
|
|
11
|
+
def index
|
|
12
|
+
@messages = SmsOpener.messages
|
|
13
|
+
@selected_message = if params[:id]
|
|
14
|
+
SmsOpener.find(params[:id]) || @messages.first
|
|
15
|
+
else
|
|
16
|
+
@messages.first
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
if json_request?
|
|
20
|
+
render json: @messages.map(&:to_h)
|
|
21
|
+
else
|
|
22
|
+
render html: render_inbox_template.html_safe, layout: false
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def show
|
|
27
|
+
@message = SmsOpener.find(params[:id])
|
|
28
|
+
if @message.nil?
|
|
29
|
+
if json_request?
|
|
30
|
+
render json: { error: "Message not found" }, status: :not_found
|
|
31
|
+
else
|
|
32
|
+
render plain: "Message not found", status: :not_found
|
|
33
|
+
end
|
|
34
|
+
return
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if json_request?
|
|
38
|
+
render json: @message.to_h
|
|
39
|
+
else
|
|
40
|
+
html = File.exist?(@message.filepath) ? File.read(@message.filepath) : @message.rendered_html
|
|
41
|
+
render html: html.html_safe, layout: false
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def create
|
|
46
|
+
to = params[:to].to_s.strip
|
|
47
|
+
from = params[:from].to_s.strip
|
|
48
|
+
body = params[:body].to_s
|
|
49
|
+
|
|
50
|
+
if to.empty? || body.empty?
|
|
51
|
+
if json_request?
|
|
52
|
+
render json: { error: "Both 'to' and 'body' are required" }, status: :unprocessable_entity
|
|
53
|
+
else
|
|
54
|
+
redirect_to root_path
|
|
55
|
+
end
|
|
56
|
+
return
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
message = SmsOpener::DeliveryMethod.new(launch_browser: false).deliver!(
|
|
60
|
+
to: to,
|
|
61
|
+
from: from.empty? ? nil : from,
|
|
62
|
+
body: body,
|
|
63
|
+
provider: params[:provider].presence || "web_test"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if json_request?
|
|
67
|
+
render json: message.to_h, status: :created
|
|
68
|
+
else
|
|
69
|
+
redirect_to root_path(id: message.id)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def destroy
|
|
74
|
+
message = SmsOpener.find(params[:id])
|
|
75
|
+
message&.delete!
|
|
76
|
+
|
|
77
|
+
if json_request?
|
|
78
|
+
head :no_content
|
|
79
|
+
else
|
|
80
|
+
redirect_to root_path
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def clear
|
|
85
|
+
SmsOpener.clear!
|
|
86
|
+
|
|
87
|
+
if json_request?
|
|
88
|
+
head :no_content
|
|
89
|
+
else
|
|
90
|
+
redirect_to root_path
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def json_request?
|
|
97
|
+
request.format.json? || request.path.end_with?(".json") || request.headers["Accept"].to_s.include?("application/json")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def render_inbox_template
|
|
101
|
+
template_file = File.expand_path("../../views/sms_opener/messages/index.html.erb", __dir__)
|
|
102
|
+
ERB.new(File.read(template_file), trim_mode: "-").result(binding)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|