mitake_sms 2.0.0 → 3.0.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: 39775d8249e44177aa4d89da4716abc178ccd834e6c653549389a419f305d08c
4
- data.tar.gz: c47d00857751465b785cfe5da97b858b7c78722a8f65fd81d6e965de06541730
3
+ metadata.gz: 0cf544b42ee5585ab48ee884c607ac8172d612050808ad6d7a04d3d1b50f84de
4
+ data.tar.gz: 5ab5cb554757c3f064c130941bc5f6c7e1fb46b861d80f07031dbdc466eccf19
5
5
  SHA512:
6
- metadata.gz: 04bea4cc56b8294f4e5ecc6667a6378ed49bc13221c4766e6b3102421ad598b118d0793bdf7cf900340013fcfe83484f6b64b75d55c1f2e508330c7aef26c49c
7
- data.tar.gz: df170765544707a1b51ee0240811f4b379211f146725504d5b496c41792be713f13cf86ed4565689d51f2d09510f1b0a6df72a43512753bdc901fdb632402a38
6
+ metadata.gz: 528e04551ed3d097c43c513ea289096167b58c16b0a11637fc6ba0baa414a1082cc0ffbbac11ea470349aab2f536fe4280736340929d5eb468bbe7680ff72b29
7
+ data.tar.gz: f40efa10e466af71bea9cabb2d5f18dbf82a38250528f7cf319d7aac19ab02141fd0759105890f08c82f7e03e44f8881a801382a6cffbaca90bc747a35cdcdd6
data/CHANGELOG.md CHANGED
@@ -5,7 +5,59 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
8
+ ## [3.0.0] - 2026-07-28
9
+ ### Changed
10
+ - Raised `required_ruby_version` to `>= 3.3.0`, from `>= 2.6.0`. CI now runs against
11
+ 3.3, 3.4 and ruby/head rather than 3.3 alone
12
+ - Raised the Faraday floor to `>= 2.14.3`, which is the oldest release without
13
+ CVE-2026-54297 (`NestedParamsEncoder` recursion DoS), CVE-2026-33637 and
14
+ CVE-2026-25765 (SSRF via protocol-relative URL host override). This drops support
15
+ for the Faraday 1.x line, whose patched `1.10.6` cannot be expressed alongside the
16
+ 2.x floor in a single requirement
17
+
18
+ ### Added
19
+ - `Response#message_ids`, `#client_id`, `#duplicate?`, `#sms_point` and `#records`, exposing the
20
+ `[clientid]`, `Duplicate` and `smsPoint` fields the API returns but the parser previously dropped
21
+
22
+ ### Fixed
23
+ - `batch_send` raises `ArgumentError` when a field other than `:text` contains `$$` or a line break.
24
+ Both are `SmBulkSend` delimiters, so a `destname` holding either one silently shifted the remaining
25
+ fields or split the row in two, corrupting that message and every field after it
26
+ - `batch_send` forwards options such as `:objectID` and `:smsPointFlag` to the API. It previously
27
+ accepted and discarded everything except `:charset`, so `smsPoint` could never be requested
28
+ - Message bodies containing `\r\n` or a bare `\r` are now converted to ASCII code 6 like `\n` already
29
+ was. A `\r\n` body previously kept a stray carriage return, and a `\r`-only body was left unconverted
30
+ - Message bodies are transcoded to UTF-8 before sending, so a Big5 or otherwise non-UTF-8 string
31
+ arrives intact instead of as mojibake, and a batch mixing encodings no longer raises
32
+ `Encoding::CompatibilityError` while being assembled
33
+ - `Response#success?` returns false for a reply containing no records at all, rather than reporting
34
+ success off a vacuously true empty check
35
+ - `Response` now parses the reply as a list of records split on the `[clientid]` header, instead of
36
+ flattening every line into one hash. A `SmBulkSend` reply previously kept only the last message's
37
+ `msgid` and `statuscode`, silently discarding the result of every other message in the batch
38
+ - `Response#error` returns the documented reason for a failing `statuscode` (附錄二). It previously
39
+ read an `Error` field that does not exist in the API, so it was always `nil`
40
+ - `Response#success?` accepts statuscode `0` (預約傳送中), `2` and `4` in addition to `1`. Scheduled
41
+ sends were previously reported as failures. For a batch it is now true only if every record was
42
+ accepted
43
+ - `Response#account_point` is read as a reply-level value rather than being attached to whichever
44
+ record happened to be last
45
+ - `send_sms` now sends the callback URL as `response` and the dedup key as `clientid`, matching the
46
+ Mitake API spec. They were previously sent as `response_url` and `client_id`, which the API ignores,
47
+ so delivery report callbacks and 12-hour duplicate suppression never took effect
48
+
49
+ ### Removed
50
+ - Removed `advanced_batch_send` and `advanced_batch_send_with_limit`. They posted to a `SmPost`
51
+ endpoint that does not exist in the Mitake API, and passed recipient numbers and message bodies in
52
+ the query string, which leaked them into access logs and broke on large batches due to URL length
53
+ limits. Use `batch_send`, which implements the same `$$` format against `SmBulkSend`
54
+ - Removed the `charset` option from `send_sms` and `batch_send`. It only labelled the payload and
55
+ never converted it, so `charset: 'BIG5'` sent UTF-8 bytes tagged as Big5, guaranteeing mojibake.
56
+ Requests are now always UTF8 and non-UTF-8 input is transcoded for you
57
+ - Removed `batch_send_with_limit`. The 500 message cap is an API constraint rather than a tuning
58
+ knob, and `batch_send` already applies it
59
+ - Dropped the `faraday-multipart` runtime dependency. The multipart middleware was registered after
60
+ `url_encoded`, so it never handled a request, and the gem never sends file uploads
9
61
 
10
62
  ## [2.0.0] - 2025-05-26
11
63
  ### Changed
data/README.md CHANGED
@@ -8,13 +8,17 @@ A Ruby client for the Mitake SMS API, providing a simple and efficient way to se
8
8
 
9
9
  ## Features
10
10
 
