Skip to content

Commit 9cdd391

Browse files
committed
[commands] add batch send command
1 parent 926b312 commit 9cdd391

21 files changed

Lines changed: 1385 additions & 159 deletions

Makefile

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
.PHONY: all version fmt lint test coverage benchmark air deps release clean build help
1+
.PHONY: all version fmt lint test coverage benchmark air deps gen release clean build help
22

33
BINARY_NAME := $(shell basename $(PWD))
44
GIT_VERSION := $(shell git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0")
55
VERSION ?= $(GIT_VERSION)
66
DOCKER_CR ?= $(shell basename $$(dirname $(PWD)))
77
DOCKER_IMAGE := ${DOCKER_CR}/$(BINARY_NAME):$(VERSION)
88

9-
all: fmt lint coverage ## Run all tests and checks
9+
all: fmt gen lint coverage ## Run all tests and checks
1010

1111
version: ## Display current version
1212
@echo "Current version: $(VERSION)"
@@ -20,6 +20,9 @@ lint: ## Run linter
2020
test: ## Run tests
2121
go test -race -shuffle=on -count=1 -covermode=atomic -coverpkg=./... -coverprofile=coverage.out ./...
2222

23+
test-e2e: test ## Run end-to-end tests
24+
cd tests/e2e && go test -count=1 .
25+
2326
coverage: test ## Generate coverage
2427
go tool cover -func=coverage.out
2528
go tool cover -html=coverage.out -o coverage.html
@@ -38,6 +41,9 @@ air: ## Run development server
3841
deps: ## Install dependencies
3942
go mod download
4043

44+
gen: ## Generate code
45+
go generate ./...
46+
4147
release: ## Create release
4248
goreleaser release --snapshot --clean
4349

README.md

Lines changed: 173 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
- [Exit codes](#exit-codes)
4242
- [Examples](#examples)
4343
- [Sending messages](#sending-messages)
44+
- [Batch message sending](#batch-message-sending)
4445
- [Getting message status](#getting-message-status)
4546
- [Getting logs](#getting-logs)
4647
- [Output formats](#output-formats-1)
@@ -55,7 +56,7 @@
5556
There are two CLI tools in this repository: `smsgate` and `smsgate-ca`. The first one is for SMS Gateway for Android itself, and the second one is for the Certificate Authority.
5657

5758
This CLI provides a robust interface for:
58-
- Sending and managing SMS messages
59+
- Sending and managing SMS messages (including batch operations from CSV and Excel files)
5960
- Configuring webhook integrations
6061
- Issuing certificates for private deployments
6162

@@ -133,9 +134,9 @@ smsgate [global options] command [command options] [arguments...]
133134

134135
### Commands
135136

136-
The CLI offers three main groups of commands:
137+
The CLI offers four main groups of commands:
137138

138-
- **Messages**: Commands for sending messages and checking their status.
139+
- **Messages**: Commands for sending messages and checking their status, including batch operations from CSV and Excel files.
139140
- **Webhooks**: Commands for managing webhooks, including creating, updating, and deleting them.
140141
- **Logs**: Commands for retrieving logs for a specific time range.
141142

@@ -164,16 +165,183 @@ smsgate -u <username> -p <password> send --phones '+12025550123' 'Hello, Dr. Tur
164165

165166
#### Sending messages
166167

168+
The `send` command supports various options to customize message delivery:
169+
167170
```bash
168-
# Send a message
171+
# Send a simple text message
169172
smsgate send --phones '+12025550123' 'Hello, Dr. Turk!'
170173

171-
# Send a message to multiple numbers
174+
# Send to multiple numbers
172175
smsgate send --phones '+12025550123' --phones '+12025550124' 'Hello, doctors!'
173176
# or
174177
smsgate send --phones '+12025550123,+12025550124' 'Hello, doctors!'
178+
179+
# Send with explicit device selection
180+
smsgate send --phones '+12025550123' --device-id device123 'Message'
181+
182+
# Send with SIM number selection (1-based)
183+
smsgate send --phones '+12025550123' --sim-number 2 'Message'
184+
185+
# Send with priority (>=100 bypasses limits)
186+
smsgate send --phones '+12025550123' --priority 100 'Urgent message'
187+
188+
# Send with time-to-live (TTL)
189+
smsgate send --phones '+12025550123' --ttl 1h30m 'Expiring message'
190+
191+
# Send with expiration date (RFC3339 format)
192+
smsgate send --phones '+12025550123' --valid-until '2024-12-31T23:59:59Z' 'Message'
193+
194+
# Disable delivery report
195+
smsgate send --phones '+12025550123' --delivery-report=false 'Message'
196+
197+
# Skip phone number validation
198+
smsgate send --phones '+12025550123' --skip-phone-validation 'Message'
199+
200+
# Filter by device activity (devices active within last 12 hours)
201+
smsgate send --phones '+12025550123' --device-active-within 12 'Message'
202+
203+
# Send data message (base64 encoded)
204+
echo -n 'hello world' | base64
205+
smsgate send --phones '+12025550123' --data --data-port 12345 'aGVsbG8gd29ybGQ='
206+
```
207+
208+
**Send command options:**
209+
210+
| Option | Description | Default Value | Example |
211+
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ----------------------- |
212+
| `--id` | A unique message ID. If not provided, one will be automatically generated. | empty | `zXDYfTmTVf3iMd16zzdBj` |
213+
| `--device-id`, `--device` | Optional device ID for explicit selection. If not provided, a random device will be selected. | empty | `oi2i20J8xVP1ct5neqGZt` |
214+
| `--phones`, `--phone`, `-p` | Specifies the recipient's phone number(s). This option can be used multiple times or accepts comma-separated values. Numbers must be in E.164 format. | **required** | `+12025550123` |
215+
| `--sim-number`, `--sim` | The one-based SIM card slot number. If not specified, the device's SIM rotation feature will be used. | empty | `2` |
216+
| `--delivery-report` | Enables delivery report for the message. | `true` | `true` / `false` |
217+
| `--priority` | Sets the priority of the message. Messages with priority >= 100 bypass all limits and delays. Range: -128 to 127. | `0` | `100` |
218+
| **Data Message** | | | |
219+
| `--data` | Send data message instead of text (content must be base64 encoded). | `false` | `true` |
220+
| `--data-port` | Destination port for data message (1 to 65535). | `53739` | `12345` |
221+
| **Options** | | | |
222+
| `--ttl` | Time-to-live (TTL) for the message. Duration format (e.g., `1h30m`). If not provided, the message will not expire.<br>**Conflicts with `--valid-until`.** | empty | `1h30m` |
223+
| `--valid-until` | The expiration date and time for the message. RFC3339 format (e.g., `2006-01-02T15:04:05Z07:00`).<br>**Conflicts with `--ttl`.** | empty | `2024-12-31T23:59:59Z` |
224+
| `--skip-phone-validation` | Skip phone number validation. | `false` | `true` |
225+
| `--device-active-within` | Time window in hours for device activity filtering. `0` means no filtering. | `0` | `12` |
226+
227+
#### Batch message sending
228+
229+
The CLI supports sending messages in bulk from CSV and Excel files. The `batch send` command also supports the shared delivery/device options from `send` (such as `--device-id`, `--sim-number`, `--priority`, `--ttl`, `--valid-until`, `--delivery-report`, `--skip-phone-validation`, `--device-active-within`), allowing fine-grained control over each message in the batch.
230+
231+
**Supported file formats:**
232+
- **CSV** (Comma-Separated Values)
233+
- **XLSX** (Excel files)
234+
235+
**Basic usage:**
236+
237+
```bash
238+
# Send messages from a CSV file
239+
smsgate batch send contacts.csv --map phone=Phone,text=Message
240+
241+
# Send messages from an Excel file with specific sheet
242+
smsgate batch send campaign.xlsx --sheet Sheet1 --map phone=A,text=B
243+
244+
# Send with custom delimiter and no header
245+
smsgate batch send data.csv --delimiter ';' --header=false --map phone=col_1,text=col_2
246+
```
247+
248+
**Batch-specific options:**
249+
250+
| Option | Description | Default Value | Example |
251+
| --------------------- | -------------------------------------------------- | ------------- | -------------------------- |
252+
| `--sheet` | XLSX sheet name (defaults to first sheet) | empty | `Sheet1` |
253+
| `--delimiter` | CSV delimiter character | `,` | `;` |
254+
| `--header` | Treat first row as header | `true` | `false` |
255+
| `--map` | Column mapping (required) | **required** | `phone=Phone,text=Message` |
256+
| `--dry-run` | Validate and print normalized rows without sending | `false` | `true` |
257+
| `--validate-only` | Validate input only (no preview, no sending) | `false` | `true` |
258+
| `--concurrency` | Number of concurrent send workers | CPU cores | `5` |
259+
| `--continue-on-error` | Continue sending after per-row failures | `false` | `true` |
260+
261+
**Inherited message options:**
262+
The shared delivery/device options from the `send` command are also available (e.g., `--device-id`, `--sim-number`, `--priority`, `--ttl`, `--valid-until`, `--delivery-report`, `--skip-phone-validation`, `--device-active-within`). These apply to every message sent in the batch.
263+
264+
**Column mapping:**
265+
266+
The `--map` option defines how columns in your file map to message fields:
267+
268+
| Field | Required | Description |
269+
| ------------ | -------- | ------------------------------------- |
270+
| `phone` || Phone number column |
271+
| `text` || Message text column |
272+
| `device_id` || Device identifier column |
273+
| `sim_number` || SIM number column (1-255) |
274+
| `priority` || Message priority column (-128 to 127) |
275+
276+
**Mapping examples:**
277+
278+
```bash
279+
# Basic mapping with headers
280+
smsgate batch send --map phone=Phone,text=Message contacts.csv
281+
282+
# Excel file with specific sheet
283+
smsgate batch send --sheet Sheet1 --map phone=Phone,text=Message campaign.xlsx
284+
285+
# No headers, column positions
286+
smsgate batch send --header=false --map phone=col_1,text=col_2 data.csv
287+
288+
# Full mapping with optional fields
289+
smsgate batch send --map phone=Phone,text=Message,device_id=Device,sim_number=SIM,priority=Priority contacts.csv
290+
```
291+
292+
**File format examples:**
293+
294+
**CSV with Headers:**
295+
```csv
296+
Phone,Message,Device,Priority
297+
+12025550123,"Hello Dr. Turk!",device1,1
298+
+12025550124,"Hello Dr. Smith!",device1,2
299+
+12025550125,"Hello Dr. Jones!",device2,1
300+
```
301+
302+
**CSV without Headers:**
303+
```csv
304+
+12025550123,"Hello Dr. Turk!",device1,1
305+
+12025550124,"Hello Dr. Smith!",device1,2
306+
+12025550125,"Hello Dr. Jones!",device2,1
175307
```
176308

309+
**Excel Files:**
310+
- Supports multiple sheets (use `--sheet` to specify)
311+
- First row treated as headers by default
312+
- Column mapping works the same as CSV
313+
314+
**Workflow modes:**
315+
316+
1. **Validation Only** - Validates file format and column mapping, checks required fields, exits without sending:
317+
```bash
318+
smsgate batch send --map phone=Phone,text=Message --validate-only contacts.csv
319+
```
320+
321+
2. **Dry Run** - Validates and processes all rows, shows what would be sent without actually sending:
322+
```bash
323+
smsgate batch send --map phone=Phone,text=Message --dry-run contacts.csv
324+
```
325+
326+
3. **Full Send** - Sends all messages with real-time progress:
327+
```bash
328+
smsgate batch send --map phone=Phone,text=Message --concurrency=5 contacts.csv
329+
```
330+
331+
**Output and error handling:**
332+
333+
- **Summary**: `Batch send summary: total=100 enqueued=95 failed=3 skipped=2`
334+
- **Real-time progress**: Shows each message's UUID and state during sending
335+
- **Error handling**: By default stops on first error; use `--continue-on-error` to send all rows even if some fail
336+
337+
**Best practices:**
338+
339+
1. Always use `--dry-run` or `--validate-only` first to test your configuration
340+
2. Ensure phone numbers are in E.164 format
341+
3. Start with lower concurrency values and increase as needed
342+
4. Use `--continue-on-error` for non-critical bulk sends
343+
5. Combine with inherited message options (e.g., `--device-id`, `--priority`) for advanced scenarios
344+
177345
#### Getting message status
178346

179347
```bash

go.mod

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,30 @@
11
module github.com/android-sms-gateway/cli
22

3-
go 1.23.2
3+
go 1.25.0
44

55
require (
66
github.com/android-sms-gateway/client-go v1.12.0
7+
github.com/google/uuid v1.6.0
78
github.com/joho/godotenv v1.5.1
89
github.com/samber/lo v1.52.0
10+
github.com/stretchr/testify v1.10.0
911
github.com/urfave/cli/v2 v2.27.5
12+
github.com/xuri/excelize/v2 v2.9.0
1013
)
1114

1215
require (
1316
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
17+
github.com/davecgh/go-spew v1.1.1 // indirect
18+
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
19+
github.com/pmezard/go-difflib v1.0.0 // indirect
20+
github.com/richardlehane/mscfb v1.0.4 // indirect
21+
github.com/richardlehane/msoleps v1.0.4 // indirect
1422
github.com/russross/blackfriday/v2 v2.1.0 // indirect
1523
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
16-
golang.org/x/text v0.22.0 // indirect
24+
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect
25+
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect
26+
golang.org/x/crypto v0.49.0 // indirect
27+
golang.org/x/net v0.52.0 // indirect
28+
golang.org/x/text v0.35.0 // indirect
29+
gopkg.in/yaml.v3 v3.0.1 // indirect
1730
)

go.sum

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,47 @@
1-
github.com/android-sms-gateway/client-go v1.9.2 h1:e9HFgvR+LRMV0dOJvFkxt998UxOMWNf8hfnXwMIc39I=
2-
github.com/android-sms-gateway/client-go v1.9.2/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
3-
github.com/android-sms-gateway/client-go v1.11.1-0.20260315032244-641ce286d8b5 h1:04bQhao7QKaD5QwsHI8PDpd32ZAYO7Cw1/5bRMbfuNM=
4-
github.com/android-sms-gateway/client-go v1.11.1-0.20260315032244-641ce286d8b5/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
51
github.com/android-sms-gateway/client-go v1.12.0 h1:4YWnzLi4AWEQIXvvUSLiTK+ZAgQD6SV+xfvlTCjZ8pI=
62
github.com/android-sms-gateway/client-go v1.12.0/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
73
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
84
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
5+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
6+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
8+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
99
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
1010
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
11+
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
12+
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
13+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
14+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
15+
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
16+
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
17+
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
18+
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
19+
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
1120
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
1221
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
1322
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
1423
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
24+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
25+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
1526
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
1627
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
1728
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
1829
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
19-
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
20-
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
30+
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY=
31+
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
32+
github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE=
33+
github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE=
34+
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A=
35+
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
36+
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
37+
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
38+
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
39+
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
40+
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
41+
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
42+
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
43+
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
44+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
45+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
46+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
47+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/commands/flags/errors.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package flags
2+
3+
import "errors"
4+
5+
var (
6+
ErrValidationFailed = errors.New("validation failed")
7+
)

0 commit comments

Comments
 (0)