# kwtsms
Rust client for the [kwtSMS API](https://www.kwtsms.com). Send SMS, check balance, validate numbers, list sender IDs, check coverage, get delivery reports.
## Install
```toml
# Cargo.toml
[dependencies]
kwtsms = "0.1"
```
Or: `cargo add kwtsms`
## Quick Start
```rust
use kwtsms::KwtSms;
fn main() {
// Load credentials from .env file or environment variables
let sms = KwtSms::from_env(None).unwrap();
// Verify credentials
let result = sms.verify();
println!("Balance: {:?}", result.balance);
// Send SMS
let response = sms.send_one("96598765432", "Hello from Rust!", None).unwrap();
println!("{}", response);
}
```
## Environment Variables
Create a `.env` file or set these environment variables:
```ini
KWTSMS_USERNAME=your_api_user
KWTSMS_PASSWORD=your_api_pass
KWTSMS_SENDER_ID=KWT-SMS
KWTSMS_TEST_MODE=1
KWTSMS_LOG_FILE=kwtsms.log
```
Or pass credentials directly:
```rust
let sms = KwtSms::new("username", "password", Some("MY-SENDER"), false, None).unwrap();
```
## API Reference
### Verify Credentials
```rust
let result = sms.verify();
if result.ok {
println!("Balance: {}", result.balance.unwrap());
println!("Purchased: {}", result.purchased.unwrap());
}
```
### Send SMS
```rust
// Single number
let result = sms.send_one("96598765432", "Hello!", None).unwrap();
// Multiple numbers
let result = sms.send(&["96598765432", "96512345678"], "Hello!", None).unwrap();
// Custom sender ID
let result = sms.send_one("96598765432", "Hello!", Some("MY-APP")).unwrap();
// Comma-separated
let result = sms.send_one("96598765432,96512345678", "Hello!", None).unwrap();
```
The send method automatically:
- Normalizes phone numbers (strips +, 00, spaces, dashes, converts Arabic digits)
- Validates each number locally (rejects emails, too short/long, no digits)
- Cleans the message (strips emojis, HTML, invisible chars, converts Arabic digits)
- Deduplicates normalized numbers
- Auto-batches when >200 numbers (200 per batch, 0.5s delay)
### Check Balance
```rust
let balance = sms.balance(); // Option<f64>
```
### Validate Numbers
```rust
let result = sms.validate(&["96598765432", "96512345678"]).unwrap();
// Returns: ok[], er[], nr[], rejected[]
```
### Sender IDs
```rust
let result = sms.senderids().unwrap();
```
### Coverage
```rust
let result = sms.coverage().unwrap();
```
### Message Status
```rust
let result = sms.status("msg-id-from-send-response").unwrap();
```
### Delivery Report (international only)
```rust
let result = sms.dlr("msg-id-from-send-response").unwrap();
```
### Utility Functions
```rust
use kwtsms::{normalize_phone, validate_phone_input, clean_message};
// Normalize phone number
let phone = normalize_phone("+965 9876-5432"); // "96598765432"
// Validate phone input
let (valid, error, normalized) = validate_phone_input("user@gmail.com");
// valid=false, error="'user@gmail.com' is an email address..."
// Clean message text
let cleaned = clean_message("Hello \u{1F600} OTP: \u{0661}\u{0662}\u{0663}");
// "Hello OTP: 123"
```
### Error Codes
All 33 kwtSMS error codes are mapped to developer-friendly action messages:
```rust
use kwtsms::{API_ERRORS, enrich_error};
// Access the error map directly
if let Some(action) = API_ERRORS.get("ERR003") {
println!("{}", action);
// "Wrong API username or password. Check KWTSMS_USERNAME and KWTSMS_PASSWORD..."
}
```
## Credential Management
**Never hardcode credentials.** Use one of these approaches:
1. **Environment variables / .env file** (default): `KwtSms::from_env(None)` loads from env vars, then `.env` file.
2. **Constructor injection**: `KwtSms::new(username, password, ...)` for custom config systems.
3. **Secrets manager**: Load from AWS Secrets Manager, Vault, etc., then pass to the constructor.
## Best Practices
### Always save msg-id and balance-after
```rust
let result = sms.send_one("96598765432", "Hello!", None).unwrap();
if result["result"] == "OK" {
// Save msg-id for status checks and delivery reports
let msg_id = result["msg-id"].as_str().unwrap();
// Save balance-after: never call balance() after send()
let balance = result["balance-after"].as_f64().unwrap();
}
```
### Use Transactional Sender ID for OTP
`KWT-SMS` is a shared test sender. Register a private Transactional sender ID for OTP messages. Promotional sender IDs are blocked by DND on Zain and Ooredoo, causing silent delivery failure.
### Validate locally before calling the API
```rust
use kwtsms::validate_phone_input;
let (valid, error, normalized) = validate_phone_input(user_input);
if !valid {
// Return error to user without hitting the API
}
```
### Security Checklist
Before going live:
- [ ] Bot protection enabled (CAPTCHA for web)
- [ ] Rate limit per phone number (max 3-5/hour)
- [ ] Rate limit per IP address (max 10-20/hour)
- [ ] Rate limit per user/session if authenticated
- [ ] Monitoring/alerting on abuse patterns
- [ ] Admin notification on low balance
- [ ] Test mode OFF (`KWTSMS_TEST_MODE=0`)
- [ ] Private Sender ID registered (not KWT-SMS)
- [ ] Transactional Sender ID for OTP (not promotional)
## CLI
Build the CLI binary:
```
cargo install kwtsms --features cli
```
Commands:
```
kwtsms verify # test credentials
kwtsms balance # show credits
kwtsms senderid # list sender IDs
kwtsms coverage # list active prefixes
kwtsms send <mobile> <message> [--sender ID] # send SMS
kwtsms validate <number> [number ...] # validate numbers
kwtsms status <msg-id> # check status
kwtsms dlr <msg-id> # delivery report
```
## Testing
```bash
# Unit + mock tests (no credentials needed)
cargo test
# Integration tests (real API, test mode, no credits consumed)
export RUST_USERNAME=your_api_user
export RUST_PASSWORD=your_api_pass
cargo test --features integration
```
## Publishing to crates.io
```bash
# 1. Sign in at https://crates.io (GitHub account)
# 2. Get API token: crates.io -> Account Settings -> New Token
cargo login <token>
# 3. Publish
cargo publish
# 4. Updates: bump version in Cargo.toml, then:
cargo publish
```
## License
MIT