11
- - Send single SMS messages
12
- - Send batch SMS messages with automatic handling of the 500 message API limit
13
- - UTF-8 encoding support by default
11
+ - Send single SMS messages (`SmSend`)
12
+ - Send batch SMS messages (`SmBulkSend`) with automatic handling of the 500 message API limit
13
+ - Always sends UTF-8, transcoding input in other encodings
14
+ - Parses every record of a reply, not just the first
14
15
  - Configurable API settings
15
- - Simple and intuitive API
16
16
  - Comprehensive error handling
17
17
 
18
+ Status queries (`SmQuery`) and cancelling a scheduled send (`SmCancel`) are not
19
+ implemented yet. To learn whether a message reached the handset, have Mitake call
20
+ the `response_url` callback.
21
+
18
22
  ## Installation
19
23
 
20
24
  Add this line to your application's Gemfile:
@@ -48,9 +52,14 @@ MitakeSms.configure do |config|
48
52
  config.username = 'your_username' # Your Mitake SMS API username
49
53
  config.password = 'your_password' # Your Mitake SMS API password
50
54
  config.api_url = 'https://smsapi.mitake.com.tw/api/mtk/' # Default API URL
55
+ config.timeout = 30 # Read timeout in seconds, default 30
56
+ config.open_timeout = 5 # Connection timeout in seconds, default 5
51
57
  end
52
58
  ```
53
59
 
60
+ The default `api_url` targets the HTTP API. If your account is provisioned for the
61
+ B2C variant, set the path to `/b2c/mtk/` instead; the endpoints are otherwise identical.
62
+
54
63
  ### Sending a Single SMS
55
64
 
56
65
  ```ruby
@@ -68,153 +77,129 @@ end
68
77
  response = MitakeSms.send_sms(
69
78
  to: '0912345678',
70
79
  text: 'Hello with options!',
71
- destname: 'John Doe', # Recipient name or integration key value
72
- response_url: 'https://your-callback-url.com/delivery-reports',
73
- client_id: 'your-client-reference-id',
74
- charset: 'BIG5' # Override the default UTF-8 encoding if needed
80
+ destname: 'John Doe', # Recipient name or integration key value (sent as `destname`)
81
+ response_url: 'https://your-callback-url.com/delivery-reports', # Sent as `response`
82
+ client_id: 'your-client-reference-id', # Sent as `clientid`, used by Mitake to de-duplicate
83
+ dlvtime: '20250526120000', # Any other documented SmSend field is forwarded as-is
84
+ vldtime: '20250527120000'
75
85
  )
76
86
  ```
77
87
 
88
+ Requests are always sent as UTF-8. Strings in another encoding are transcoded for
89
+ you, so `text: '中文'.encode('BIG5')` arrives intact.
90
+
78
91
  ### Sending Multiple SMS in Batch
79
92
 
93
+ Each message accepts the same fields as a single send. `client_id` is generated for
94
+ you when omitted; Mitake uses it to suppress duplicates within 12 hours.
95
+
80
96
  ```ruby
81
97
  messages = [
82
- { to: '0912345678', text: 'First message' },
83
- { to: '0922333444', text: 'Second message', from: 'YourBrand' },
84
- { to: '0933555777', text: 'Third message', response_url: 'https://your-callback-url.com/reports' }
98
+ {
99
+ client_id: 'unique-id-20250525-001', # Optional, auto-generated when absent
100
+ to: '0912345678', # Required
101
+ dlvtime: '20250526120000', # Optional scheduled delivery (YYYYMMDDhhmmss)
102
+ vldtime: '20250527120000', # Optional validity deadline (YYYYMMDDhhmmss)
103
+ destname: '大寶', # Optional recipient name
104
+ response_url: 'https://callback.url', # Optional per-message callback
105
+ text: '這是一則測試簡訊' # Required
106
+ },
107
+ { to: '0922333444', text: '這是另一則測試簡訊' }
85
108
  ]
86
109
 
87
- # Automatically handles batches according to the Mitake SMS API limit (500 messages per request)
88
- # If you send more than 500 messages, they will be automatically split into multiple requests
89
- # Uses UTF-8 encoding by default
90
110
  response = MitakeSms.batch_send(messages)
91
111
 
92
- # You can specify a different character encoding if needed
93
- response = MitakeSms.batch_send(messages, charset: 'BIG5')
112
+ # Any other documented SmBulkSend field is forwarded as a query parameter
113
+ response = MitakeSms.batch_send(messages, smsPointFlag: '1', objectID: 'nightly-run')
114
+ ```
94
115
 
95
- # If fewer than 500 messages, you'll get a single response
96
- if response.is_a?(MitakeSms::Response) && response.success?
97
- puts "Batch sent successfully!"
98
- puts "Message ID: #{response.message_id}"
99
- puts "Remaining points: #{response.account_point}"
116
+ Batches over 500 messages are split automatically, which is the API's per-request
117
+ limit. Under the limit you get one `Response`; over it you get an array of them.
100
118
 
101
- # If more than 500 messages, you'll get an array of responses
102
- elsif response.is_a?(Array)
103
- response.each_with_index do |batch_response, index|
104
- if batch_response.success?
105
- puts "Batch #{index + 1} sent successfully!"
106
- puts "Message ID: #{batch_response.message_id}"
107
- puts "Remaining points: #{batch_response.account_point}"
108
- else
109
- puts "Batch #{index + 1} failed: #{batch_response.error}"
110
- end
119
+ ```ruby
120
+ Array(response).each_with_index do |batch, index|
121
+ if batch.success?
122
+ puts "Batch #{index + 1} sent. Message IDs: #{batch.message_ids.join(', ')}"
123
+ puts "Remaining points: #{batch.account_point}"
124
+ else
125
+ puts "Batch #{index + 1} failed: #{batch.error}"
111
126
  end
112
- else
113
- puts "Failed to send batch: #{response.error}"
114
127
  end
115
128
  ```
116
129
 
117
- ### Sending Large Batches with Custom Limit
118
-
119
- ```ruby
120
- # Create a large batch of messages
121
- messages = (1..1000).map do |i|
122
- { to: '0912345678', text: "Message #{i}" }
123
- end
130
+ Line breaks in `text` are converted to ASCII code 6, as the API requires, so
131
+ `"First line\nSecond line"` renders as two lines on the handset. Messages longer
132
+ than one SMS are split by Mitake unless your account has long-message permission.
124
133
 
125
- # The Mitake SMS API has a limit of 500 messages per request
126
- # However, you can set a lower limit if needed for your use case
127
- # This will split into batches of 300 messages each
128
- # Uses UTF-8 encoding by default
129
- responses = MitakeSms.batch_send_with_limit(messages, 300)
134
+ #### Delimiters in batch fields
130
135
 
131
- # You can specify a different character encoding if needed
132
- responses = MitakeSms.batch_send_with_limit(messages, 300, charset: 'BIG5')
136
+ The batch wire format separates fields with `$$` and rows with a newline. Either
137
+ one inside `client_id`, `to`, `dlvtime`, `vldtime`, `destname` or `response_url`
138
+ would shift every field after it, so `batch_send` raises `ArgumentError` naming
139
+ the offending message and field instead of sending a corrupted row:
133
140
 
134
- # Process the array of responses
135
- responses.each_with_index do |batch_response, index|
136
- if batch_response.success?
137
- puts "Batch #{index + 1} sent successfully!"
138
- else
139
- puts "Batch #{index + 1} failed: #{batch_response.error}"
140
- end
141
- end
141
+ ```ruby
142
+ MitakeSms.batch_send([{ to: '0912345678', destname: 'A$$B', text: 'hi' }])
143
+ # => ArgumentError: messages[0][:destname] must not contain "$$" or a line break, ...
142
144
  ```
143
145
 
144
- ### Sending Batch SMS with Advanced Format
146
+ `text` is exempt because it is the last field on the row, so a message body may
147
+ contain `$$` freely. Mitake's documentation does not define an escape sequence,
148
+ though, so if a body containing `$$` ever comes back truncated, send it through
149
+ `send_sms` instead.
150
+
151
+ ### Reading a Response
145
152
 
146
- The batch_send method now uses the advanced format by default, which provides more control over each message in the batch, including scheduled delivery, validity period, recipient name, and more:
153
+ A batch reply contains one record per message, each introduced by a `[clientid]` line,
154
+ followed by an `AccountPoint` shared by the whole reply. A single send may omit the
155
+ `[clientid]` header; either shape is parsed the same way. `MitakeSms::Response` keeps
156
+ every record rather than collapsing them:
147
157
 
148
158
  ```ruby
149
- # Create messages with advanced options
150
- messages = [
151
- {
152
- client_id: 'unique-id-20250525-001', # Client reference ID (auto-generated if not provided)
153
- to: '0912345678', # Required recipient phone number
154
- dlvtime: '20250526120000', # Optional delivery time (YYYYMMDDhhmmss)
155
- vldtime: '20250527120000', # Optional validity period (YYYYMMDDhhmmss)
156
- destname: '大寶', # Optional recipient name
157
- response_url: 'https://callback.url', # Optional callback URL
158
- text: '這是一則測試簡訊' # Required message content
159
- },
160
- {
161
- # client_id will be auto-generated if not provided
162
- to: '0922333444',
163
- text: '這是另一則測試簡訊'
164
- # Other fields are optional
165
- }
166
- ]
159
+ response.success? # true only if there is at least one record and every one was accepted
160
+ response.error? # the inverse of success?
161
+ response.error # nil on success, otherwise the documented reason for each failed record
162
+ response.code # statuscode of the first record
163
+ response.message_id # msgid of the first record
164
+ response.message_ids # msgid of every record, for batch sends
165
+ response.client_id # clientid of the first record
166
+ response.account_point # remaining points, shared by the whole reply
167
+ response.duplicate? # true when Mitake suppressed a resend of the same clientid
168
+ response.sms_point # points deducted, only present when smsPointFlag=1 was sent
169
+ response.records # raw per-message hashes, if you need a field not listed above
170
+ response.raw_response # the untouched reply body
171
+ ```
167
172
 
168
- # Note about ClientID:
169
- # - ClientID is used by Mitake to prevent duplicate message sending within 12 hours
170
- # - If not provided, a unique ID will be automatically generated using timestamp and random values
171
- # - For custom tracking, you can provide your own unique ClientID
172
- #
173
- # Note about message text formatting:
174
- # - If your message text contains line breaks (\n), they will be automatically converted
175
- # to ASCII code 6 as required by the Mitake API
176
- # - Example: "First line\nSecond line" will be properly displayed with a line break on the recipient's device
177
- # - Special characters like '&' are automatically URL encoded to ensure proper transmission
178
- # - Long messages will be automatically split into multiple SMS messages if your account doesn't
179
- # have long message permissions
180
-
181
- # Send using batch_send (automatically handles the advanced format)
182
- response = MitakeSms.batch_send(messages)
173
+ Accepted status codes are `0`, `1`, `2` and `4`. An empty or unparseable body is not a
174
+ success, and `error` reports `'Empty or unparseable response'` for it.
183
175
 
184
- # Process response similar to regular batch sending
185
- if response.is_a?(MitakeSms::Response) && response.success?
186
- puts "Advanced batch sent successfully!"
187
- puts "Message ID: #{response.message_id}"
188
- puts "Remaining points: #{response.account_point}"
189
- elsif response.is_a?(Array)
190
- response.each_with_index do |batch_response, index|
191
- if batch_response.success?
192
- puts "Batch #{index + 1} sent successfully!"
193
- else
194
- puts "Batch #{index + 1} failed: #{batch_response.error}"
195
- end
196
- end
197
- end
198
- ```
176
+ Note that `statuscode=0` means 預約傳送中 (queued for scheduled delivery) and counts as a success.
177
+ Failures are the numeric codes `5`–`9` and the letter codes documented in 附錄二, such as `e`
178
+ (帳號、密碼錯誤) or `v` (無效的手機號碼).
199
179
 
200
180
  ### Error Handling
201
181
 
202
- The gem provides specific error classes for different types of errors:
182
+ HTTP status codes are mapped to specific error classes, while transport failures are
183
+ raised as their original `Faraday::Error` subclass so you can retry on timeouts alone:
203
184
 
204
185
  ```ruby
205
186
  begin
206
- response = MitakeSms.send_sms('invalid', 'test')
187
+ response = MitakeSms.send_sms(to: '0912345678', text: 'test')
207
188
  rescue MitakeSms::Client::AuthenticationError => e
208
189
  puts "Authentication failed: #{e.message}"
209
190
  rescue MitakeSms::Client::InvalidRequestError => e
210
191
  puts "Invalid request: #{e.message}"
211
192
  rescue MitakeSms::Client::ServerError => e
212
193
  puts "Server error: #{e.message}"
213
- rescue MitakeSms::Client::Error => e
214
- puts "An error occurred: #{e.message}"
194
+ rescue Faraday::TimeoutError => e
195
+ puts "Timed out, safe to retry: #{e.message}"
215
196
  end
216
197
  ```
217
198
 
199
+ Note that `SmBulkSend` takes the username and password as query string parameters,
200
+ which the API mandates and the gem cannot avoid. They are protected in transit by
201
+ HTTPS, but if you log outbound request URLs, filter them at that layer.
202
+
218
203
  ## Development
219
204
 
220
205
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -225,6 +210,7 @@ To install this gem onto your local machine, run `bundle exec rake install`.
225
210
 
226
211
  Bug reports and pull requests are welcome on GitHub at https://github.com/7a6163/mitake_sms.
227
212
 
213
+
228
214
  ## License
229
215
 
230
216
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'faraday'
4
- require 'faraday/multipart'
4
+ require 'securerandom'
5
5
  require_relative 'configuration'
6
6
  require_relative 'response'
7
7
 
@@ -12,6 +12,21 @@ module MitakeSms
12
12
  class InvalidRequestError < Error; end
13
13
  class ServerError < Error; end
14
14
 
15
+ # The Mitake API represents a line break inside smbody as ASCII code 6.
16
+ LINE_BREAK = 6.chr
17
+
18
+ BATCH_LIMIT = 500
19
+
20
+ # The API also accepts Big5, but it only labels the payload and never
21
+ # converts it, so sending anything other than UTF8 just invites mojibake.
22
+ CHARSET = 'UTF8'
23
+
24
+ FIELD_SEPARATOR = '$$'
25
+
26
+ # Every SmBulkSend field except smbody, in wire order. A '$$' or a line
27
+ # break in any of these shifts the remaining fields of the row.
28
+ STRUCTURAL_FIELDS = %i[client_id to dlvtime vldtime destname response_url].freeze
29
+
15
30
  # Initialize a new MitakeSms::Client
16
31
  # @param config [MitakeSms::Configuration] configuration object
17
32
  def initialize(config = nil)
@@ -25,220 +40,97 @@ module MitakeSms
25
40
  # @param destname [String] recipient name or key value for system integration (optional)
26
41
  # @param response_url [String] callback URL for delivery reports (optional)
27
42
  # @param client_id [String] client reference ID (optional)
28
- # @param charset [String] character encoding, defaults to 'UTF8' (optional)
29
- # @param options [Hash] additional options (optional)
43
+ # @param options [Hash] any other documented SmSend field, such as
44
+ # :dlvtime, :vldtime, :objectID or :smsPointFlag
30
45
  # @return [MitakeSms::Response] response object
31
- def send_sms(to:, text:, destname: nil, response_url: nil, client_id: nil, charset: 'UTF8', **options)
32
- require 'uri'
33
-
34
- # Create options hash with only non-nil values
35
- param_options = {}
36
- param_options[:destname] = destname if destname
37
- param_options[:response_url] = response_url if response_url
38
- param_options[:client_id] = client_id if client_id
39
-
40
- # Replace any newline characters with ASCII code 6 (ACK)
41
- # This is required by the Mitake API to represent line breaks
42
- processed_text = text.to_s.gsub("\n", 6.chr)
43
-
44
- # Prepare query parameters - only CharsetURL is sent as query parameter
45
- query_params = {
46
- CharsetURL: charset
47
- }
48
-
49
- # Prepare form parameters - all other parameters are sent in the POST body
46
+ def send_sms(to:, text:, destname: nil, response_url: nil, client_id: nil, **options)
50
47
  form_params = {
51
48
  username: @config.username,
52
49
  password: @config.password,
53
50
  dstaddr: to,
54
- smbody: processed_text
55
- }.merge(param_options).merge(options)
56
-
57
- # Construct the endpoint URL
58
- endpoint = "SmSend"
51
+ smbody: normalize_body(text)
52
+ }
53
+ form_params[:destname] = destname if destname
54
+ form_params[:response] = response_url if response_url
55
+ form_params[:clientid] = client_id if client_id
59
56
 
60
- response = @connection.post(endpoint) do |req|
61
- req.params = query_params
62
- req.body = form_params
57
+ perform_request('SmSend', params: { CharsetURL: CHARSET }) do |req|
58
+ req.body = form_params.merge(options)
63
59
  end
64
-
65
- handle_response(response)
66
60
  end
67
61
 
68
- # Send multiple SMS in a single request
62
+ # Send multiple SMS in a single request, splitting at the API's 500 message limit
69
63
  # @param messages [Array<Hash>] array of message hashes
70
- # Each hash should contain :to and :text keys, and can include :from, :response_url, :client_id
71
- # @param options [Hash] additional options
72
- # @option options [String] :charset character encoding, defaults to 'UTF8'
73
- # @option options [Boolean] :skip_encoding skip URL encoding (for tests)
64
+ # Each hash requires :to and :text, and may include :client_id, :dlvtime,
65
+ # :vldtime, :destname and :response_url
66
+ # @param options [Hash] any other documented SmBulkSend field, such as
67
+ # :objectID or :smsPointFlag
68
+ # @raise [ArgumentError] if a field other than :text contains '$$' or a line break
74
69
  # @return [MitakeSms::Response, Array<MitakeSms::Response>] response object or array of response objects if batch was split
75
70
  def batch_send(messages, options = {})
76
- # Mitake SMS API has a limit of 500 messages per request
77
- # Automatically split larger batches into multiple requests of 500 messages each
78
- batch_send_with_limit(messages, 500, options)
79
- end
80
-
81
- # Send multiple SMS in a single request with a limit per request
82
- # @param messages [Array<Hash>] array of message hashes
83
- # Each hash should contain :to and :text keys, and can include :from, :response_url, :client_id
84
- # @param limit [Integer] maximum number of messages per request (default: 500)
85
- # @param options [Hash] additional options
86
- # @option options [String] :charset character encoding, defaults to 'UTF8'
87
- # @option options [Boolean] :skip_encoding skip URL encoding (for tests)
88
- # @return [MitakeSms::Response, Array<MitakeSms::Response>] response object or array of response objects if batch was split
89
- def batch_send_with_limit(messages, limit = 500, options = {})
90
- charset = options[:charset] || 'UTF8'
91
-
92
- # If messages count is within the limit, use the regular batch send
93
- return send_batch(messages, charset, options) if messages.size <= limit
94
-
95
- # Otherwise, split into batches of the specified limit
96
- responses = []
97
- messages.each_slice(limit) do |batch|
98
- responses << send_batch(batch, charset, options)
99
- end
100
-
101
- # Return array of responses
102
- responses
103
- end
104
-
105
- # Send multiple SMS in a single request using advanced format
106
- # @param messages [Array<Hash>] array of message hashes with advanced options
107
- # Each hash can contain the following keys:
108
- # - :client_id [String] client reference ID (required)
109
- # - :to [String] recipient phone number (required)
110
- # - :dlvtime [String] delivery time in format YYYYMMDDHHMMSS (optional)
111
- # - :vldtime [String] valid until time in format YYYYMMDDHHMMSS (optional)
112
- # - :dest_name [String] recipient name (optional)
113
- # - :response [String] callback URL for delivery reports (optional)
114
- # - :text [String] message content (required)
115
- # @param options [Hash] additional options
116
- # @option options [String] :charset character encoding, defaults to 'UTF8'
117
- # @option options [Boolean] :skip_encoding skip URL encoding (for tests)
118
- # @return [MitakeSms::Response, Array<MitakeSms::Response>] response object or array of response objects if batch was split
119
- def advanced_batch_send(messages, options = {})
120
- # Mitake SMS API has a limit of 500 messages per request
121
- # Automatically split larger batches into multiple requests of 500 messages each
122
- advanced_batch_send_with_limit(messages, 500, options)
123
- end
124
-
125
- # Send multiple SMS in a single request with a limit per request using advanced format
126
- # @param messages [Array<Hash>] array of message hashes with advanced options
127
- # @param limit [Integer] maximum number of messages per request (default: 500)
128
- # @param options [Hash] additional options
129
- # @option options [String] :charset character encoding, defaults to 'UTF8'
130
- # @option options [Boolean] :skip_encoding skip URL encoding (for tests)
131
- # @return [MitakeSms::Response, Array<MitakeSms::Response>] response object or array of response objects if batch was split
132
- def advanced_batch_send_with_limit(messages, limit = 500, options = {})
133
- charset = options[:charset] || 'UTF8'
134
-
135
- # If messages count is within the limit, use the regular batch send
136
- return send_advanced_batch(messages, charset, options) if messages.size <= limit
71
+ messages.each_with_index { |msg, index| validate_row!(msg, index) }
137
72
 
138
- # Otherwise, split into batches of the specified limit
139
- responses = []
140
- messages.each_slice(limit) do |batch|
141
- responses << send_advanced_batch(batch, charset, options)
142
- end
143
-
144
- # Return array of responses
145
- responses
73
+ batch_send_with_limit(messages, BATCH_LIMIT, options)
146
74
  end
147
75
 
148
76
  private
149
77
 
150
- # Internal method to send a single batch
151
- # @param batch [Array<Hash>] array of message hashes for a single batch
152
- # @param charset [String] character encoding, defaults to 'UTF8'
153
- # @param options [Hash] additional options
154
- # @return [MitakeSms::Response] response object
155
- def send_batch(batch, charset = 'UTF8', options = {})
156
- require 'uri'
157
-
158
- # Format each message according to the advanced format
159
- # ClientID $$ dstaddr $$ dlvtime $$ vldtime $$ destname $$ response $$ smbody
160
- data = batch.map do |msg|
161
- # ClientID is required and must be unique
162
- # If not provided, generate a unique ID
163
- client_id = msg[:client_id]
164
- if client_id.nil? || client_id.empty?
165
- client_id = generate_unique_client_id
166
- end
167
-
168
- to = msg[:to]
169
- dlvtime = msg[:dlvtime] || ''
170
- vldtime = msg[:vldtime] || ''
171
- dest_name = msg[:destname] || ''
172
- response_url = msg[:response_url] || ''
78
+ def batch_send_with_limit(messages, limit, options)
79
+ return send_batch(messages, options) if messages.size <= limit
173
80
 
174
- # Replace any newline characters in the message text with ASCII code 6 (ACK)
175
- # This is required by the Mitake API to represent line breaks within message content
176
- processed_text = msg[:text].to_s.gsub("\n", 6.chr)
177
-
178
- # Format according to API documentation: ClientID $$ dstaddr $$ dlvtime $$ vldtime $$ destname $$ response $$ smbody
179
- [client_id, to, dlvtime, vldtime, dest_name, response_url, processed_text].join('$$')
180
- end.join("\n")
81
+ messages.each_slice(limit).map { |batch| send_batch(batch, options) }
82
+ end
181
83
 
182
- # Parameters for the request
84
+ def send_batch(batch, options)
183
85
  query_params = {
184
86
  username: @config.username,
185
87
  password: @config.password,
186
- Encoding_PostIn: charset
187
- }
88
+ Encoding_PostIn: CHARSET
89
+ }.merge(options)
188
90
 
189
- # According to the API documentation, the data should be in the request body
190
- response = @connection.post('SmBulkSend') do |req|
191
- req.params = query_params
192
- req.body = data
91
+ perform_request('SmBulkSend', params: query_params) do |req|
92
+ req.body = batch.map { |msg| format_batch_row(msg) }.join("\n")
193
93
  req.headers['Content-Type'] = 'text/plain'
194
94
  end
195
-
196
- handle_response(response)
197
95
  end
198
96
 
199
- # Internal method to send a single batch using advanced format
200
- # @param batch [Array<Hash>] array of message hashes for a single batch with advanced options
201
- # @param charset [String] character encoding, defaults to 'UTF8'
202
- # @param options [Hash] additional options
203
- # @return [MitakeSms::Response] response object
204
- def send_advanced_batch(batch, charset = 'UTF8', options = {})
205
- require 'uri'
206
-
207
- # Format each message according to the advanced format
208
- # ClientID $$ dstaddr $$ dlvtime $$ vldtime $$ destname $$ response $$ smbody
209
- data = batch.map do |msg|
210
- # ClientID is required and must be unique
211
- # If not provided, generate a unique ID
212
- client_id = msg[:client_id]
213
- if client_id.nil? || client_id.empty?
214
- client_id = generate_unique_client_id
215
- end
216
-
217
- to = msg[:to]
218
- dlvtime = msg[:dlvtime] || ''
219
- vldtime = msg[:vldtime] || ''
220
- dest_name = msg[:dest_name] || ''
221
- response_url = msg[:response] || ''
97
+ # ClientID $$ dstaddr $$ dlvtime $$ vldtime $$ destname $$ response $$ smbody
98
+ def format_batch_row(msg)
99
+ client_id = msg[:client_id]
100
+ client_id = generate_unique_client_id if client_id.to_s.empty?
101
+
102
+ [
103
+ client_id,
104
+ msg[:to],
105
+ msg[:dlvtime],
106
+ msg[:vldtime],
107
+ msg[:destname],
108
+ msg[:response_url],
109
+ normalize_body(msg[:text])
110
+ ].join(FIELD_SEPARATOR)
111
+ end
222
112
 
223
- # Replace any newline characters in the message text with ASCII code 6 (ACK)
224
- # This is required by the Mitake API to represent line breaks within message content
225
- processed_text = msg[:text].to_s.gsub("\n", 6.chr)
113
+ def validate_row!(msg, index)
114
+ STRUCTURAL_FIELDS.each do |field|
115
+ value = msg[field].to_s
116
+ next unless value.include?(FIELD_SEPARATOR) || value.match?(/[\r\n]/)
226
117
 
227
- [client_id, to, dlvtime, vldtime, dest_name, response_url, processed_text].join('$$')
228
- end.join("\n")
118
+ raise ArgumentError,
119
+ "messages[#{index}][:#{field}] must not contain #{FIELD_SEPARATOR.inspect} or a line break, " \
120
+ 'because both are SmBulkSend delimiters'
121
+ end
122
+ end
229
123
 
230
- # All parameters should be sent as query string parameters
231
- query_params = {
232
- username: @config.username,
233
- password: @config.password,
234
- data: data,
235
- Encoding_PostIn: charset
236
- }
124
+ # Mitake expects UTF8 bytes with ASCII code 6 standing in for a line break.
125
+ # Transcoding also keeps a batch of mixed-encoding strings from failing to join.
126
+ def normalize_body(text)
127
+ text.to_s.encode(Encoding::UTF_8).gsub(/\r\n?|\n/, LINE_BREAK)
128
+ end
237
129
 
238
- # Use empty body with all parameters in query string
239
- response = @connection.post('SmPost') do |req|
240
- req.params = query_params
241
- req.body = {}
130
+ def perform_request(endpoint, params:)
131
+ response = @connection.post(endpoint) do |req|
132
+ req.params = params
133
+ yield req
242
134
  end
243
135
 
244
136
  handle_response(response)
@@ -247,7 +139,6 @@ module MitakeSms
247
139
  def build_connection
248
140
  Faraday.new(url: @config.api_url) do |conn|
249
141
  conn.request :url_encoded
250
- conn.request :multipart
251
142
  conn.adapter Faraday.default_adapter
252
143
  conn.options.timeout = @config.timeout
253
144
  conn.options.open_timeout = @config.open_timeout
@@ -269,15 +160,10 @@ module MitakeSms
269
160
  end
270
161
  end
271
162
 
272
- # Generate a unique client ID for SMS messages
273
- # @return [String] a unique ID combining timestamp and random values
163
+ # Mitake uses the client ID to suppress duplicate sends within 12 hours,
164
+ # so it has to be unique per message rather than merely random.
274
165
  def generate_unique_client_id
275
- require 'securerandom'
276
-
277
- # Generate a unique ID using timestamp (to milliseconds) and a random UUID portion
278
- timestamp = Time.now.strftime('%Y%m%d%H%M%S%L')
279
- random_part = SecureRandom.uuid.gsub('-', '')[0, 8]
280
- "#{timestamp}-#{random_part}"
166
+ "#{Time.now.strftime('%Y%m%d%H%M%S%L')}-#{SecureRandom.uuid.delete('-')[0, 8]}"
281
167
  end
282
168
  end
283
169
  end
@@ -2,37 +2,128 @@
2
2
 
3
3
  module MitakeSms
4
4
  class Response
5
- attr_reader :raw_response, :code, :message_id, :account_point, :error
5
+ # 附錄一 / 附錄二:statuscode 對應說明
6
+ STATUS_MESSAGES = {
7
+ '0' => '預約傳送中',
8
+ '1' => '已送達業者',
9
+ '2' => '已送達業者',
10
+ '4' => '已送達手機',
11
+ '5' => '內容有錯誤',
12
+ '6' => '門號有錯誤',
13
+ '7' => '簡訊已停用',
14
+ '8' => '逾時無送達',
15
+ '9' => '預約已取消',
16
+ '*' => '系統發生錯誤,請聯絡三竹資訊窗口人員',
17
+ 'a' => '簡訊發送功能暫時停止服務,請稍候再試',
18
+ 'b' => '簡訊發送功能暫時停止服務,請稍候再試',
19
+ 'c' => '請輸入帳號',
20
+ 'd' => '請輸入密碼',
21
+ 'e' => '帳號、密碼錯誤',
22
+ 'f' => '帳號已過期',
23
+ 'h' => '帳號已被停用',
24
+ 'k' => '無效的連線位址',
25
+ 'l' => '帳號已達到同時連線數上限',
26
+ 'm' => '必須變更密碼,在變更密碼前,無法使用簡訊發送服務',
27
+ 'n' => '密碼已逾期,在變更密碼前,將無法使用簡訊發送服務',
28
+ 'p' => '沒有權限使用外部Http程式',
29
+ 'r' => '系統暫停服務,請稍後再試',
30
+ 's' => '帳務處理失敗,無法發送簡訊',
31
+ 't' => '簡訊已過期',
32
+ 'u' => '簡訊內容不得為空白',
33
+ 'v' => '無效的手機號碼',
34
+ 'w' => '查詢筆數超過上限',
35
+ 'x' => '發送檔案過大,無法發送簡訊',
36
+ 'y' => '參數錯誤',
37
+ 'z' => '查無資料'
38
+ }.freeze
39
+
40
+ # 簡訊已被三竹收下或已送達。其餘 statuscode 皆視為失敗。
41
+ ACCEPTED_STATUS_CODES = %w[0 1 2 4].freeze
42
+
43
+ attr_reader :raw_response, :records, :account_point
6
44
 
7
45
  def initialize(raw_response)
8
46
  @raw_response = raw_response
9
- parse_response(raw_response)
47
+ @records = []
48
+ @account_point = nil
49
+ parse(raw_response)
50
+ end
51
+
52
+ def code
53
+ first_record['statuscode']
54
+ end
55
+
56
+ def message_id
57
+ first_record['msgid']
58
+ end
59
+
60
+ def client_id
61
+ first_record['clientid']
62
+ end
63
+
64
+ def sms_point
65
+ first_record['smsPoint']
66
+ end
67
+
68
+ def duplicate?
69
+ first_record['Duplicate'] == 'Y'
70
+ end
71
+
72
+ def message_ids
73
+ @records.map { |record| record['msgid'] }.compact
10
74
  end
11
75
 
12
76
  def success?
13
- @code == '1'
77
+ return false if @records.empty?
78
+
79
+ @records.all? { |record| accepted?(record) }
14
80
  end
15
81
 
16
82
  def error?
17
83
  !success?
18
84
  end
19
85
 
86
+ def error
87
+ return nil if success?
88
+
89
+ failures = @records.reject { |record| accepted?(record) }
90
+ return 'Empty or unparseable response' if failures.empty?
91
+
92
+ failures.map { |record| describe(record) }.join('; ')
93
+ end
94
+
20
95
  private
21
96
 
22
- def parse_response(response)
97
+ def first_record
98
+ @records.first || {}
99
+ end
100
+
101
+ def accepted?(record)
102
+ ACCEPTED_STATUS_CODES.include?(record['statuscode'])
103
+ end
104
+
105
+ def describe(record)
106
+ status = record['statuscode']
107
+ description = "#{status || '?'} #{STATUS_MESSAGES.fetch(status, '未知的狀態')}"
108
+ record['msgid'] ? "#{record['msgid']}: #{description}" : description
109
+ end
110
+
111
+ def parse(response)
23
112
  return unless response.is_a?(String)
24
-
25
- # Split by newline and create a hash from key=value pairs
26
- @parsed_response = {}
27
- response.each_line do |line|
28
- key, value = line.strip.split('=', 2)
29
- @parsed_response[key] = value if key && value
113
+
114
+ response.each_line do |raw_line|
115
+ case raw_line.strip
116
+ # 每筆回覆以 [clientid] 開頭,例如 [1] 或 [my-guid]
117
+ when /\A\[(.*)\]\z/
118
+ @records << { 'clientid' => Regexp.last_match(1) }
119
+ # AccountPoint 是整批回覆共用的餘額,不屬於任何單筆簡訊
120
+ when /\AAccountPoint=(.*)\z/
121
+ @account_point = Regexp.last_match(1)
122
+ when /\A([^=]+)=(.*)\z/
123
+ @records << {} if @records.empty?
124
+ @records.last[Regexp.last_match(1)] = Regexp.last_match(2)
125
+ end
30
126
  end
31
-
32
- @code = @parsed_response['statuscode']
33
- @message_id = @parsed_response['msgid']
34
- @account_point = @parsed_response['AccountPoint']
35
- @error = @parsed_response['Error']
36
127
  end
37
128
  end
38
129
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MitakeSms
4
- VERSION = "2.0.0"
4
+ VERSION = "3.0.0"
5
5
  end
data/lib/mitake_sms.rb CHANGED
@@ -41,18 +41,15 @@ module MitakeSms
41
41
  # @param destname [String] recipient name or key value for system integration (optional)
42
42
  # @param response_url [String] callback URL for delivery reports (optional)
43
43
  # @param client_id [String] client reference ID (optional)
44
- # @param charset [String] character encoding, defaults to 'UTF8' (optional)
45
- # @param options [Hash] additional options (optional)
44
+ # @param options [Hash] any other documented SmSend field
46
45
  # @return [MitakeSms::Response] response object
47
- def send_sms(to:, text:, destname: nil, response_url: nil, client_id: nil, charset: 'UTF8', **options)
48
- # Forward all parameters to the client method using named parameters
46
+ def send_sms(to:, text:, destname: nil, response_url: nil, client_id: nil, **options)
49
47
  client.send_sms(
50
48
  to: to,
51
49
  text: text,
52
50
  destname: destname,
53
51
  response_url: response_url,
54
52
  client_id: client_id,
55
- charset: charset,
56
53
  **options
57
54
  )
58
55
  end
@@ -61,45 +58,13 @@ module MitakeSms
61
58
  # The Mitake SMS API has a limit of 500 messages per request
62
59
  # If more than 500 messages are provided, they will be automatically split into multiple requests
63
60
  # @param messages [Array<Hash>] array of message hashes
64
- # Each hash should contain :to and :text keys, and can include :from, :response_url, :client_id
65
- # @param options [Hash] additional options
66
- # @option options [String] :charset character encoding, defaults to 'UTF8'
67
-
61
+ # Each hash requires :to and :text, and may include :client_id, :dlvtime,
62
+ # :vldtime, :destname and :response_url
63
+ # @param options [Hash] any other documented SmBulkSend field
64
+ # @raise [ArgumentError] if a field other than :text contains '$$' or a line break
68
65
  # @return [MitakeSms::Response, Array<MitakeSms::Response>] response object or array of response objects if batch was split
69
66
  def batch_send(messages, options = {})
70
- client.batch_send_with_limit(messages, 500, options)
71
- end
72
-
73
- # Send multiple SMS messages in a single request with a limit per request
74
- # @param messages [Array<Hash>] array of message hashes
75
- # Each hash should contain :to and :text keys, and can include :from, :response_url, :client_id
76
- # @param limit [Integer] maximum number of messages per request (default: 500)
77
- # @param options [Hash] additional options
78
- # @option options [String] :charset character encoding, defaults to 'UTF8'
79
-
80
- # @return [MitakeSms::Response, Array<MitakeSms::Response>] response object or array of response objects if batch was split
81
- def batch_send_with_limit(messages, limit = 500, options = {})
82
- client.batch_send_with_limit(messages, limit, options)
67
+ client.batch_send(messages, options)
83
68
  end
84
-
85
- # Send multiple SMS messages in a single request using advanced format
86
- # @param messages [Array<Hash>] array of message hashes with advanced options
87
- # Each hash can contain the following keys:
88
- # - :client_id [String] client reference ID (optional)
89
- # - :to [String] recipient phone number (required)
90
- # - :dlvtime [String] delivery time in format YYYYMMDDHHMMSS (optional)
91
- # - :vldtime [String] valid until time in format YYYYMMDDHHMMSS (optional)
92
- # - :dest_name [String] recipient name (optional)
93
- # - :response [String] callback URL for delivery reports (optional)
94
- # - :text [String] message content (required)
95
- # @param options [Hash] additional options
96
- # @option options [String] :charset character encoding, defaults to 'UTF8'
97
-
98
- # @return [MitakeSms::Response, Array<MitakeSms::Response>] response object or array of response objects if batch was split
99
- def advanced_batch_send(messages, options = {})
100
- client.advanced_batch_send(messages, options)
101
- end
102
-
103
-
104
69
  end
105
70
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mitake_sms
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zac
@@ -15,7 +15,7 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '1.0'
18
+ version: 2.14.3
19
19
  - - "<"
20
20
  - !ruby/object:Gem::Version
21
21
  version: '3.0'
@@ -25,37 +25,17 @@ dependencies:
25
25
  requirements:
26
26
  - - ">="
27
27
  - !ruby/object:Gem::Version
28
- version: '1.0'
28
+ version: 2.14.3
29
29
  - - "<"
30
30
  - !ruby/object:Gem::Version
31
31
  version: '3.0'
32
- - !ruby/object:Gem::Dependency
33
- name: faraday-multipart
34
- requirement: !ruby/object:Gem::Requirement
35
- requirements:
36
- - - ">="
37
- - !ruby/object:Gem::Version
38
- version: '0.1'
39
- - - "<"
40
- - !ruby/object:Gem::Version
41
- version: '2.0'
42
- type: :runtime
43
- prerelease: false
44
- version_requirements: !ruby/object:Gem::Requirement
45
- requirements:
46
- - - ">="
47
- - !ruby/object:Gem::Version
48
- version: '0.1'
49
- - - "<"
50
- - !ruby/object:Gem::Version
51
- version: '2.0'
52
32
  - !ruby/object:Gem::Dependency
53
33
  name: dry-configurable
54
34
  requirement: !ruby/object:Gem::Requirement
55
35
  requirements:
56
36
  - - ">="
57
37
  - !ruby/object:Gem::Version
58
- version: 0.13.0
38
+ version: 1.4.0
59
39
  - - "<"
60
40
  - !ruby/object:Gem::Version
61
41
  version: '2.0'
@@ -65,7 +45,7 @@ dependencies:
65
45
  requirements:
66
46
  - - ">="
67
47
  - !ruby/object:Gem::Version
68
- version: 0.13.0
48
+ version: 1.4.0
69
49
  - - "<"
70
50
  - !ruby/object:Gem::Version
71
51
  version: '2.0'
@@ -131,28 +111,28 @@ dependencies:
131
111
  requirements:
132
112
  - - "~>"
133
113
  - !ruby/object:Gem::Version
134
- version: 0.21.0
114
+ version: 1.0.3
135
115
  type: :development
136
116
  prerelease: false
137
117
  version_requirements: !ruby/object:Gem::Requirement
138
118
  requirements:
139
119
  - - "~>"
140
120
  - !ruby/object:Gem::Version
141
- version: 0.21.0
121
+ version: 1.0.3
142
122
  - !ruby/object:Gem::Dependency
143
123
  name: simplecov-cobertura
144
124
  requirement: !ruby/object:Gem::Requirement
145
125
  requirements:
146
126
  - - "~>"
147
127
  - !ruby/object:Gem::Version
148
- version: '2.1'
128
+ version: '4.0'
149
129
  type: :development
150
130
  prerelease: false
151
131
  version_requirements: !ruby/object:Gem::Requirement
152
132
  requirements:
153
133
  - - "~>"
154
134
  - !ruby/object:Gem::Version
155
- version: '2.1'
135
+ version: '4.0'
156
136
  description: A simple and efficient Ruby client for sending SMS messages through Mitake
157
137
  SMS API
158
138
  email:
@@ -184,14 +164,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
184
164
  requirements:
185
165
  - - ">="
186
166
  - !ruby/object:Gem::Version
187
- version: 2.6.0
167
+ version: 3.3.0
188
168
  required_rubygems_version: !ruby/object:Gem::Requirement
189
169
  requirements:
190
170
  - - ">="
191
171
  - !ruby/object:Gem::Version
192
172
  version: '0'
193
173
  requirements: []
194
- rubygems_version: 3.6.7
174
+ rubygems_version: 4.0.16
195
175
  specification_version: 4
196
176
  summary: A Ruby client for Mitake SMS API
197
177
  test_files: []