Plan the interoperability tests against real-world SMPP peers before 1.0.0

This commit is contained in:
2026-09-05 17:31:02 +02:00
parent 9ca83ac05c
commit 8a565cf557
5 changed files with 888 additions and 0 deletions
@@ -0,0 +1,285 @@
# SMPP ESME clients, load generators, and independent PDU validators — interop-testing `@larvit/smpp`
Research note: this session's shared WebSearch budget (200 calls, spent across parallel research threads on this task) was exhausted partway through. Sections B and C below are fully search-backed. Section A was completed with WebFetch only (direct GitHub API / raw-file / PyPI / crates.io / RubyGems / Hex.pm requests) after the search budget ran out and a parallel research agent covering Go/Erlang/other-language clients was lost to a session rate limit. Where a claim could not be independently re-verified this session, it is marked **unverified** rather than guessed — this hits Kannel hardest (see A18): `kannel.org`'s TLS certificate failed validation on every fetch attempt this session, so its exact config keys could not be re-confirmed against a live primary source.
---
## A. ESME client libraries / tools
### Summary
| # | Library | Language | Licence | Repo | Last activity | Maintained? |
|---|---|---|---|---|---|---|
| A1 | Cloudhopper SMPP | Java | Apache-2.0 per README; GitHub shows `NOASSERTION` | [fizzed/cloudhopper-smpp](https://github.com/fizzed/cloudhopper-smpp) (fork of [twitter-archive/cloudhopper-smpp](https://github.com/twitter-archive/cloudhopper-smpp), archived) | fork: no commits since 2018-10-03, issues triaged to 2026-08-24; original archived 2021-08-18 | Fork only, lightly |
| A2 | jsmpp | Java | Apache-2.0 | [opentelecoms-org/jsmpp](https://github.com/opentelecoms-org/jsmpp) | pushed 2026-06-17, 68 open issues | Yes |
| A3 | OpenSMPP (Logica) | Java | BSD (per README) | [OpenSmpp/opensmpp](https://github.com/OpenSmpp/opensmpp) | pushed 2023-11-01 | Weakly |
| A4 | python-smpplib | Python | LGPL-3.0 | [python-smpplib/python-smpplib](https://github.com/python-smpplib/python-smpplib) | PyPI 2.2.4, 2025-01-17 | Yes |
| A5 | smpp.twisted | Python | LICENSE file present, text unverified | [jookies/smpp.twisted](https://github.com/jookies/smpp.twisted) | 7 commits total | Stable/frozen |
| A6 | smpp.pdu | Python | LICENSE file present, text unverified | [jookies/smpp.pdu](https://github.com/jookies/smpp.pdu) | 7 commits, 2 open issues | Stable/frozen |
| A7 | Jasmin (`smppccm`) | Python/Twisted | `NOASSERTION` on GitHub | [jookies/jasmin](https://github.com/jookies/jasmin) | pushed 2026-04-26 | Yes |
| A8 | fiorix/go-smpp | Go | MIT | [fiorix/go-smpp](https://github.com/fiorix/go-smpp) | pushed 2026-04-25 | Yes |
| A9 | linxGnu/gosmpp | Go | Apache-2.0 | [linxGnu/gosmpp](https://github.com/linxGnu/gosmpp) | pushed 2026-07-13, 21 open issues | Yes |
| A10 | oserl | Erlang | unverified | [archaelus/oserl](https://github.com/archaelus/oserl) | last updated 2009-04-03 | No — frozen, embedded in smppload |
| A11 | libsmpp34 | C | LGPL-2.1 | [osmocom/libsmpp34](https://github.com/osmocom/libsmpp34) | pushed 2026-08-20 | Yes |
| A12 | smpp34 (crate) | Rust | unverified | [crates.io/crates/smpp34](https://crates.io/crates/smpp34) | v1.4.1 on crates.io | unverified |
| A13 | JamaaSMPP | C# | unverified | [AdhamAwadhi/JamaaSMPP](https://github.com/AdhamAwadhi/JamaaSMPP) | pushed 2026-08-18 | Yes |
| A14 | php-smpp | PHP | none declared | [alexandr-mironov/php-smpp](https://github.com/alexandr-mironov/php-smpp) (fork of [OnlineCity/php-smpp](https://github.com/OnlineCity/php-smpp), unmaintained) | fork pushed 2022-12-02 | Weakly |
| A15 | Net::SMPP | Perl | unknown | [metacpan.org/pod/Net::SMPP](https://metacpan.org/pod/Net::SMPP) | v1.19, 2011-06-01 | No |
| A16 | ruby-smpp | Ruby | unverified | [rubygems.org/gems/ruby-smpp](https://rubygems.org/gems/ruby-smpp) | v0.6.0, 108,794 downloads | unverified |
| A17 | smppex | Elixir | unverified | [hex.pm/packages/smppex](https://hex.pm/packages/smppex) | v3.3.0, updated ~2 months ago | Yes |
| A18 | Kannel `bearerbox` (SMPP client) | C | Kannel Software License 1.0 (Apache-1.1-like) | kannel.org — canonical source unreachable this session | unverified this session | unverified this session |
---
### A1. Cloudhopper SMPP
[fizzed/cloudhopper-smpp](https://github.com/fizzed/cloudhopper-smpp) is the maintained fork of Twitter's archived [twitter-archive/cloudhopper-smpp](https://github.com/twitter-archive/cloudhopper-smpp) (archived 2021-08-18). The fork itself has had no new commits since 2018-10-03 but still receives issue triage (last activity 2026-08-24, 24 open issues) — treat as lightly maintained, not actively developed. Netty-based session library.
- **Docker**: no official image. Build from source with Maven inside a JDK image you pin yourself (never guess a tag) — the repo's own instructions run demos via `make client`/`make server` wrapping the Maven build.
- **Bind/send** (from the demo client, [`ClientMain.java`](https://raw.githubusercontent.com/fizzed/cloudhopper-smpp/master/src/test/java/com/cloudhopper/smpp/demo/ClientMain.java)):
```java
config0.setSystemId("1234567890");
config0.setPassword("password");
config0.setType(SmppBindType.TRANSCEIVER);
config0.setWindowSize(1);
config0.setRequestExpiryTimeout(30000);
config0.setWindowMonitorInterval(15000);
SubmitSm submit0 = new SubmitSm();
submit0.setShortMessage(textBytes);
session0.submit(submit0, 10000);
```
`systemType`/`interfaceVersion` are not set in the demo (library defaults apply — not confirmed further this session).
- **Encoding**: demo encodes with `CharsetUtil.encode(text160, CharsetUtil.CHARSET_GSM)` for data_coding 0. Packed vs. unpacked septets not confirmed from the demo alone — **unverified**.
- **Long messages**: demo sends a single ≤160-char GSM segment only; no UDH/SAR/message_payload shown — **unverified** whether the library auto-splits.
- **enquire_link**: demo only sends it synchronously on a keypress (`session0.enquireLink(new EnquireLink(), 10000)`); no periodic automatic keepalive demonstrated.
- **Windowing**: first-class — `setWindowSize()`, `setRequestExpiryTimeout()`, `setWindowMonitorInterval()` are real, documented session-config knobs. This is the strongest windowing/backpressure story of any Java client here.
- **Strictness / quirk**: [twitter/cloudhopper-smpp#39](https://github.com/twitter/cloudhopper-smpp/issues/39) — a user reports throughput capped at ~200 msg/s even after raising window size ("window wait time is always zero"), i.e. windowing alone won't reveal a throughput bug if your server's per-PDU response latency dominates. Unresolved, on an archived repo.
### A2. jsmpp
[opentelecoms-org/jsmpp](https://github.com/opentelecoms-org/jsmpp), Apache-2.0, actively maintained (pushed 2026-06-17). README: "supports SMPP v3.3, v3.4 and v5.0" and explicitly "is not a high-level library" — it does not auto-split long messages; the caller owns UDH/SAR/message_payload chunking, which makes it good for controlled wire-level testing.
- **interface_version negotiation** (confirmed directly from [`SMPPSession.java`](https://raw.githubusercontent.com/opentelecoms-org/jsmpp/master/jsmpp/src/main/java/org/jsmpp/session/SMPPSession.java)):
```java
InterfaceVersion commonInterfaceVersion = scVersion != null
? InterfaceVersion.IF_50.min(InterfaceVersion.valueOf(scVersion.getValue()))
: InterfaceVersion.IF_34;
```
If the server's `bind_resp` omits the `sc_interface_version` optional TLV, jsmpp silently assumes 3.4 — it never rejects a bind over a missing/absent TLV. If the TLV **is** present with a value `InterfaceVersion.valueOf()` doesn't recognize, that call is an enum-style lookup that can throw — a server sending a bogus `sc_interface_version` byte is a good way to probe jsmpp-based tooling for a crash vs. graceful handling.
- **Docker**: no official image found — **unverified**; build with Maven/Gradle inside a self-pinned Java image.
- **Long message / encoding**: the bundled `StressClient.java` example hardcodes `DataCodings.ZERO` and calls `submitShortMessage()` directly — no UDH/SAR/message_payload, confirming the library leaves long-message strategy entirely to the caller.
- **enquire_link**: known quirk — [opentelecoms-org/jsmpp#142](https://github.com/opentelecoms-org/jsmpp/issues/142) "Enquire time not respected?" reports the library's enquire-link-interval not firing as configured.
- **Windowing**: no true max-outstanding window found in the stress example (`maxOutstanding` there is a fixed thread-pool size for response processing, not a protocol window) — **unverified** whether the core session API exposes a real window elsewhere.
### A3. OpenSMPP (Logica)
[OpenSmpp/opensmpp](https://github.com/OpenSmpp/opensmpp), Java, pushed 2023-11-01. Per its README: "originally issued under the Logica Open Source License Version 1.0, but was subsequently put in the public domain under the current BSD licence." This is the historical root that much SMPP tooling's PDU model descends from. Related forks found: [cornet/logica-smpp](https://github.com/cornet/logica-smpp) and a companion simulator [cornet/logica-smscsim](https://github.com/cornet/logica-smscsim) (server-side, out of scope here). Bind/send example, Docker packaging, long-message defaults, encoding, and enquire_link behaviour were **not independently re-verified this session** (search budget exhausted before a source read) — mark all operational specifics **unverified**.
### A4. python-smpplib
[python-smpplib/python-smpplib](https://github.com/python-smpplib/python-smpplib), LGPL-3.0 (confirmed via both GitHub API and the [PyPI JSON API](https://pypi.org/pypi/smpplib/json): latest `2.2.4`, released 2025-01-17).
- **Bind**: `client.bind_transmitter(**kwargs)` / `bind_transceiver(**kwargs)` — `system_id`/`password`/`system_type`/`interface_version` all pass through as kwargs into the PDU (confirmed from [`smpplib/client.py`](https://raw.githubusercontent.com/python-smpplib/python-smpplib/master/smpplib/client.py)).
- **Encoding — notable**: [`smpplib/consts.py`](https://raw.githubusercontent.com/python-smpplib/python-smpplib/master/smpplib/consts.py) defines `SMPP_ENCODING_DEFAULT = 0x00`. [`smpplib/gsm.py`](https://raw.githubusercontent.com/python-smpplib/python-smpplib/master/smpplib/gsm.py)'s `gsm_encode()` maps each character to **one unpacked byte** (with `\x1B`-escape for the extension table) rather than packing 8 characters into 7 septet bytes:
```python
def gsm_encode(plaintext):
return b''.join(
six.int2byte(index) if index < 0x80 else b'\x1B' + six.int2byte(index - 0x80)
for index in map(GSM_CHARACTER_TABLE.index, plaintext))
```
**This is a real cross-check opportunity**: if `@larvit/smpp` decodes data_coding 0 as standard packed 7-bit, a raw submit_sm from this client will decode as garbage unless the harness explicitly unpacks/repacks — worth testing deliberately either way.
- **Long messages**: `make_parts_encoded()` in the same file builds UDH concatenation (`\x05\x00\x03` + random ref id + total-parts + this-part), i.e. UDH is the default splitting method, not `sar_*`/`message_payload`.
- **enquire_link**: opt-in and reactive, not a fixed timer — on a socket read timeout, if `auto_send_enquire_link` is set the client sends `enquire_link` instead of raising; otherwise it re-raises the timeout.
- **Docker**: no official image; `python:3.12.14-slim-bookworm` (verified current full-patch tag via [Docker Hub's tag API](https://hub.docker.com/v2/repositories/library/python/tags?name=3.12) as of this research) + `pip install smpplib` is a safe base.
### A5A6. Jasmin's `smpp.twisted` / `smpp.pdu`
[jookies/smpp.twisted](https://github.com/jookies/smpp.twisted) (Twisted-based SMPP 3.4 client/session engine) and [jookies/smpp.pdu](https://github.com/jookies/smpp.pdu) (the PDU encode/decode layer underneath it) are Jasmin's internal SMPP engine, each with only 7 commits on `master` — essentially frozen/stable rather than actively developed, which is consistent with being a settled dependency rather than a product. Both repos confirm a `LICENSE` file exists but its exact text wasn't fetched this session — **unverified** licence specifics. TLV/long-message/encoding details for these two packages specifically were not independently confirmed this session — for practical testing, use them via Jasmin itself (A7) rather than standalone.
### A7. Jasmin SMPP client connector (`smppccm`)
[jookies/jasmin](https://github.com/jookies/jasmin), GitHub shows licence `NOASSERTION` (a `LICENSE` file exists in-repo; exact terms not re-confirmed this session), actively maintained (pushed 2026-04-26).
- **Docker**: official image `jookies/jasmin` on Docker Hub. [Tag listing](https://hub.docker.com/v2/repositories/jookies/jasmin/tags) shows the latest tagged release is **`0.11.0`** (pushed 2023-11-10) — use `jookies/jasmin:0.11.0`. Note the image lags the git repo (repo pushed as recently as 2026-04-26, image not retagged since 2023).
- **Configuring + driving `smppccm`** (exact key names confirmed from source, [`jasmin/protocols/cli/smppccm.py`](https://raw.githubusercontent.com/jookies/jasmin/master/jasmin/protocols/cli/smppccm.py) — full key list: `cid, host, port, username, password, systype, bind, bind_ton, bind_npi, logfile, loglevel, logrotate, logprivacy, bind_to, elink_interval, res_to, pdu_red_to, trx_to, con_loss_retry, con_loss_delay, con_fail_retry, con_fail_delay, src_addr, src_ton, src_npi, dst_ton, dst_npi, addr_range, proto_id, priority, validity, ripf, def_msg_id, coding, requeue_delay, submit_throughput, dlr_expiry, dlr_msgid, ssl, custom_tlvs`):
```
jcli
smppccm -a
cid myconnector
host <target-host>
port <target-port>
username <system_id>
password <password>
bind transceiver
ok
smppccm -1 myconnector
```
To actually push a `submit_sm`, Jasmin doesn't send it from a jCli command directly — you add an MT route (`mtrouter -a`) pointing traffic at the connector, then trigger it via Jasmin's own HTTP send API (`GET/POST /send`), which the MT router forwards to the connector as an outbound `submit_sm`.
- **enquire_link**: `elink_interval` config key controls the periodic timer per-connector — confirmed directly from source.
- **Windowing**: only `submit_throughput` (a rate cap) was found in the confirmed key list — no distinct max-outstanding-window key surfaced. **Unverified** whether one exists elsewhere in the connector implementation.
- **Long messages / encoding**: `coding` sets the connector's default data_coding; long-message splitting strategy inherited from `smpp.pdu`/`smpp.twisted` beneath it — not independently confirmed this session.
- Being Python/Twisted and used in real MNO/aggregator deployments, Jasmin exercises a genuinely different stack/style from every Java option here — see recommendation notes below.
### A8. fiorix/go-smpp
[fiorix/go-smpp](https://github.com/fiorix/go-smpp), MIT, active (pushed 2026-04-25). Ships a one-shot CLI, `cmd/sms` (`sms send <from> <to> <text>`, env `SMPP_USER`/`SMPP_PASSWD`, flags `--addr`/`--tls`) — good for a single bind+submit+deliver smoke test, not a load driver (no count/rate/window flags). Long-message default, data_coding behaviour, enquire_link interval, and windowing were not independently confirmed this session beyond the CLI's absence of relevant flags — **unverified** at the library level.
### A9. linxGnu/gosmpp
[linxGnu/gosmpp](https://github.com/linxGnu/gosmpp), Apache-2.0, active (pushed 2026-07-13, 21 open issues), described in its own README as "porting from Java OpenSMPP Library." Bind example:
```go
trans, err := gosmpp.NewSession(
gosmpp.TRXConnector(gosmpp.NonTLSDialer, auth),
gosmpp.Settings{ /* EnquireLink: 5*time.Second, OnPDU, OnClosed, ... */ },
5*time.Second)
```
`Settings.EnquireLink` sets a real periodic keepalive interval; `OnClosed`/rebind hooks suggest built-in auto-reconnect behaviour on connection loss (worth probing — does it reconnect and rebind automatically if your server closes on `idleTimeout`?). Supported-PDU list includes `submit_sm_multi`/`data_sm`; exact UDH/SAR default-splitting and data_coding behaviour not confirmed from the README excerpt fetched — **unverified**. No bundled load tool.
### A10. oserl
Canonical repo [archaelus/oserl](https://github.com/archaelus/oserl) — "Enrique Marcote Peña's SMPP for Erlang (mirrored from sourceforge with minor patches)," last updated 2009-04-03: effectively abandoned for 15+ years. Its practical relevance today is that PowerMeMobile's `smppload` (Section B) depends on it under the hood (per `smppload`'s `rebar.config`). Minor satellites found: [dergraf/smpp](https://github.com/dergraf/smpp) (2011, thin Erlang wrapper) and [netDalek/smppex_oserl](https://github.com/netDalek/smppex_oserl) (2019, Elixir SMPPEX↔oserl PDU converter). Treat oserl as "the engine inside smppload," not a library to stand up fresh — use `smppload` itself, or `smppex` (A17) for a genuinely maintained Erlang-VM option.
### A11. libsmpp34
[osmocom/libsmpp34](https://github.com/osmocom/libsmpp34) (mirrored from `gitea.osmocom.org`), C, LGPL-2.1, actively maintained (pushed 2026-08-20, i.e. 16 days before this research). It's a PDU encode/decode codec used inside Osmocom's telecom stack (e.g. OsmoSMSC), not a ready bind/submit/receive CLI. Docker packaging, long-message defaults, and encoding behaviour need a source read not completed this session — **unverified**.
### A12. Rust: smpp34 crate
Of the Rust crates found on [crates.io](https://crates.io/api/v1/crates?q=smpp) — `smpp` (client+server, v0.1.2), **`smpp34`** ("Pure-Rust SMPP 3.4 codec with an async (tokio) client and server", v1.4.1), `smpp-pdu` (parsing only, v0.1.4), `smpp-codec` (SMPP v5 codec, v0.2.1), and the `rusmpp`/`rusmppc`/`rusmppz`/`rusmpp-macros` family (all v0.4.0, modular core + a dedicated client crate `rusmppc`) — **`smpp34`** is the most complete single credible pick (highest version number, explicit client+server+async claim). Repo URL, licence, and operational details (bind example, long-message/encoding defaults) were not independently fetched this session beyond the crates.io registry metadata — **unverified**; read its crates.io page/repo before relying on it.
### A13. C#: JamaaSMPP
[AdhamAwadhi/JamaaSMPP](https://github.com/AdhamAwadhi/JamaaSMPP), a fork/continuation of the original Jamaa Technologies SmppClient, actively maintained (pushed 2026-08-18). NuGet: `Install-Package JamaaSMPP`. README confirms: concatenated/long-message handling via **UDH, SAR, and message_payload — all three, developer's choice**; GSM 03.38 alphabet encoding; custom encoding configurable per client instance; separate-or-combined TX/RX connections. v2.0.0 dropped .NET Framework 4.0 and reworked response-handler threading reliability (worth checking current issues for residual races — not reviewed this session). Licence not confirmed this session — **unverified**, check the repo's `LICENSE` before adoption.
### A14. PHP: php-smpp
Original [OnlineCity/php-smpp](https://github.com/OnlineCity/php-smpp) (pushed 2022-09-18, no licence declared) explicitly states in its README: **"THIS REPO IS NO LONGER MAINTAINED!"**, pointing to the fork [alexandr-mironov/php-smpp](https://github.com/alexandr-mironov/php-smpp) (pushed 2022-12-02, 6 open issues, also no licence declared — flag this as a legal gap before adopting either).
- **Bind**: `bindTransmitter()` / `bindReceiver()` with host/port/username/password. **No `bind_transceiver` support** — README: *"You can't connect as a transceiver, otherwise supported by SMPP v.3.4."* This forces you to run separate TX and RX connections against the server under test, directly exercising direction-enforcement (e.g. your server's `ESME_RINVBNDSTS` handling) on genuinely separate binds rather than one combined TRX session.
- **submit_sm**: `sendSMS()`.
- **Long messages**: three built-in modes — `CSMS_16BIT_TAGS`, `CSMS_PAYLOAD` (`message_payload`), `CSMS_8BIT_UDH` — the broadest single-library choice of long-message wire strategy found in this research.
- **Encoding**: assumes GSM 03.38; ships `GsmEncoder::utf8_to_gsm0338()`.
- **enquire_link**: application-driven on an inactivity timeout (README example: "every 30 seconds of inactivity"), not automatic by default.
### A15. Perl: Net::SMPP
[metacpan.org/pod/Net::SMPP](https://metacpan.org/pod/Net::SMPP), last release **1.19, 2011-06-01** — unmaintained 14+ years, licence unknown per MetaCPAN.
- `bind_transceiver()` and `submit_sm()` documented directly.
- **Long messages**: docs recommend `message_payload` for bodies over 254 bytes (leaving `short_message` empty) — its documented path is message_payload, not UDH/SAR.
- **Encoding**: explicitly does **not** auto-encode — *"Net::SMPP also does not automatically perform the encoding"* — ships `pack_7bit()`/`unpack_7bit()` helpers the caller must invoke explicitly. Useful precisely because you control packed-vs-unpacked GSM 7-bit deliberately, letting you test both against the server on purpose.
### A16. Ruby: ruby-smpp
[ruby-smpp](https://rubygems.org/gems/ruby-smpp) (EventMachine-based), v0.6.0, 108,794 downloads — the most-downloaded of seven SMPP-related Ruby gems found, ahead of its own fork `anjlab-ruby-smpp` (33,740 downloads, v0.6.4) and smaller options (`smpp_encoding`, `rocket_sms`, `Crota`, `rock-queue-smpp`, `anthill_smpp_ruby`). Bind/long-message/encoding specifics not independently fetched this session beyond gem metadata — **unverified**.
### A17. Elixir: smppex
[smppex on hex.pm](https://hex.pm/packages/smppex), "SMPP 3.4 protocol and framework implemented in Elixir," v3.3.0, updated roughly 2 months before this research, 200,620 total downloads — clearly the maintained choice over `esmpp` (v0.0.13, ~9 years stale) and the companion `smppex_telemetry` package (~5 years stale, but its existence confirms a real surrounding ecosystem). Bind/config specifics not independently fetched this session — **unverified**.
### A18. Kannel `bearerbox` as an SMPP client
Kannel is confirmed (via [Wikipedia](https://en.wikipedia.org/wiki/Kannel_(telecommunications))) to be licensed under the "Kannel Software License 1.0" (Apache-1.1-like), official site `kannel.org`. **This session could not independently verify any operational SMPP-client detail**: every fetch attempt against `kannel.org`'s user guide failed with a TLS certificate validation error, and no working canonical GitHub/GitLab mirror of the primary (historically SVN-hosted) repo was confirmed — third-party forks found (e.g. `pruiz/kannel`) are stale (last touched 2014) and not authoritative. The well-known architecture (a `group = smsc` / `smsc = smpp` config stanza binding `bearerbox` as an SMPP client, driven by `smsbox`'s HTTP `sendsms` interface) is widely documented under normal circumstances, but restating its exact config keys from memory here would violate this report's "verify or mark unverified" rule — **treat all Kannel specifics as unverified pending a follow-up session with working TLS to kannel.org (or a fixed CA bundle)**.
---
## B. Load / soak generators
*(Fully researched with WebSearch+WebFetch by a parallel research thread this session.)*
### Summary
| Tool | Repo | Licence | Language | Maintained? |
|---|---|---|---|---|
| smppload | [PowerMeMobile/smppload](https://github.com/PowerMeMobile/smppload) | none found | Erlang | Sporadic (tags 2014→2.5.3; 1 open issue since 2023) |
| SMPPSim | e.g. [haifzhan/SMPPSim](https://github.com/haifzhan/SMPPSim) (unofficial mirror) | custom freeware, non-OSI | Java | Server-only — **not a client load tool** |
| fiorix/go-smpp (`cmd/sms`) | [fiorix/go-smpp](https://github.com/fiorix/go-smpp) | MIT | Go | Active, but one-shot only, no load mode |
| linxGnu/gosmpp | [linxGnu/gosmpp](https://github.com/linxGnu/gosmpp) | Apache-2.0 | Go | Active; no bundled load tool |
| veoo/smpperf | [veoo/smpperf](https://github.com/veoo/smpperf) | MIT | Go | Dead since 2020-06-10; sends one message and exits despite the name |
| vponomarev/libsmpp (`smpp-dumb-client`) | [vponomarev/libsmpp](https://github.com/vponomarev/libsmpp) | LGPL-3.0 | Go | Low activity (2025-07-01) — **the one Go tool with a real bounded window** |
| AShabana/smpp-load-test | [AShabana/smpp-load-test](https://github.com/AShabana/smpp-load-test) | none found | Go | Dead since 2022-08-10 |
| cloudhopper-smpp (library) | [fizzed/cloudhopper-smpp](https://github.com/fizzed/cloudhopper-smpp) | Apache-2.0 per README | Java | Basis for bespoke load tools, not one itself |
| Java-SMPP-Load-GUI | [shaf2k/Java-SMPP-Load-GUI](https://github.com/shaf2k/Java-SMPP-Load-GUI) | none found | Java | Dead, 1 commit (2015) |
| jsmpp StressClient/StressServer | [opentelecoms-org/jsmpp](https://github.com/opentelecoms-org/jsmpp) | Apache-2.0 | Java | Active; bundled example, not a packaged tool |
| wizardjedi/smpp-test-tools | [wizardjedi/smpp-test-tools](https://github.com/wizardjedi/smpp-test-tools) | none found | Java | Low activity (2023-02-23) |
| emgload/emgsink | [smpp.com/smpp-benchmarking.html](https://smpp.com/smpp-benchmarking.html) | proprietary | native, unverified | Actively sold |
| Melrose Labs Load Test Tool | [melroselabs.com/tools/smpploadtest](https://melroselabs.com/tools/smpploadtest/) | proprietary, hosted | n/a | Active service |
### smppload (PowerMeMobile) — the practical free/open pick
- CLI: `-H/--host -P/--port -B/--bind_type(TX|TRX|RX) -i/--system_id -p/--password -r/--rps(1000) -T/--thread_count(10) -c/--count(1) -s/--source -d/--destination -l/--length(140) -D/--delivery -C/--data_coding(3=Latin1)` (from [`src/smppload.erl`](https://raw.githubusercontent.com/PowerMeMobile/smppload/master/src/smppload.erl)):
```
smppload -H smsc.example.com -P 2775 -B trx -i myuser -p mypass \
-s 1234:1,1 -d 15555550100 -c 100000 -r 500 -T 20 -l 140 -D 1
```
- **Windowing**: no true bounded in-flight window — "window" is really `thread_count × rps` (confirmed from [`smppload_esme.erl`](https://raw.githubusercontent.com/PowerMeMobile/smppload/master/src/smppload_esme.erl)).
- **Throttling**: all non-zero `command_status` responses fold into one `send_fail` counter ([`smppload_stats.erl`](https://raw.githubusercontent.com/PowerMeMobile/smppload/master/src/smppload_stats.erl)) — no ESME_RTHROTTLED-specific backoff.
- **Long messages**: yes, UDH-based multipart; UCS2-BE for Unicode.
- **enquire_link**: not implemented at all.
- **Docker**: no official image; needs a hand-built Erlang/OTP 19+ image, tag chosen and pinned by you.
- **Known issues**: [#8](https://github.com/PowerMeMobile/smppload/issues/8) rebar3/BEAM load errors (open); a closed issue documents Erlang 22.3.2 compile failures.
### The one tool with a real bounded window: `vponomarev/libsmpp`'s `smpp-dumb-client`
YAML-driven ([`config.yml`](https://raw.githubusercontent.com/vponomarev/libsmpp/master/app/smpp-dumb-client/config.yml)):
```yaml
smpp:
remote: 127.0.0.1:2500
bind: { systemID: test, systemType: test, password: test, mode: TRX }
generator:
enabled: yes
count: 0 # total messages
rate: 100 # messages/sec
window: 2000 # max outstanding unacked submit_sm — real, enforced
stayConnected: yes
```
Confirmed enforced in [`generator.go`](https://raw.githubusercontent.com/vponomarev/libsmpp/master/app/smpp-dumb-client/generator.go): sending is skipped once in-flight count reaches `window`. No ESME_RTHROTTLED-aware backoff; no long-message (UDH/SAR) support; plain `ShortMessage` body only. Same repo also ships a matching test SMSC (`smpp-dumb-server`) and an `smpp-lb` load balancer.
### SMPPSim — out of scope as a *client* tool
Confirmed server-only from its [official README](https://github.com/haifzhan/SMPPSim/blob/master/SMPPSim_OFFICIAL_README): "a testing utility which mimics the behaviour of an SMPP based SMSC." No client-side load mode exists. Docker mirrors found (`balsagoth/smppsim` last pushed 2017-08-03; `jookies/smppsim` last pushed 2022-08-07) are unofficial and stale.
### Other notes
- **Cloudhopper's demo** (`ClientMain.java`) has real windowing (`setWindowSize`) but is a one-message demo, not a load driver; a bespoke load tool would need to be built on top.
- **jsmpp's `StressClient`/`StressServer`** examples ([source](https://raw.githubusercontent.com/opentelecoms-org/jsmpp/master/jsmpp-examples/src/main/java/org/jsmpp/examples/StressClient.java)) fire `bulkSize` (default 100,000) submits bounded only by a `pduProcessorDegree`-sized thread pool — no rate control, no long messages, no differentiated throttle handling.
- **emgload/emgsink** ([smpp.com](https://smpp.com/smpp-benchmarking.html)) is the one tool here with explicit UDH-via-TLV long-message load support (`--smpp_udh_via_optional`) and very high claimed throughput (~25,000 msg/s), but it's commercial (~599 EUR/yr/host, unlicensed use capped at 10 msg/s) and closed-source.
- **Melrose Labs' hosted Load Test Tool** exposes TPS, concurrent binds, a distinct "submit window," message quantity, and long-message splitting all as separate dials — useful as terminology/prior-art confirmation that "window" and "rate" are properly orthogonal knobs, even though it's not automatable in CI (paid, web-UI only).
**Bottom line for Section B**: `smppload` is the only free, scriptable, protocol-real load generator with long-message support; pair it with `vponomarev/libsmpp`'s `smpp-dumb-client` specifically when you need to test a genuine bounded-window/backpressure scenario against `maxOutstanding`, since `smppload` cannot do that.
---
## C. Independent PDU validators
*(Fully researched with WebSearch+WebFetch by a parallel research thread this session.)*
### C1. Wireshark / tshark — the one to always run
- Built-in dissector: `epan/dissectors/packet-smpp.c` ([source](https://raw.githubusercontent.com/wireshark/wireshark/master/epan/dissectors/packet-smpp.c)); official reference: [wiki.wireshark.org/SMPP](https://wiki.wireshark.org/SMPP). Decodes "most of the version 3.4 specific fields," ~48 distinct TLV tags (`0x0005``0x1383`), and independently reassembles UDH/concatenation by handing `short_message`/`message_payload` off to Wireshark's own GSM-SMS dissector once it sees the `sar_msg_ref_num`/`sar_total_segments`/`sar_segment_seqnum` TLVs.
- **Port handling**: SMPP has an IANA-registered port (2775/tcp, per [IANA's service-names registry](https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml)), but per the wiki, Wireshark's dissector does **not** rely on it — "No well known port is defined for this protocol. The dissector will use heuristics." Force it explicitly on a non-standard port with `-d`.
- **Capture + decode** (flag syntax confirmed against the official [dumpcap](https://www.wireshark.org/docs/man-pages/dumpcap.html)/[tshark](https://www.wireshark.org/docs/man-pages/tshark.html) man pages):
```bash
dumpcap -i lo -f "tcp port 2775" -w smpp.pcapng
tshark -r smpp.pcapng -d tcp.port==2775,smpp -Y smpp -V # verbose text
tshark -r smpp.pcapng -d tcp.port==2775,smpp -Y smpp -T json # JSON, CI-diffable
```
- **Malformed-PDU flagging**: two SMPP-specific expert-info fields were confirmed in source (`ei_smpp_message_payload_duplicate`, `ei_smpp_date_time_decoding_failed`); a full sweep for a generic SMPP "Malformed Packet" entry wasn't completed (file too large to fetch whole) — but Wireshark's core engine independently raises a generic "Malformed Packet" item on any dissector exception (truncated/out-of-bounds read) regardless of protocol, so malformed SMPP still typically surfaces this way.
- **Docker**: no official `wireshark`/`tshark` image under Docker Hub's `library/` namespace (confirmed 404). Verified working: [`nicolaka/netshoot`](https://hub.docker.com/r/nicolaka/netshoot) (tag `v0.16`, pushed 2026-07-01) — its [Dockerfile](https://raw.githubusercontent.com/nicolaka/netshoot/master/Dockerfile) does `apk add tshark`. Fallback (verified package + current tags):
```dockerfile
FROM debian:13.6-slim
RUN apt-get update && apt-get install -y --no-install-recommends tshark \
&& rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["tshark"]
```
(Debian `trixie`'s `tshark` package is confirmed at Wireshark 4.4.18 per [packages.debian.org](https://packages.debian.org/trixie/tshark); `debian:13.6-slim` and `alpine:3.24.1` confirmed as current full-patch tags via the Docker Hub tag API.)
### C2. sngrep-like alternatives
[sngrep](https://github.com/irontec/sngrep/blob/master/README) is confirmed SIP/RTP-only ("displaying SIP calls message flows... SIP packets... PCAP viewer") — no SMPP support, no SMPP mention anywhere. No sngrep-style interactive TUI analyzer specifically for SMPP was found — **none found / unverified**, not ruled out, but nothing credible surfaced.
### C3. Standalone SMPP PDU decoders
| Name | What it is | Maintenance/Licence |
|---|---|---|
| [SMPP PDU Decoder (gurk4n)](https://smpp.gurk4n.com/) | Browser tool, decodes raw hex PDUs; GSM 7-bit/8-bit, UCS2, Latin-1, ASCII; 100% client-side | Unverified |
| [isimplelab SMPP PDU decoder](https://smpp.isimplelab.com/pdu?lang=en) | Part of a broader SMPP server-emulator site | Unverified |
| sysop.fr / Ozeki decoder pages | Found in search, pages unreachable this session (TLS/connection errors) | Unverified |
| jsmpp | Java library; per a (not re-fetched) Google Groups thread, newer versions log all sent/received PDUs at DEBUG — usable as a cross-check dumper | Apache-2.0, active |
| cloudhopper-smpp demo (`ClientMain.java`) | `make client`/`make parser`, logs raw PDU bytes (`setLogBytes(true)`) | Apache-2.0 per README |
| SMPPSim | `smppsim.props` independently exposes `DECODE_PDUS_IN_LOG`, `CAPTURE_SME_BINARY[_TO_FILE]`, `CAPTURE_SMPPSIM_BINARY[_TO_FILE]`, `CAPTURE_SME_DECODED[_TO_FILE]`, `CAPTURE_SMPPSIM_DECODED[_TO_FILE]` — a genuine independent PDU log for cross-checking both directions ([`conf/smppsim.props`](https://raw.githubusercontent.com/haifzhan/SMPPSim/master/conf/smppsim.props)) | Custom freeware, non-OSI |
| [Black Duck Defensics SMPP Server test suite](https://www.blackduck.com/fuzz-testing/defensics/protocols/smpp-server.html) | Commercial fuzzer/conformance suite, 14 SMPP v3.4 message categories | Commercial |
| [Melrose Labs SMPP Analyser / conformance testing](https://melroselabs.com/services/smpp-testing/) | Commercial SMPP-packet analysis + PICS-based conformance service | Commercial |
Note: several near-identical-sounding tools (Diafaan, smspdu.be, smsdeliverer.com) decode the unrelated **GSM SMS-PDU** (3GPP TS 23.040 AT-command) format, not SMPP protocol PDUs — excluded above.
---
## Ranked recommendation
**Run these 34 clients — each catches a different class of bug, deliberately non-overlapping:**
1. **jsmpp (Java)** — for PDU-level/version-negotiation correctness. It silently defaults to interface_version 3.4 if your `bind_resp` omits `sc_interface_version`, but a bogus TLV value there risks an enum-lookup exception client-side — cheap way to probe how forgiving vs. fragile your bind_resp encoding is. Being "not a high-level library," it also gives you full manual control over UDH/SAR/message_payload bytes on the wire, unlike libraries that pick a strategy for you.
2. **php-smpp (`alexandr-mironov` fork)** — for direction-enforcement and long-message wire variety. It refuses `bind_transceiver` outright, forcing separate TX/RX binds against your server (a different code path than a single TRX session) and is the only library found here offering all three long-message strategies (UDH, 16-bit-tag SAR, `message_payload`) from one client.
3. **Cloudhopper-smpp (fizzed fork, Java/Netty)** — for windowing/backpressure. First-class `setWindowSize`/`setRequestExpiryTimeout`/`setWindowMonitorInterval`, and an async `WindowFuture` submission model architecturally distinct from jsmpp's synchronous style — best tool here to stress-test `maxOutstanding` and slow-response handling.
4. **python-smpplib (Python)** — for encoding correctness. It sends GSM 7-bit **unpacked** for data_coding 0 by default (confirmed from source), a genuine, checkable divergence from packed-septet encoding — deliberately useful for catching a data_coding=0 decode bug either way. Its enquire_link is reactive (opt-in, fired on read-timeout) rather than timer-driven, exercising idle-handling differently than the Java options.
Honorable mention: **Jasmin's `smppccm`** (Python/Twisted, real MNO-style production tooling) is worth adding as a 5th if you want production-representative behaviour, but this session couldn't verify enough of its wire-level specifics (long-message default, exact windowing) to rank it with confidence above.
**Validator to always run: Wireshark/tshark (C1).** It is implementation-independent of every client above, decodes every PDU field including TLVs and UDH/concatenation reassembly, and its `-T json` output is scriptable for CI diffing against your own encoder's expected bytes — the only tool in this report that verifies wire encoding rather than exercising server behaviour.
+237
View File
@@ -0,0 +1,237 @@
# Real-world SMPP behaviours & quirks checklist
For `@larvit/smpp` (ESME client + SMSC server, SMPP 3.4 default) pre-1.0 testing. Every item: behaviour, who, source URL, one-line test. Unsourced items are marked **unverified — folklore**.
Legend for repeat sources: Kannel = Kannel User's Guide 1.4.5 (kannel.org); spec = SMPP v3.4 Issue 1.2 (smpp.org/SMPP_v3_4_Issue1_2.pdf, cross-read via sysop.fr HTML mirror where the PDF extractor cut off).
---
## 1. Bind
- interface_version `0x34` = SMPP 3.4; `0x00``0x33` = "3.3 or earlier" — spec. Test: bind with 0x50 (v5) against a 3.4-only server, confirm reject or negotiate via `sc_interface_version`.
- `sc_interface_version` optional TLV in bind_resp reports the SMSC's actual max supported version, independent of what was requested — spec. Test: assert bind_resp TLV reflects server's true version even if client requested something else.
- Kannel: default source-addr TON=2/NPI=1 if left unset in `smsc smpp` group — Kannel User's Guide 1.4.5, https://www.kannel.org/download/1.4.5/userguide-1.4.5/userguide.html. Test: bind without explicit TON/NPI, confirm defaults appear on outgoing PDUs.
- Melrose Labs SMSC Simulator: rejects destination_address <8 chars with `ESME_RINVDSTADR` (0x0B); requires TLS 1.1+ (no SSL/TLS1.0) — https://melroselabs.com/services/smsc-simulator/smsc-simulator-technical-details/. Test: submit_sm with a short dest address, expect rejection; attempt TLS1.0 handshake, expect refusal.
- Sinch (current SMPP product): TLS port 3601, host `<host>.smpp.api.sinch.com`; IP restricted to pre-registered addresses by default; only 2 parallel connections per host/system_id — https://developers.sinch.com/docs/sms/smpp/connectivity. Test: bind a 3rd session on the same system_id, expect rejection.
- Sinch (legacy "Cloud SMPP"): max 3 sessions; TLS on **different ports per encoding** — 8443 standard, 9443 Latin-1 — https://developers.sinch.com/docs/sms/other/sms-other-cloud-smpp. Test: connect TLS to 8443 vs 9443, confirm encoding-dependent routing.
- Infobip: bind as transceiver or receiver to get DLRs (transmitter-only gets none); max 4 sessions by default; only SMPP **3.4** accepted (3.3/5.0 rejected); hosts `smpp3.infobip.com:8888` primary / `smpp1.infobip.com:8888` secondary, TLS `smpp2.infobip.com:8887`; `system_type=HLR` required for Number Lookup bind — https://www.infobip.com/docs/essentials/api-essentials/smpp-specification. Test: bind interface_version 0x33/0x50, expect failure; bind TX-only, confirm no DLR arrives.
- Telnyx: host `smpp.telnyx.com:2775`, TLS required; bind requires `addr_ton=1`/`addr_npi=1`; gated to contracted accounts ($5000/month, 12mo min) — https://support.telnyx.com/en/articles/1667062-short-message-peer-to-peer-set-up-guide. Test: bind with other addr_ton/npi values, expect rejection.
- Vonage: SMPP 3.4; default 2 connections/account — 3rd bind rejected with `command_status=0x00000005` (ESME_RALYBND); recommends clustered host pairs; unique `system_type` per host needed for multi-host inbound; error 13 decimal/`0x0d` (ESME_RBINDFAIL) = wrong zone/host or bad creds — https://api.support.vonage.com/hc/en-us/articles/204015713 , https://api.support.vonage.com/hc/en-us/articles/204015783 , https://api.support.vonage.com/hc/en-us/articles/204591147. Test: open a 3rd bind, assert command_status 0x00000005.
- Bird: server only actually validates `system_id`/`password`/`interface_version` at bind — other fields are ignored; supports 3.3/3.4/5.0 (3.3 loses TLV info in deliver_sm); per-account bind-count/throughput enforced **per SMPP server** (so binding to N servers multiplies the effective cap); plaintext TCP 2775, TLS 2776 (SSLv3+ only) — https://docs.bird.com/connectivity-platform/other-integrations/how-can-i-use-the-short-message-peer-to-peer-smpp-network-protocol. Test: bind with garbage in address_range, confirm still accepted; open the (N+1)th bind on one server and confirm it's rejected while a bind to a second server still works.
- Clickatell: 3.3/3.4 supported; new integrations must pass a **compliance test SMSC** before being switched to production — https://archive.clickatell.com/developers/api-docs/using-the-smpp-api/. Test: model two distinct SMSC endpoints (test/prod) with separate credentials.
- Kaleyra: `interface_version` must be `0x34` for v3.4 features, else silently defaults to 3.3 behaviour; `system_type` should be `smpp`; TRX bind only available when negotiating 3.4 — https://messaging.kaleyra.com/support/solutions/articles/3000091795-introduction-to-smpp-interface. Test: bind at 0x33, confirm TRX-gated features (e.g. TLVs) are withheld vs 0x34.
- tyntec: 4 connection modes — plain SMPP, SSL-SMPP, SMPP-in-VPN, SMPP-with-TLS (on request) — https://www.tyntec.com/helpcenter/docs/faqs/sms/getting-started/what-type-of-connections-does-tyntec-support-for-smpp/. Test: model plain and TLS as distinct configured listeners, not one socket with a flag.
- CM.com: endpoints `smpp_gw.messaging.cm.com:30006` / `smpp_sgw.messaging.cm.com:30007` behind a global load balancer — https://developers.cm.com/messaging/docs/smpp. Test: bind against both, confirm 0x34 accepted on each.
- LINK Mobility (Implementation Guide): interface_version must be 3.4 — anything else fails the bind; `system_type`/`address_range` accepted but ignored; recommends separate TX+RX binds over a single TRX; requires client IP pre-registered (firewalled otherwise) — https://a.storyblok.com/f/151608/x/bb282a50a7/link-mobility-implementation-guide-smpp.pdf. Test: bind with interface_version ≠0x34, expect failure; bind from an unregistered IP, expect connection refused.
- LINK Mobility (SMSC-SMPP User Guide, separate product): TX/RX/TRX all supported; `address_range` **not supported at all**; TLS 1.2/1.3 required (1.0/1.1 discontinued 2020-11-15); TLS port 3601 vs plaintext 3600 — https://www.linkmobility.com/resources/developer/SMSC-SMPP-User-Guide-1.5.pdf. Test: connect on 3600 without TLS vs 3601 with TLS1.2+; attempt TLS1.1, expect rejection.
- Telia: interface_version must be 0x34; `system_type`/`address_range` "not in use" — send NULL; static IP must be ACL-whitelisted; TLS ≥1.2 with SNI required; endpoint `smpp.messaging.teliacompany.com:3550` — https://cdn.messaging.teliacompany.com/documents/developer/index.html. Test: bind with non-null system_type (still succeeds, field ignored); connect from a non-whitelisted IP, expect refusal.
- Telenor: SMSC "supports the most important operations... but does not fully support the protocols" of 3.3/3.4; **OUTBIND, SUBMIT_MULTI and ALERT_NOTIFICATION explicitly not supported** — https://developer.telenor.no/images/97-322719.pdf. Test: send those 3 PDU types to a Telenor-modeled mock, expect no support/rejection.
- Twilio: does offer an SMPP API but with **no self-service onboarding** — enterprise-only, provisioned via account manager for existing SMPP deployments — https://help.twilio.com/articles/31537798746267. Test: not directly testable; document as enterprise-gated rather than a public bind target.
- Telesign: SMPP 3.4/5.0; TX/RX/TRX (persistent TRX preferred); `system_id` max 16 octets, `password` max 9 octets; TLS 1.2 mandatory; Session Init Timer 10s (time to bind after TCP connect); explicitly advises against IP-allowlisting Telesign's own gateway IP (it can change) — https://developer.telesign.com/enterprise/docs/smpp-protocol. Test: assert bind fails if not completed within 10s of TCP connect; reject system_id>16 or password>9 octets.
- Route Mobile: interface_version must be 0x34 to enable v3.4-only fields (long SMS, message_payload) — omitted/0x33 disables them silently rather than erroring; `system_type` unused/must be null; multiple simultaneous binds allowed — https://routemobile.com/pdf_files/developer/api/routemobilesmpp.pdf. Test: bind at 0x33, then attempt message_payload/UDH long-SMS, confirm treated as unavailable (not rejected loudly).
- Bandwidth: bind mode is one of Transceiver (recommended, "for proper DLR delivery")/Sender Only/Receiver Only — DLR delivery is tied to the chosen mode, not forced-TRX; IP allowlisting via priority-weighted groups, highest weight used first — https://www.bandwidth.com/support/en/articles/12823933. Test: bind Receiver Only, submit_sm, confirm rejected/behaves differently than under Transceiver.
- Syniverse: TLS version/ports/certs/IP-allowlisting for the raw SMPP bind live only in a private per-customer "Detail Technical Plan" — not publicly documented — https://sdcsupport.syniverse.com/hc/en-us/articles/18883083316247. Test: document as an untestable gap rather than guess values.
## 2. Keepalive
- spec itself only mandates "reliable data transfer" at the transport layer and defines enquire_link as a no-op PDU — it does **not** mandate a specific interval or disconnect timeout (§2.4) — spec via smpp.org PDF. Test: none — confirms interval/timeout is vendor policy, not a spec requirement.
- Kannel `enquire-link-interval`: default 30 seconds — https://www.kannel.org/download/1.4.5/userguide-1.4.5/userguide.html. Test: idle a bound session 30s+, assert an enquire_link is sent.
- Melrose Labs simulator: sends enquire_link to the ESME after 45s of inactivity (server-initiated keepalive) — https://melroselabs.com/services/smsc-simulator/smsc-simulator-technical-details/. Test: idle a session against the simulator, assert a server-initiated enquire_link at ~45s.
- Sinch (current product): recommends enquire_link every 30s — https://developers.sinch.com/docs/sms/smpp/connectivity. Test: send enquire_link every 30s, confirm connection survives.
- Sinch (legacy Cloud SMPP): recommends 60s instead of 30s — https://developers.sinch.com/docs/sms/other/sms-other-cloud-smpp. Test: verify a different cadence is accepted on this endpoint vs. the current product's.
- Infobip: 30-second keep-alive timeout — https://www.infobip.com/docs/essentials/api-essentials/smpp-specification. Test: withhold all traffic (incl. enquire_link) >30s, expect session closed.
- LINK Mobility (Implementation Guide): SMSC sends enquire_link if idle 60s; read timeout 15s for DLR acks / 30s for other requests; recommended submit_sm_resp timeout 10 minutes; after receiving unbind, client should wait ≥30s before reconnecting — https://a.storyblok.com/f/151608/x/bb282a50a7/link-mobility-implementation-guide-smpp.pdf. Test: idle >60s, expect SMSC-initiated enquire_link; reconnect <30s after an unbind, check for rejection.
- LINK Mobility (SMSC-SMPP User Guide): client should call enquire_link every 60 seconds — https://www.linkmobility.com/resources/developer/SMSC-SMPP-User-Guide-1.5.pdf. Test: send at >60s intervals and confirm no disconnect vs sending too rarely.
- Telia: recommended connect/read timeout 10 seconds — https://cdn.messaging.teliacompany.com/documents/developer/index.html. Test: set client socket read timeout to 10s baseline.
- Telenor: general SMSC doc states 300s (5 min) default TCP inactivity timeout (scope not confirmed SMPP-exclusive — lower confidence) — https://developer.telenor.no/images/97-322719.pdf. Test: idle 5 min, expect disconnect.
- Clickatell: must send ≥1 enquire_link every **5 minutes** or be disconnected; recommends sending one every 1 minute for prompt lost-connection detection — https://archive.clickatell.com/developers/api-docs/connection-details/. Test: assert connection survives at 4:59 idle with no enquire_link, torn down by ~5:00.
- Kaleyra: client should enquire_link every 60s; platform auto-disconnects any link idle >5 minutes — https://messaging.kaleyra.com/support/solutions/articles/3000091795-introduction-to-smpp-interface. Test: same 5-minute idle-disconnect boundary assertion.
- Telesign: Enquire Link Timer = 30s; Response Timer = 30s for awaiting responses — https://developer.telesign.com/enterprise/docs/additional-features-smpp-protocol. Test: assert client sends enquire_link at ≤30s idle; assert a pending response times out at 30s.
- Route Mobile: client should enquire_link every minute; platform auto-disconnects any link inactive >5 minutes — routemobile.com PDF above. Test: simulate 5+ min silence, assert server closes socket.
- Bandwidth, Syniverse: no public keepalive numbers found — skipped.
## 3. Windowing / throughput
- Kannel `max-pending-submits`: max outstanding unacked SMPP ops between ESME/SMSC, default (recommended) 10 — Kannel User's Guide 1.4.5. Test: submit >10 concurrently without waiting for resp, assert Kannel throttles/queues further sends.
- Kannel `wait-ack` (default 60s) + `wait-ack-expire`: `0x00` disconnect/reconnect (default), `0x01` requeue ("could result in the msg arriving twice"), `0x02` keep waiting — Kannel User's Guide 1.4.5. Test: hold back a submit_sm_resp past `wait-ack` seconds and assert the configured expire action.
- Melrose Labs: deliver_sm window size = 25 (default) — https://melroselabs.com/services/smsc-simulator/smsc-simulator-technical-details/. Test: have the simulator queue >25 unacked deliver_sm and confirm it stalls further sends until acked.
- smpp.org free public test simulator: max 2 SMS/sec; paid tiers up to 100/sec or 10,000/sec — https://smpp.org/smpp-testing-development.html. Test: burst >2/sec against the free tier, expect throttling.
- Jasmin `submit_throughput`: MPS cap per connector, 0 = unlimited — https://docs.jasminsms.com/en/latest/management/jcli/modules.html. Test: exceed configured MPS on a connector, assert submissions delayed/rejected.
- `ESME_RTHROTTLED` (0x00000058/88, "ESME exceeded allowed message limits") vs `ESME_RMSGQFUL` (0x00000014/20, "Message Queue Full") — https://smpp.org/smpp-error-codes.html. Test: assert the library distinguishes these and applies different backoff (throttled = slow down; queue full = far-end resource, not rate, exhausted).
- SMPPSim: on receiving `ESME_RMSGQFUL` from the ESME for a deliver_sm, retries after a delay governed by `DELAYED_INBOUND_QUEUE_PROCESSING_PERIOD`/`DELAYED_INBOUND_QUEUE_MAX_ATTEMPTS` — https://github.com/haifzhan/SMPPSim/blob/master/SMPPSim_OFFICIAL_README. Test: reply ESME_RMSGQFUL to a deliver_sm, assert SMPPSim retries rather than dropping.
- Sinch: default throughput 10 msg/s per bind; recommended default window size 10 — https://developers.sinch.com/docs/sms/smpp/connectivity. Test: submit 11 concurrent unacked submit_sm, confirm throttling/rejection.
- Infobip: "no other TPS limitations besides internet speed"; scale via more binds (up to 4) rather than a hard cap — https://www.infobip.com/docs/essentials/api-essentials/smpp-specification. Test: ramp TPS on one bind and confirm no ESME_RTHROTTLED absent an explicit account limit.
- Telnyx: rate limits are **per-number**, not per-bind/window — 10 msg/number/min long code, 1200 msg/number/min toll-free — https://support.telnyx.com/en/articles/1667062-short-message-peer-to-peer-set-up-guide. Test: submit >10/min to the same long code sender, confirm per-number (not per-connection) throttling.
- Vonage: default 30 SMS/s per account; `ESME_RTHROTTLED`=0x00000058 returned on excess; no server-side queueing beyond this — https://api.support.vonage.com/hc/en-us/articles/204015703. Test: submit >30/s, assert 0x00000058 on the excess.
- Bird: provisioned bind-count + msg/s per account, enforced per SMPP server — https://docs.bird.com/connectivity-platform/other-integrations/how-can-i-use-the-short-message-peer-to-peer-smpp-network-protocol. Test: submit above provisioned rate, confirm a throttling status rather than silent unbounded queueing.
- Clickatell: recommends configuring the client with the account's max bind count and using async "windowing" (pipeline submit_sm without waiting per-resp) to reach full throughput — https://archive.clickatell.com/developers/api-docs/using-the-smpp-api/. Test: pipeline N outstanding submit_sm before the first resp arrives, confirm client doesn't serialize.
- LINK Mobility (Implementation Guide): default max window = 20 outstanding ops; default max throughput 10 msg/s per account (both configurable); exceeding either triggers throttling (ESME_RTHROTTLED=88), sometimes **without the vendor's own extension TLV present** — https://a.storyblok.com/f/151608/x/bb282a50a7/link-mobility-implementation-guide-smpp.pdf. Test: send >20 outstanding or >10/s, expect ESME_RTHROTTLED, possibly missing the vendor TLV.
- Telia: default window size 10, adjustable by contacting support — https://cdn.messaging.teliacompany.com/documents/developer/index.html. Test: send 11+ outstanding requests against the default window, expect throttling.
- Telenor: window size configurable 199, defaulting to 1 (stop-and-wait) — general SMSC doc, scope not confirmed SMPP-exclusive — https://developer.telenor.no/images/97-322719.pdf. Test: with window=1, verify strict request/response serialization required.
- Route Mobile: `ESME_RTHROTTLED`=0x00000058, no published numeric window/TPS — routemobile.com PDF. Test: assert 0x58 in submit_sm_resp triggers retry-with-backoff, not a hard failure.
- Bandwidth: baseline 1 MPS; Local A2P/10DLC default 1 MPS; toll-free default 3 MPS; short codes default 100 MPS; account-level limit overrides per-number limit; queue length default 15 min, max 4h; T-Mobile P2P capped at 300 unique recipients/day/sending number; **inbound (MO) has no rate limit** — https://www.bandwidth.com/support/en/articles/12823226 , https://www.bandwidth.com/support/en/articles/12823224. Test: burst MO traffic at the client, confirm no artificial inbound throttling expected; assert MT caps match documented MPS per number class.
- Syniverse: distinct codes 1039 "Throttling error" vs 1038 "Outgoing queue is full" (conceptually mirrors RTHROTTLED vs RMSGQFUL) — https://sdcsupport.syniverse.com/hc/en-us/articles/360038257473. Test: assert distinct handling paths exist for "throttled" vs "queue full" outcomes.
## 4. Message ids
- Kannel `msg-id-type` (03): `0x00` both decimal, `0x01` deliver_sm decimal/submit_sm_resp hex, `0x02` deliver_sm hex/submit_sm_resp decimal, `0x03` both hex — Kannel User's Guide 1.4.5. Test: set each value and assert submit_sm_resp id vs. DLR `id:` field parse in the matching base.
- Jasmin `dlr_msgid`: `0` = identical id in both submit_sm_resp and deliver_sm, `1` = submit_sm_resp hex / deliver_sm decimal, `2` = reverse — https://docs.jasminsms.com/en/latest/management/jcli/modules.html. Test: mirrors Kannel's quirk under a different config name — same base-mismatch bug class, two vendors, two spellings.
- Jasmin: internal message id is a UUID (gateway-side tracking) distinct from `id_smsc` (an integer — what the SMSC actually returned) — https://docs.jasminsms.com/en/latest/apis/http/index.html. Test: verify a test double returning a UUID as submit_sm_resp message_id is accepted (spec only requires a C-octet string, no fixed format).
- Melrose Labs simulator: message_id is 8 chars for bound v3.3 sessions, 64 chars for v3.4/v5 — https://melroselabs.com/services/smsc-simulator/smsc-simulator-technical-details/. Test: bind at 3.3 vs 3.4 against the simulator, assert id length differs.
- SMPPSim: randomised message ID start value with a configurable prefix (v2.6.0+) — not sequential/predictable — https://github.com/haifzhan/SMPPSim/blob/master/SMPPSim_OFFICIAL_README. Test: assert client doesn't assume sequential/incrementing ids from this simulator.
- Sinch (legacy Cloud SMPP): message_id is **hex on SMPP 3.3 binds, standard ASCII integer (decimal) on SMPP 3.4 binds** — same backend, format keyed off negotiated interface_version — https://developers.sinch.com/docs/sms/other/sms-other-cloud-smpp. Test: bind at 0x33 vs 0x34, confirm message_id format in submit_sm_resp changes accordingly.
- Vonage: by default, `submit_sm_resp.message_id` is **hex** while the corresponding `deliver_sm` DLR `id:` field is **decimal** — the two don't match without conversion unless the account is reconfigured; hex deprecated in 2023 in favor of UUID format for both — https://api.support.vonage.com/hc/en-us/articles/204015803 , https://api.support.vonage.com/hc/en-us/articles/204015663. Test: correlate submit_sm_resp message_id (hex) against DLR `id:` (decimal), confirm conversion is required to match; also test UUID-format ids on newer accounts.
- Bird: `receipted_message_id` TLV (tag 0x001E) in the DLR carries the **same value** as `submit_sm_resp.message_id` — https://developers.messagebird.com/api/sms-messaging/. Test: capture submit_sm_resp message_id, assert the DLR's receipted_message_id TLV matches byte-for-byte.
- Clickatell: message identity is carried in the DLR's `short_message` text (`id:xxx`) as well as via TLV — https://archive.clickatell.com/developers/api-docs/pdu-details/. Test: parse the DLR text body's `id:` token and confirm it matches submit_sm_resp's message_id.
- Telesign: `message_id` is a 32-digit hex value, random per request; for a concatenated message, submit_sm_resp returns the reference id **only for the first part** (a `message_parts_count` TLV covers the rest) — https://developer.telesign.com/enterprise/docs/smpp-protocol. Test: submit a message that auto-splits into N parts, assert only one message_id returns while `message_parts_count`=N.
- Bandwidth: `submit_sm_resp.message_id` is a **36-char UUID** (migrated from a legacy 10-char id), echoed back in the `receipted_message_id` TLV (0x1E) of the DLR — https://support.bandwidth.com/hc/en-us/articles/360046149833. Test: assert the library accepts a 36-char UUID-format message_id (not just numeric/short ids) and correlates via the TLV, not string-matching the DLR body.
- Route Mobile: only documents `ESME_RINVMSGID` (0x0C); no explicit hex/decimal format published — routemobile.com PDF. Test: N/A — document as an unpublished detail.
## 5. Delivery receipts
- spec: `sm_length`/`short_message` max 254 octets; `sm_length` must be 0 when `message_payload` TLV is used to carry >254 octets of user data — spec §1.2.21/1.2.22 via sysop.fr mirror. Test: assert a client switches to `message_payload` with `sm_length=0` once payload exceeds 254 octets rather than truncating.
- spec `esm_class` bit meanings for MC→ESME (deliver_sm): `0x00` normal message, `0x04` "Short Message contains SMSC Delivery Receipt", `0x08` SME delivery acknowledgement, `0x10` SME manual/user acknowledgement, `0x20` "Intermediate Delivery Notification" — spec via sysop.fr mirror. Test: assert a DLR arrives with esm_class 0x04 (not 0x00) and an intermediate notification with 0x20 — a naive "esm_class != 0 means DLR" check wrongly classifies intermediates as final.
- spec `registered_delivery` bits 1,0: `01` receipt on success or failure, `10` receipt on failure only, `00` none (default); intermediate notification requested via `registered_delivery & 0x10 == 0x10` — corroborated by SMPPSim ("set to 0x11 for both intermediate and final") — spec via sysop.fr mirror; https://github.com/haifzhan/SMPPSim/blob/master/SMPPSim_OFFICIAL_README. Test: request 0x01 vs 0x02 vs 0x11, assert the right receipt combination arrives.
- smpp.org receipt text format: `id` (variable), `sub` (3 octets, zero-padded), `dlvrd` (3 octets, zero-padded), `submit date`/`done date` (10 octets, YYMMDDhhmm), `stat` (7 octets), `err` (3 octets), `text` (**20 octets, documented as "unused field, result will be blank"** — not truncated message content, contrary to some vendor implementations) — https://smpp.org/smpp-delivery-receipt.html. Test: assert `text:` is blank by default and that a parser doesn't assume it always carries 20 chars of truncated body text.
- smpp.org's own `message_state`-numbered stat list: 0 SCHEDULED (intermediate), 1 ENROUTE (intermediate), 2 DELIVERED, 3 EXPIRED, 4 DELETED, 5 UNDELIVERABLE — https://smpp.org/smpp-delivery-receipt.html — cross-validated against Telesign's TLV table (`message_state`: 2=DELIVRD, 3=EXPIRED, 4=DELETED, 5=UNDELIV, 6=ACCEPTD, 7=UNKNOWN, 8=REJECTD, 9=SKIPPED vendor-added) — https://developer.telesign.com/enterprise/docs/sms-smpp-tlvs. Test: assert the numeric `message_state` TLV and the 7-char `stat:` text field are kept in sync by any test double (they're two encodings of the same fact and can drift).
- Melrose Labs simulator: DLR TLVs `receipted_message_id`, `message_state` (2=DELIVERED), `network_error_code` (0=no error); DLR "returned <1s after submit_sm_resp"; MO deliver_sm echoes the `data_coding` used on the original submit — https://melroselabs.com/services/smsc-simulator/smsc-simulator-technical-details/. Test: assert DLR arrives within ~1s and that MO deliver_sm's data_coding matches the original submit's.
- Jasmin `dlr_level`: `1`=SMS-C level, `2`=Terminal level, `3`=Both — https://docs.jasminsms.com/en/latest/apis/http/index.html. Test: request each level, confirm the number/timing of DLR callbacks differs (SMSC-level fires once at ack; terminal-level needs a simulated MO ack).
- Sinch: DLR body follows spec Appendix B; deliver_sm inbound fields (service_type, esm_class, protocol_id, priority_flag, schedule_delivery_time, validity_period, registered_delivery, replace_if_present_flag, sm_default_msg_id) are "always 0"; optional vendor TLV 0x1403 carries destination MCC+MNC — https://developers.sinch.com/docs/sms/smpp/send-message , https://developers.sinch.com/docs/sms/smpp/receive-message. Test: parse deliver_sm and confirm those fields are always zero; check for optional TLV 0x1403.
- Infobip: DLR format `id: sub: dlvrd: submit date: done date: stat: err:`; documented stat values include **DELIVRD, EXPIRED, UNDELIV, ACCEPTD, UNKNOWN, ENROUTE, REJECTD** — note ENROUTE appears as a possible DLR stat here, not just an intermediate flag — https://www.infobip.com/docs/essentials/api-essentials/smpp-specification. Test: assert the parser accepts `stat:ENROUTE` as a valid value, not just the plain 6-value spec set.
- Vonage: DLR format `id: sub:001 dlvrd:000 submit date: done date: stat: err: text:none`; `err:` only populated (non-000) when state ≠ DELIVRD/ACCEPTD; documented stat values: **ACCEPTD, DELIVRD, UNDELIV, EXPIRED, FAILED, REJECTD, DELETED, UNKNOWN** (8 values, including FAILED/DELETED beyond the plain spec set); **one DLR per segment** for concatenated messages, not one per logical message; retries deliver_sm toward the ESME on temporary-failure command_status, up to 130 attempts ≥62s apart, discarding after 24h — https://api.support.vonage.com/hc/en-us/articles/204015663 , https://api.support.vonage.com/hc/en-us/articles/204015773 , https://developer.vonage.com/en/messaging/sms/guides/delivery-receipts. Test: (a) assert err:000 only on DELIVRD/ACCEPTD; (b) return a temporary-failure status on deliver_sm_resp, confirm Vonage retries rather than treating it as final; (c) confirm one DLR per segment on a multi-part SMS.
- Bird: `message_state` TLV (0x0427) = final receipt state; `network_error_code` TLV (0x0423) = carrier-level error, distinct from the text `err:` value — both map into Bird's own numeric error table (0131) — https://developers.messagebird.com/api/sms-messaging/. Test: build DLR fixtures with both TLVs set and assert the parser surfaces them independently rather than conflating carrier code with SMPP err digits.
- Clickatell: DLR body exactly `id:xxx sub:001 dlvrd:NNN submit date:YYMMDDHHMMSS done date:YYMMDDHHMMSS stat:xxx err:000 text:`; observed stat values DELIVRD/ACCEPTD/REJECTD/UNDELIV — https://archive.clickatell.com/developers/api-docs/pdu-details/. Test: parse against this exact field order, reject malformed variants (missing `sub:`, wrong date length).
- Kaleyra: DLR only returned if `registered_delivery=1` **and** bound as receiver/transceiver (TX-only gets none); stat values DELIVRD/FAILED/EXPIRED/UNDELIV with an extensive per-operator err code table (000 DELIVRD, 001 unknown subscriber, 003/010 absent subscriber, 004/006/007 handset error, 005 barred, 008/012 net error, 011 SMSC system failure, 013 mobile off, 016 handset busy, 023032 DLT/template failures, hex 10241911 for blacklisting/DND/etc) — https://messaging.kaleyra.com/support/solutions/articles/3000091798-delivery-reports , https://messaging.kaleyra.com/support/solutions/articles/3000091797-error-codes-and-descriptions. Test: (a) submit with registered_delivery=0, assert no DLR ever arrives; (b) submit with registered_delivery=1 on a TX-only bind, assert none arrives there; (c) table-driven test mapping numeric err code to stat value.
- tyntec: sends a **temporary "buffered" DLR shortly after submission, followed by a final DLR later** — two receipts per message is expected, not a bug; large GSM error code table (0x0000 no error, 0x0001 unknown subscriber, 0x0006 absent subscriber, 0x000d call barred, 0x0021 message-waiting-list full, 0x0022 system failure, 0x6000 SIM memory full, 0xa001 no network response, 0xa002 message too long) — https://www.tyntec.com/helpcenter/docs/faqs/sms/sms-delivery/how-can-i-troubleshoot-sms-delivery/ , https://www.tyntec.com/helpcenter/docs/faqs/sms/sms-delivery/gsm-error-codes/. Test: assert two deliver_sm's for one submit_sm are accepted, not treated as a duplicate.
- CM.com: DLR format `id:(32 chars) submit date: done date: stat: err:`; confirmed 7-char stat values DELIVRD/EXPIRED/DELETED/UNDELIV/ACCEPTD/UNKNOWN/REJECTD; sent via deliver_sm's short_message OR via data_sm's message_payload depending on account config; also carries a `message_state` TLV; proprietary TLVs `operator` (0x1401, MCC/MNC) and `tariff` (0x140A) — https://developers.cm.com/messaging/docs/smpp. Test: parse both text-body stat and message_state TLV, assert agreement; assert exactly 7-char stat.
- LINK Mobility (SMSC-SMPP User Guide): stat values DELIVRD/EXPIRED/REJECTD/UNDELIV/DELETED; extended format `sub:000 dlvrd:000 submit date: done date: stat: err: text:` where **`sub`/`dlvrd` are always literally "000" and `text` is always empty**; only final delivery reports supported, **no intermediate receipts** — https://www.linkmobility.com/resources/developer/SMSC-SMPP-User-Guide-1.5.pdf. Test: assert sub/dlvrd are literally "000" and text is empty on every DLR from this profile; assert requesting an intermediate notification has no effect.
- LINK Mobility (Implementation Guide): `esm_class` always 0x04 for DLRs (unless configured for legacy "Logica style" text-only DRs); TLVs `message_state` (0x0427), `network_error_code` (0x0423), `receipted_message_id` (0x001E); vendor extension TLVs 0x17000x1706 (timestamp/operator/operator_timestamp/status_code/reason_code/status_text/message_id); non-standard `registered_delivery=0x21` ("request server delivery report") beyond the spec's normal value set — https://a.storyblok.com/f/151608/x/bb282a50a7/link-mobility-implementation-guide-smpp.pdf. Test: bind with/without "Logica style DR" config, assert DR shape differs (TLV-based vs legacy text); test 0x21 as a vendor-extension registered_delivery value.
- Telia: `network_error_code` TLV values SUBSCRIBER_UNKNOWN(201), SUBSCRIBER_TEMPORARILY_BARRED(202), REFUSED_BY_SERVICE_PROVIDER(207), SUBSCRIBER_ABSENT(208), ILLEGAL_EQUIPMENT(209) — https://cdn.messaging.teliacompany.com/documents/developer/index.html. Test: assert network_error_code values map to this reason set on simulated failure receipts.
- Twilio: supports DLRs across short/long code, 10DLC, alphanumeric via its enterprise SMPP API but explicitly **not** Messaging-Service-specific REST features (link shortening, scheduling, advanced opt-out) via that path — https://help.twilio.com/articles/31537798746267. Test: assert SMPP-submitted messages don't trigger REST-only status-callback features.
- Telesign: DLR presence signaled by esm_class bit 2; `err` is a 3-octet hex code with 40+ vendor values (e.g. 0x000004A6 blocked, 0x000004C2 sender-ID restriction); status given redundantly in both text body and TLVs; `message_parts_count` TLV reports segment count; DLRs route to a random connected data-center unless `system_type` targets one — https://developer.telesign.com/enterprise/docs/smpp-protocol , https://developer.telesign.com/enterprise/docs/sms-smpp-tlvs. Test: assert DLR text and TLV state never disagree; assert message_state=9 maps to a SKIPPED state most other vendors lack.
- Route Mobile: DLR only if bound RX/TRX and registered_delivery=1; base stats DELIVRD/FAILED/EXPIRED/UNDELIV/REJECTD plus a numeric 000412 status-error table (e.g. 001 unidentified subscriber→UNDELIV, 027 absent subscriber→EXPIRED, 408 DND→REJECTD, 411 duplicate submission→REJECTD) — routemobile.com PDF. Test: fixture-test each numeric code against its documented stat bucket.
- Bandwidth: `ACCEPTD` is "the most precise indicator of a successful message" (vs `DELIVRD` = actual handset confirmation); DLR listening window is **73 hours**, after which error 902/9902 (not a failure) or 4794 ("expired by carrier") if a late DLR does arrive; international "Intermediate DLR" only confirms carrier hand-off, not handset delivery, and DLRs aren't guaranteed internationally — https://www.bandwidth.com/support/en/articles/12823143 , https://www.bandwidth.com/support/en/articles/12823161 , https://www.bandwidth.com/support/en/articles/12823152. Test: assert a DLR arriving after 73h is flagged distinctly (902/9902/4794), not as generic UNDELIV; assert an intermediate DLR isn't conflated with terminal DELIVRD.
- Syniverse: explicit code table — DELIVRD=0, DELIVERED-TO-CARRIER=3/ACCEPTED-BUFFERED=4 (interim success), DELETED=5, UNDELIV=7/9/90/181/190, EXPIRED=8, REJECTD=23/35, INTERNAL ERROR=98, BAD ADDRESS=90, INVALID ROUTING=91, CARRIER GATEWAY ERROR=93, intermediate carrier states 184191 ("UNDELIV-INTERIM"), UNKNOWN=999 — https://sdcsupport.syniverse.com/hc/en-us/articles/360038257473. Test: fixture-test each numeric code against its documented interim-vs-terminal classification.
## 6. Long messages
- spec: `message_payload` TLV holds up to 64K octets — spec via smpp.org PDF fetch. Test: cap concatenation/payload tests at 64K.
- spec segmentation TLV tags: `sar_msg_ref_num`=0x020C, `sar_total_segments`=0x020E, `sar_segment_seqnum`=0x020F — spec via sysop.fr mirror. Test: submit a 3-segment message via sar_* TLVs (not UDH), confirm the receiver reassembles using the TLVs.
- UDH 8-bit vs 16-bit reference number, MO reassembly rules, max-segment limits, and a hard ">160 char rejection" rule: **not documented in the SMPP spec itself** (GSM 03.40 territory) — spec review. unverified — folklore beyond vendor-specific figures below.
- Sinch: auto-splits oversized single submissions server-side; client is not required to pre-segment — https://developers.sinch.com/docs/sms/smpp/send-message. Test: submit a single long body exceeding one segment, confirm Sinch splits it rather than rejecting.
- Infobip: **long SMS not supported for Flash notifications** — a concatenated + flash (data_coding 0x10/0x18) combination is unsupported — https://www.infobip.com/docs/essentials/api-essentials/smpp-specification. Test: submit a >1-segment flash SMS, confirm rejection/silent truncation.
- Vonage: UDH concatenation via esm_class=0x40 (UDHI bit); char budgets 160/306/459/612 for 14 part GSM-7 (≈153/part after UDH), 70 for single UCS-2 segment; accepts up to 3200 chars but "not all carriers do" — recommends max 6 parts; **all parts must go through the same SMPP server/gateway** or the handset never reassembles; **20-minute window** to submit all parts of one multipart SMS; MO concat params (`concat`/`concat-ref`/`concat-total`/`concat-part`) absent for US Sprint/Verizon — recommends grouping by same-sender + close timestamp as fallback — https://developer.vonage.com/en/messaging/sms/guides/concatenation-and-encoding , https://api.support.vonage.com/hc/en-us/articles/360046874352 , https://api.support.vonage.com/hc/en-us/articles/11615117187484 , https://api.support.vonage.com/hc/en-us/articles/205704158. Test: (a) submit segments with the same UDH ref to two different binds, confirm no reassembly; (b) delay a segment past 20 min, confirm the multipart message fails; (c) simulate MO concat without concat-ref params, confirm the client falls back to a heuristic.
- Bird: no public max-segment/UDH-vs-sar specifics beyond what's in topic 5/7 — skip.
- Clickatell: `message_payload` (up to 64K) as an alternative to short_message; optional `sar_msg_ref_num`/`sar_segment_seqnum`/`sar_total_segments` TLVs supported on submit_sm — https://archive.clickatell.com/developers/api-docs/using-the-smpp-api/ , https://archive.clickatell.com/developers/api-docs/pdu-details/. Test: submit >160 chars via message_payload only (short_message len 0), confirm accepted; submit the same via sar_* TLVs, confirm equivalent behaviour.
- Kaleyra: no `submit_multi`; concatenation via esm_class=0x43 ("Store and Forward with UDHI") with UDH inside short_message; `message_payload` (≤64K) **only available at interface_version 0x34** — requesting it under 3.3 silently has no effect — https://messaging.kaleyra.com/support/solutions/articles/3000091796-submitting-messages-through-smpp. Test: bind at 0x33, attempt message_payload, confirm rejected/ignored; works after rebinding at 0x34.
- tyntec: without UDH-based concatenation, MO/MT segment **order is not guaranteed**; fallback is appending a page marker (`1/3, 2/3, 3/3`) to each part's text — https://www.tyntec.com/helpcenter/docs/faqs/sms/sms-features/will-my-sms-messages-arrive-in-order-in-case-of-concatenated-sms/. Test: send 3 segments out of order from a mock SMSC, confirm the client reassembles by sar/UDH sequence number rather than arrival order.
- CM.com: no long-message specifics beyond DLR/payload routing already in topic 5 — skip.
- LINK Mobility (Implementation Guide): UDH only, signaled purely via esm_class bit 6 (0x40 present / 0x00 absent) — **no sar_* TLVs, no message_payload** supported; non-GSM chars force a 70-char UCS2 segment, not 160-char GSM — https://a.storyblok.com/f/151608/x/bb282a50a7/link-mobility-implementation-guide-smpp.pdf. Test: assert non-GSM chars force UCS2 70-char splitting, not GSM 160.
- LINK Mobility (SMSC-SMPP User Guide): `message_payload` TLV **not supported** — "only one SMS may be delivered per call" — https://www.linkmobility.com/resources/developer/SMSC-SMPP-User-Guide-1.5.pdf. Test: submit with message_payload against this profile, expect rejection/ignored.
- Telia: `message_payload` explicitly supported to avoid splitting into separate submit_sm; 153 GSM chars/segment when concatenating; REST API (separate from SMPP) caps at 16 segments — https://cdn.messaging.teliacompany.com/documents/developer/index.html. Test: assert 153-char GSM segment boundary for concatenated (not single) SMS via SMPP.
- Telesign: base SMS 140 bytes; single-message limits GSM7=160/ASCII=140/Latin-1=140/UTF-16BE=70; per-segment limits when concatenated GSM7=153/ASCII=134/Latin-1=134/UTF-16BE=67 (6-byte UDH overhead); **max 10 segments**; "smart splitting" avoids breaking words/URLs across segments; auto-splits an oversized single message and adds UDH per part, billed per part — https://developer.telesign.com/enterprise/docs/understand-sms-encoding-character-limits-and-splitting , https://developer.telesign.com/enterprise/docs/smpp-protocol. Test: assert segmentation breakpoints match 160/153 (GSM7) and 70/67 (UCS2); refuse an 11th segment.
- Route Mobile: **no `submit_multi` or `sar_*` TLVs at all**; concatenation only via UDH with esm_class=0x43; `message_payload` up to 64K but only at interface_version=0x34; strict UDH length validation — `ESME_RINVUDHLEN` (0x406) unless UDH length is 05/06 (any data_coding) or 0b (only data_coding=245) — routemobile.com PDF. Test: submit with sar_msg_ref_num TLV set, assert rejection (`ESME_ROPTPARNOTALLWD`); submit UDH length 07, assert `ESME_RINVUDHLEN`.
- Syniverse: GSM7 single=160/fragment=152 chars; UCS-2 single=70/fragment=67 chars; max **10 fragments**; extended GSM chars (€, ^, |, etc.) count as **2 septets** each — https://sdcsupport.syniverse.com/hc/en-us/articles/360010947674. Test: assert a message containing `€` consumes 2 GSM7 code units for segmentation-count purposes.
## 7. Encodings
- spec `data_coding` table: `0x00` SMSC Default Alphabet, `0x01` IA5/ASCII, `0x02` Octet unspecified (8-bit binary), `0x03` Latin-1, `0x04` Octet unspecified (8-bit binary) — **spec itself lists 0x02 and 0x04 as duplicate "binary" entries**; `0x05` JIS, `0x06` Cyrillic, `0x07` Latin/Hebrew, `0x08` UCS2, `0x09` Pictogram, `0x0A` ISO-2022-JP, `0x0D` Extended Kanji JIS, `0x0E` KS C 5601, `0xCC``0xDF` GSM MWI control, `0xF0``0xFF` GSM message class control (where flash/class-0 lives — the common "0x10/0x18" shorthand is a non-standard vendor simplification of this range) — spec via sysop.fr mirror. Test: verify the library picks one binary code (most vendors use 4, not 2) and documents the choice; verify flash handling covers the 0xF0-0xFF range, not just the two literal bytes 0x10/0x18.
- spec `esm_class` UDHI bit 0x40 ("UDHI Indicator") — spec via sysop.fr mirror. Test: an 8-bit binary message (data_coding=4) carrying UDH must set esm_class 0x40; a decoder should flag UDH-shaped payload bytes arriving without this bit set.
- Jasmin's `coding` parameter mirrors the spec's DCS table 1:1 (0=SMSC default…8=UCS2…13=Extended Kanji JIS, 14=KS C 5601) — https://docs.jasminsms.com/en/latest/management/jcli/modules.html. Test: round-trip all Jasmin-listed DCS values through the parser.
- National language shift tables: not found documented in any researched source — unverified — folklore.
- Sinch: data_coding 0x00 GSM-7 default, 0x01 US-ASCII, 0x02/0x04 binary, 0x03 Latin-1, 0x08 UCS2/UTF-16BE — **but "only characters within the GSM-7 table can be parsed to handset" for DCS 3**, i.e. Latin-1 bytes outside GSM-7 mis-render on many handsets — https://developers.sinch.com/docs/sms/smpp/message-encoding. Test: send data_coding=3 with non-GSM-7 chars, confirm mis-rendering is documented/expected, not a bug to "fix".
- Infobip: DCS 0/1 = GSM7/IA5 default, 3 = Latin-1, 8 = Unicode/UCS-2; flash uses DCS **16 or 24** (both valid) — https://www.infobip.com/docs/essentials/api-essentials/smpp-specification. Test: submit flash with both 0x10 and 0x18, confirm both accepted.
- Vonage: **default encoding for new accounts is Latin-1 (ISO-8859-1), not GSM-7** — a real provisioning-default quirk; example DCS: Latin-1=3, Cyrillic=6, Hebrew=7, Unicode=8 — https://api.support.vonage.com/hc/en-us/articles/204015683 , https://api.support.vonage.com/hc/en-us/articles/204015813. Test: send GSM-7-only text on a fresh Vonage account without specifying data_coding, confirm it defaults to 0x03, not 0x00.
- Bird: supported data_coding {0,1,2,8}; any other value received is **remapped to the "most appropriate" of 0/2/8** before forwarding to the operator — https://developers.messagebird.com/api/sms-messaging/. Test: submit with data_coding=3, assert the outbound MT is remapped to one of {0,2,8}, not passed through verbatim.
- Kaleyra: data_coding=0 GSM 03.38, =3 Latin-1, =8 expects **UTF-16 Big Endian**; data_coding=1 (ASCII) explicitly "NOT RECOMMENDED, known to cause problems" — https://messaging.kaleyra.com/support/solutions/articles/3000091796-submitting-messages-through-smpp. Test: submit UCS-2 as little-endian vs big-endian, confirm only big-endian round-trips; lint against data_coding=1.
- LINK Mobility (Implementation Guide): data_coding table 0x00 default/GSM, 0x01 IA5→GSM, 0x02/0x04 binary, 0x03 Latin-1, **0x050x07 not supported**, 0x08 UCS2; regardless of requested DCS, everything is **transcoded into GSM or Unicode only** before reaching the handset (arbitrary DCS is not preserved on the wire) — https://a.storyblok.com/f/151608/x/bb282a50a7/link-mobility-implementation-guide-smpp.pdf. Test: submit data_coding=3 (Latin-1), assert the network delivers transcoded GSM/Unicode, not raw Latin-1.
- Telia: configurable default among GSM8/GSM7/Latin-1/Latin-9; UCS-2 for non-GSM content — https://cdn.messaging.teliacompany.com/documents/developer/index.html. Test: assert UCS-2 required/accepted for chars outside the GSM 7-bit default alphabet.
- Telesign: data_coding 0/1 = GSM 03.38 default, 3 = Latin-1 (SMPP-only), 8 = Unicode/UTF-16BE; 0/1/3 "may result in ASCII encoding if the input stream can't be decoded" as GSM/Latin-1 — https://developer.telesign.com/enterprise/docs/smpp-protocol. Test: send non-GSM7-valid bytes with data_coding=0, confirm documented ASCII fallback rather than a hard failure.
- Route Mobile: data_coding=0 default GSM 03.38; =1 also GSM 03.38 but **explicitly flagged "NOT RECOMMENDED... known to cause problems"**; =3 Latin-1 "if and only if told so explicitly"; =8 Unicode, body expected **UTF-16 Big Endian** — routemobile.com PDF. Test: assert the library warns against data_coding=1 and defaults new integrations to 0; assert UCS-2 bytes are big-endian.
- Syniverse: customers submit UTF-8; the system auto-detects and, if **any** character isn't GSM-7, converts the **entire message** to UCS-2 (no partial/mixed encoding) — https://sdcsupport.syniverse.com/hc/en-us/articles/360010947674. Test: send one emoji mixed into otherwise-GSM7 text, confirm the whole message is segmented under UCS-2 limits, not GSM7's.
## 8. Addresses
- spec TON table: 0 Unknown, 1 International, 2 National, 3 Network Specific, 4 Subscriber Number, 5 Alphanumeric, 6 Abbreviated. NPI table: 0 Unknown, 1 ISDN(E163/E164), 3 Data(X.121), 4 Telex(F.69), 6 Land Mobile(E.212), 8 National, 9 Private, 10 ERMES, 14 Internet(IP), 18 WAP Client Id — spec via sysop.fr mirror. Test: alphanumeric sender should be TON=5; the spec treats address as a C-octet digit string and doesn't mandate '+' handling — test both '+' and bare-digit forms are accepted.
- Melrose Labs: destination_address <8 chars → `ESME_RINVDSTADR` — see topic 1. Test: useful edge case specifically for short-code-length destination addresses.
- Kannel: default source-addr TON=2/NPI=1 — Kannel User's Guide 1.4.5 (see topic 1).
- Sinch: alphanumeric sender TON=5/NPI=0, max 11 chars; MSISDN sender TON=1/NPI=1, max 18 chars; destination must be E.164 — https://developers.sinch.com/docs/sms/smpp/send-message. Test: submit a 12-char alphanumeric sender or non-E.164 destination, confirm rejection/truncation.
- Infobip: shortcode→TON 3/NPI 0; alphanumeric→TON 5/NPI 0; "+"-prefixed→TON 1/NPI 1; else→TON 0/NPI 1; recommends TON/NPI=1 + E.164 as most reliable; `address_range` optional unless the account requires it — https://www.infobip.com/docs/essentials/api-essentials/smpp-specification. Test: submit each TON/NPI combo against its matching address shape, confirm acceptance; submit mismatched combos, check rejection vs silent tolerance.
- Telnyx: bind requires addr_ton=1/addr_npi=1, fixed — https://support.telnyx.com/en/articles/1667062-short-message-peer-to-peer-set-up-guide. Test: bind with different values, confirm only 1/1 accepted.
- Vonage: documented alphanumeric-sender example uses **TON=5, NPI=1** — notably NPI=1, not the NPI=0 convention used by Infobip/Sinch for alphanumeric senders — https://api.support.vonage.com/hc/en-us/articles/204015853. Test: submit alphanumeric sender with NPI=0 vs NPI=1 against Vonage, confirm which the documented example actually expects (cross-vendor inconsistency worth its own regression test).
- Kaleyra: TON 1=International MSISDN, 3=National/Network Short Code, 5=Alphabetic — https://messaging.kaleyra.com/support/solutions/articles/3000091796-submitting-messages-through-smpp. Test: submit ton=5 with an alphanumeric source_addr, confirm accepted.
- Clickatell: TON/NPI "auto detected" rather than strictly validated; alphanumeric sender "not available on all networks" — downgraded/rejected per destination operator rather than at the SMPP layer — https://archive.clickatell.com/developers/api-docs/using-the-smpp-api/. Test: submit an alphanumeric sender to an operator that doesn't support it, confirm the failure surfaces as a per-operator DLR err, not an SMPP submit_sm_resp rejection.
- LINK Mobility (Implementation Guide): source addr_ton restricted to 1 (MSISDN Int'l)/2 (national short code)/5 (alphanumeric); addr_npi ignored entirely — https://a.storyblok.com/f/151608/x/bb282a50a7/link-mobility-implementation-guide-smpp.pdf. Test: submit with an unlisted TON (e.g. 3), check rejection/fallback.
- LINK Mobility (SMSC-SMPP User Guide): source TON options Alphanumeric(5)/International(1)/National(2)/Network-specific(3)/Subscriber(4)/Abbreviated(6), each NPI Unknown(0) or ISDN(1); **destination restricted to International(1) TON only** — https://www.linkmobility.com/resources/developer/SMSC-SMPP-User-Guide-1.5.pdf. Test: submit a national-format (non-E.164) destination TON, expect rejection.
- Telia: alphanumeric sender TON=5/NPI=0, max 11 chars; International TON=1/NPI=1; Short number TON=3/NPI=0 — https://cdn.messaging.teliacompany.com/documents/developer/index.html. Test: assert alphanumeric sender rejected/truncated beyond 11 chars.
- Telesign: destination requires dest_addr_ton=1/npi=1; source addresses require prior whitelisting/approval before use — https://developer.telesign.com/enterprise/docs/smpp-protocol. Test: submit with dest_addr_ton=0, confirm rejection per this vendor's stated model.
- Route Mobile: source_addr_ton Alphanumeric=5, National/Network Short Code=3, International MSISDN=1 (default); **destination TON/NPI not enforced at all** — "always interpreted as 1," destination must be international format **without leading "00"** — routemobile.com PDF. Test: submit dest_addr_ton=9 (garbage), confirm still accepted and treated as TON=1; submit a destination with leading "00", confirm rejection/mis-routing.
- Syniverse: alphanumeric sender requires per-operator registration/approval (no TON/NPI published); short codes 56 digits — https://sdcsupport.syniverse.com/hc/en-us/articles/360019714593 , https://sdcsupport.syniverse.com/hc/en-us/articles/229476208. Test: validate short-code source addresses are 56 digits.
## 9. Time fields
- Absolute format `YYMMDDhhmmsstnnp`: YY year, MM month, DD day, hh hour, mm minute, ss second, t tenths of a second, nn UTC time-differential in quarter-hour units, p = `+`/`-` — Kannel devel mailing list quoting the spec, https://www.kannel.org/pipermail/devel/2009-April/002384.html. Test: build an absolute validity_period string per this layout and confirm it parses.
- Relative format: replace `p` with `R`; example `000000000500000R` = "5 minutes validity from message receipt" — same source. Test: submit a relative validity_period, confirm the SMSC computes expiry from its own receipt time, not the client's clock.
- Kannel real-world quirk: "will send validity in utc therefore 00+ is hardcoded" — Kannel never emits a non-zero UTC offset even though the format supports one — same Kannel devel-list source. Test: assert a test against Kannel doesn't expect timezone-aware absolute validity_period.
- Sinch: schedule_delivery_time supports both absolute and relative; max 168 hours (1 week) ahead — https://developers.sinch.com/docs/sms/other/sms-other-cloud-smpp. Test: schedule >168h out, confirm rejection.
- Infobip: relative format worked example `"070605040302100R"` = 7y 6mo 5d 4h 3m 2s 1 tenth-sec, relative — https://www.infobip.com/docs/essentials/api-essentials/smpp-specification. Test: submit that exact string in schedule_delivery_time/validity_period, confirm Infobip parses it as relative.
- Telesign: validity_period is a 17-octet C-octet string; absolute form uses `+`/`-` UTC offset in quarter-hours as the last char; relative form uses `R` there instead with unused fields zeroed ("relative to current MC time") — https://developer.telesign.com/enterprise/docs/smpp-protocol. Test: build both absolute (+/-) and relative (R) strings per this layout, assert both parse.
- Route Mobile / Syniverse: only document the *error* codes (`ESME_RINVEXPIRY` 0x62 for Route Mobile; 1045 "Invalid Validity Period"/1040 "Invalid scheduled delivery time" for Syniverse) — no accepted-format detail published — routemobile.com PDF; https://sdcsupport.syniverse.com/hc/en-us/articles/360038257473. Test: assert malformed validity_period strings trigger these vendor-specific error codes.
- Clickatell: documents validity_period as a mechanism to stagger bulk-send delivery when the gateway has queueing delays — a throttling/scheduling tool, not just a TTL — https://archive.clickatell.com/developers/api-docs/validity-period-advanced-message-send/. Test: set a short validity_period on a message stuck behind a large queued batch, confirm it expires (stat:EXPIRED) rather than delivering late.
- LINK Mobility (SMSC-SMPP User Guide): recommends validity_period ≥15 minutes; `schedule_delivery_time` is **unsupported** on submit_sm for this profile — https://www.linkmobility.com/resources/developer/SMSC-SMPP-User-Guide-1.5.pdf. Test: submit with schedule_delivery_time set, expect rejection/ignoring on this profile.
- Absolute-vs-relative rejection rules for the remaining vendors (Bird, Clickatell beyond above, Kaleyra, tyntec, CM.com, Bandwidth): not publicly documented — unverified — folklore.
## 10. Error handling
- spec `generic_nack` triggers: invalid `command_length` ("assume the data is corrupt... a generic_nack PDU must be returned") and unknown `command_id` ("a generic_nack PDU must also be returned") — spec via smpp.org PDF fetch. Test: send a PDU with a bogus command_id, assert generic_nack, not a silent drop or close.
- spec session states OPEN/BOUND_TX/BOUND_RX/BOUND_TRX/CLOSED gate which PDUs are valid when; out-of-state PDUs aren't given an explicit error rule in the spec text itself, but `ESME_RINVBNDSTS` (0x00000004, "Incorrect BIND Status for given command") is the closest documented code — spec via smpp.org PDF fetch. Test: send deliver_sm on an unbound connection, expect ESME_RINVBNDSTS or a connection close, not silent processing.
- spec `outbind`: lets the SMSC signal an ESME to originate a bind_receiver, typically because the SMSC has outstanding messages for it (§2.2.1) — spec via smpp.org PDF fetch. Test: simulate an SMSC-initiated outbind, assert the ESME responds by binding as receiver.
- spec `alert_notification`: SMSC-issued in BOUND_RX/BOUND_TRX, and is the **one PDU with no response** — spec via smpp.org PDF fetch. Test: after receiving alert_notification, assert the ESME sends back no response PDU (unlike every other request).
- spec `data_sm`: uniquely supports Transaction Message Mode; submit_sm does **not** support transaction mode (§2.10.2/2.10.3/§4.4) — spec via smpp.org PDF fetch. Test: assert the library rejects/flags an attempt to request transaction mode via submit_sm's esm_class instead of data_sm.
- Whether submit_sm_resp with non-zero status still carries a body: not confirmed either way in the reachable spec text — unverified — folklore.
- Vonage: `command_status=0x00000005` (ESME_RALYBND) beyond the 2-connection default; `0x0d`/13 decimal (ESME_RBINDFAIL) for wrong host/zone/creds; on deliver_sm, only status codes the spec's table marks "temporary failure" trigger a Vonage retry — everything else is final, no retry — https://api.support.vonage.com/hc/en-us/articles/204015713 , https://api.support.vonage.com/hc/en-us/articles/204015783 , https://api.support.vonage.com/hc/en-us/articles/204015773. Test: return each documented temporary vs non-temporary status in deliver_sm_resp, confirm retry only for the temporary set.
- Bird: DLR text `err:` and `network_error_code` TLV both index into Bird's own proprietary 0131 error table, distinct from the underlying carrier's raw code — https://developers.messagebird.com/api/sms-messaging/. Test: table-driven test asserting err code → Bird meaning, kept separate from any carrier-code test.
- Kaleyra: successful submit_sm_resp returns error code 0 + non-null message reference; failure returns "a Kaleyra vendor specific error code" — not necessarily a standard SMPP ESME_* value — https://messaging.kaleyra.com/support/solutions/articles/3000091796-submitting-messages-through-smpp. Test: assert error handling tolerates/logs unrecognized vendor codes rather than assuming the standard ESME_* enum is exhaustive.
- LINK Mobility (Implementation Guide): generic_nack supported bidirectionally; client must handle SMSC-initiated unbind and respond before disconnecting, then wait ≥30s before reconnecting; outbind not mentioned (implicitly unsupported); ESME_RSYSERR(8)/ESME_RTHROTTLED(88) responses **lack the vendor extension TLV** even when the extension feature is enabled — https://a.storyblok.com/f/151608/x/bb282a50a7/link-mobility-implementation-guide-smpp.pdf. Test: assert reconnect within 30s of an unbind is refused; assert RSYSERR/RTHROTTLED responses lack the vendor extension even with it turned on.
- Telenor: OUTBIND/SUBMIT_MULTI/ALERT_NOTIFICATION unsupported (see topic 1); GENERIC_NACK is in the supported-operations list — https://developer.telenor.no/images/97-322719.pdf.
- Telia: ESME must send unbind before closing the TCP connection; SMSC can also unilaterally unbind for maintenance — https://cdn.messaging.teliacompany.com/documents/developer/index.html. Test: close TCP without unbind vs proper unbind, compare vendor-side session-cleanup expectations.
- Route Mobile: rich vendor error-code extension beyond standard SMPP: `ESME_CREDIT_ERROR` 0x401, `ESME_SPAM_MESSAGE` 0x404, `ESME_RINVSMLEN` 0x405 (>160 char text message), `ESME_RINVUDHLEN` 0x406, `ESME_RINSMSEMPTY` 0x407, `ESME_RINDSTDND` 0x408 (destination in DND), 0x4090x412 (invalid source/template, long-message-template error, duplicate submission, destination/source barred) — routemobile.com PDF. Test: assert each vendor-extension code round-trips through generic error parsing without falling back to "unknown error."
- Bandwidth: error ranges by origin — Bandwidth client 301-476, Bandwidth server 100-231, carrier client 700-795, carrier server 600-650, carrier ambiguous 902/999; 306 = registered_delivery out of range; 304-305 = invalid UDH; 720 = destination not in numbering plan — https://www.bandwidth.com/support/en/articles/12823160. Test: assert a registered_delivery value outside {0,1} on submission is flagged in the same class as Bandwidth's 306.
- Syniverse: 1032 invalid registered_delivery, 1033 system error, 1034 invalid service_type, 1035 invalid command length, 1036 wrong state for command, 1037 invalid message id, 1038 outgoing queue full, 1039 throttling, 1041 submit failed, 1042/1043 invalid source/dest TON, 1048 invalid data_coding — https://sdcsupport.syniverse.com/hc/en-us/articles/360038257473. Test: checklist that the library independently validates/reports each of command_length, session state, message id, service_type, registered_delivery, TON, data_coding.
## 11. Framing / transport
- spec §2.4: the underlying network connection is assumed to provide "reliable data transfer... including packet encoding, windowing, flow control and error handling," and segmentation/reassembly of PDUs across packets happens **below** the SMPP layer — spec via smpp.org PDF fetch. Test: feed a test harness PDUs deliberately split across multiple TCP writes and multiple PDUs coalesced into one read, assert the parser reassembles by `command_length` rather than assuming one PDU per read.
- spec §3.1 C-octet String: "a series of ASCII characters terminated with the NULL character... empty strings encode as a single NULL octet (0x00)" — spec via smpp.org PDF fetch. Test: a field that's non-null-terminated or contains embedded NULs should be rejected/flagged by a strict parser.
- spec PDU header: four 4-octet big-endian fields (command_length, command_id, command_status, sequence_number); command_id 0x000000000x000001FF requests / 0x800000000x800001FF responses; sequence_number 0x000000010x7FFFFFFF — spec via smpp.org PDF fetch. Test: fuzz a header with command_length smaller than actual PDU bytes present, confirm the generic_nack "invalid command_length" path (topic 10) fires.
- `sm_length=0` + `message_payload` combination is the standard way to carry >254-octet user data — spec via sysop.fr mirror (also topic 5/6). Test: assert a parser accepts short_message len 0 alongside a populated message_payload TLV.
- Telia: requires SNI on all SMPP TLS connections, lists 14 supported TLS cipher suites, min TLS1.2, port 3550 — https://cdn.messaging.teliacompany.com/documents/developer/index.html. Test: attempt a TLS handshake without SNI, expect rejection.
- LINK Mobility: TLS 1.2/1.3 required, ports 3600 (plain)/3601 (TLS), TLS1.0/1.1 discontinued since 2020-11-15 — https://www.linkmobility.com/resources/developer/SMSC-SMPP-User-Guide-1.5.pdf. Test: attempt TLS1.1, expect rejection.
- Kaleyra: `message_payload` explicitly capped at 64K octets, gated behind interface_version=0x34 — https://messaging.kaleyra.com/support/solutions/articles/3000091796-submitting-messages-through-smpp. Test: send message_payload right at and just over 64K, confirm boundary behaviour.
- Bird: plaintext SMPP on TCP 2775, TLS on TCP 2776 — https://docs.bird.com/connectivity-platform/other-integrations/how-can-i-use-the-short-message-peer-to-peer-smpp-network-protocol. Test: configure a mock SMSC on 2775/2776 to match a Bird-shaped client config.
- No vendor among the ~20 researched publicly documents multi-PDU-per-TCP-segment behaviour, `command_length`-lies-about-actual-size handling, or enquire_link burst tolerance by name — this whole topic is essentially spec-only + implementation-defined across all vendors; treat the spec §2.4/§3.1 items above as the ground truth and the rest as **unverified — folklore**.
---
## Top 10 quirks most likely to break a fresh implementation (ranked)
1. **Message ID base/field mismatch between submit_sm_resp and DLR `id:`.** Kannel `msg-id-type`, Jasmin `dlr_msgid`, and Vonage's default hex-vs-decimal split all independently produce a submit_sm_resp id that doesn't string-match the DLR's `id:` field unless converted. A correlation layer that assumes "same string" will silently lose DLR-to-message mapping. (Kannel User's Guide; Jasmin docs; Vonage API Support.)
2. **`esm_class` DLR bit confusion: 0x04 (final receipt) vs 0x20 (intermediate) vs 0x00 (plain MO).** A naive "esm_class != 0 means DLR" check misclassifies intermediate notifications and even non-DLR SME acks (0x08/0x10) as final delivery receipts. (SMPP 3.4 spec.)
3. **Long-message mechanism varies by vendor and none is universal.** UDH-only (LINK Mobility, Vonage, Route Mobile via esm_class 0x43), `sar_*` TLVs (Clickatell, spec-native), and `message_payload` (Telia, Kaleyra — gated behind interface_version 0x34 — Route Mobile) are all real, mutually exclusive requirements. A library hardcoded to one mechanism won't interop with vendors requiring another. (Multiple vendor docs above.)
4. **UCS-2 byte order.** Kaleyra and Route Mobile explicitly require UTF-16 **big-endian**; a JS/TS implementation defaulting to native (often little-endian) UTF-16 encoding silently corrupts every non-GSM message. (Kaleyra, Route Mobile docs.)
5. **Throttling semantics: `ESME_RTHROTTLED` (0x58) vs `ESME_RMSGQFUL` (0x14) need different backoff, and window/TPS limits vary 10x across vendors** (2/s free SMPPSim vs 100+/s Infobip) with some vendors closing the socket instead of responding. A fixed retry policy will misbehave against most real SMSCs. (smpp.org error codes; LINK Mobility, Sinch, Vonage, Telia docs.)
6. **DLR text-body field variability is much wider than the "standard" format suggests.** `sub`/`dlvrd` are hardcoded "000" and `text` always blank at LINK Mobility; `stat` vocabularies extend beyond the core 7 values (Vonage adds FAILED/DELETED, Infobip allows ENROUTE as a stat, Telesign adds SKIPPED). A strict fixed-format parser breaks on the first non-conforming vendor. (LINK Mobility, Vonage, Infobip, Telesign, smpp.org.)
7. **Concatenated SMS must stay on one session/server, and MO concat params can be absent.** Vonage: all parts must go through the same SMPP server or the handset never reassembles, within a 20-minute submission window; MO concat TLVs are missing entirely for some US carriers, requiring a same-sender+timestamp heuristic. tyntec: without UDH, segment order isn't guaranteed at all. (Vonage, tyntec docs.)
8. **PDU framing over TCP is not 1:1 with `read()`/`write()` calls.** The spec explicitly pushes segmentation/reassembly below the SMPP layer, but no vendor publishes concrete "multiple PDUs per segment" or "PDU split across segments" test guidance — a stream parser that assumes one PDU per socket read will break under real-world TCP behavior (Nagle, MTU, coalescing) the first time it hits production traffic. (SMPP 3.4 spec §2.4/§3.1.)
9. **Interface-version-gated features fail silently, not loudly.** Binding at 0x33 instead of 0x34 silently disables `message_payload`/certain TLVs (Kaleyra, Route Mobile) or changes message_id format (Sinch legacy) rather than rejecting the bind — a library that doesn't track negotiated version per-session will send PDUs the far end simply ignores or misinterprets. (Kaleyra, Route Mobile, Sinch docs.)
10. **TON/NPI conventions for alphanumeric sender disagree across vendors.** NPI=0 (Infobip, Sinch, Telia) vs NPI=1 (Vonage's own documented example) for the same TON=5 alphanumeric-sender case — a library validating "correct" TON/NPI combinations against one vendor's convention will reject valid submissions to another. (Infobip, Sinch, Telia, Vonage docs.)
---
### Coverage notes
- Kannel's `dlr-mask` (named in the brief) was searched for directly in the Kannel 1.4.5 User's Guide `smsc smpp` section and **does not appear there** — confirmed absent via targeted fetch, not merely unfound. It may exist under a different name/group in newer Kannel, or the brief may have conflated it with another gateway's option. Marked **unverified — folklore** rather than guessed.
- Twilio confirmed to offer SMPP, but enterprise-gated only (no public/self-service target) — see topic 1.
- Tele2 and Hi3G/Tre (3, Sweden) have no public SMPP interconnect technical spec discoverable in English or Swedish — explicitly no findings, not omitted by oversight.
- Esendex's public docs describe only capability-level facts (multi-bind, separate in/out binds); no protocol-level parameters (TON/NPI, data_coding, DLR format) could be recovered — its SMPP developer sub-pages are JS-rendered and returned empty content to fetching.
- Syniverse's raw SMPP bind-level technical manual is not publicly reachable; everything sourced instead comes from its SCG platform docs, which wrap SMPP in a REST layer but still publish SMPP-level status-code tables.
- Telenor's two public PDFs are business/operational overviews confirming supported-vs-unsupported PDU types but not publishing message_id format, DLR text format, or TON/NPI tables.
+166
View File
@@ -0,0 +1,166 @@
# SMPP SMSC-side test targets for `@larvit/smpp`
Research date 2026-09-05. All claims cite the URL they came from; anything not confirmed by a primary source is marked **unverified**.
## Quick comparison
| Candidate | Language | License | Maintained? | Docker tag (pinned) | SMPP port | SMPP versions |
|---|---|---|---|---|---|---|
| SMPPSim (Selenium Software) | Java | GPLv2 [sourceforge](https://sourceforge.net/projects/smppsim/) | Stalled — homepage `seleniumsoftware.com` returns Cloudflare 522 as of this research; last SourceForge file 2015-05-22; community fork last touched 2024-01-09 | none published — build your own | 2775 | 3.3/3.4 implied, not confirmed |
| Jasmin SMS Gateway | Python/Twisted | ISC-style (see repo) [github](https://github.com/jookies/jasmin) | Source active (last push 2026-04-26), last **tagged release** 0.11.0 (2023-11-10) | `jookies/jasmin:0.11.0` [dockerhub](https://hub.docker.com/r/jookies/jasmin/tags) | 2775 | 3.4 |
| Kannel + opensmppbox | C | BSD-style (Kannel) | Kannel core: last stable 1.4.5, 2018-06-19 [kannel.org](https://www.kannel.org/); opensmppbox (pruiz fork): last commit 2014-04-22 [github](https://github.com/pruiz/kannel-opensmppbox) | none — build from source | 2345 (configurable) | 3.3/3.4/5.0 |
| Kannel `fakesmsc` | C | BSD-style | same as Kannel | n/a | configurable (e.g. 10000) | **none — not SMPP at all** |
| Melrose Labs SMSC Simulator (hosted) | closed-source | proprietary | Actively run (commercial service) | n/a — hosted | 2775 (8775 TLS) | 3.3/3.4/5 |
| Melrose Labs SMSC Simulator (OSS, old code) | C++ | MIT [github](https://github.com/melroselabs/smpp-smsc-simulator) | Dormant, last push 2023-07-15, explicitly "old version" | none published | 2775 | subset of 3.4 |
| ukarim/smscsim | Go | MIT [github](https://github.com/ukarim/smscsim) | Source active (last push 2025-12-12); Docker tags stop at 0.2.0 | `ukarim/smscsim:0.2.0` (no newer semver tag) [dockerhub](https://hub.docker.com/r/ukarim/smscsim/tags) | 2775 (env `SMSC_PORT`) | subset of 3.4 |
| mdouchement/smsc3 | Go | MIT [github](https://github.com/mdouchement/smsc3) | Last push 2024-05-09, small project | none published — Dockerfile in repo | 20001 | 3.4 |
| K2InformaticsGmbH/smsc_simulator | Erlang | Apache-2.0 [github](https://github.com/K2InformaticsGmbH/smsc_simulator) | Abandoned, last commit 2018-08-31 | none | configurable | SMPP + UCP |
| MavoCz/smscsim (cloudhopper-based) | Java | unverified | unverified last-commit date | none | configurable (CLI `-p`) | 3.3/3.4/5.0 (via cloudhopper) |
| fizzed/cloudhopper-smpp | Java | Apache-2.0 [github](https://github.com/fizzed/cloudhopper-smpp) | Last push 2020-10-12 | none — library + demo, not a packaged sim | demo-configurable | 3.3/3.4/most of 5.0 |
| jsmpp `SMPPServerSimulator` example | Java | Apache-2.0 [github](https://github.com/opentelecoms-org/jsmpp) | jsmpp itself very active (last push 2026-06-17); the example is a demo class, not a packaged tool | none — sample code | example-configurable | 3.4/3.3 |
| node-smpp | JS | MIT [github](https://github.com/farhadi/node-smpp) | Last push 2023-12-28 | none — library, build your own server | n/a | 5.0 (back-compat with 3.4) |
| MikeSafonov/smpp-server-mock | Java (JUnit ext.) | MIT [github](https://github.com/MikeSafonov/smpp-server-mock) | Last push 2023-03-10 | none — embedded-in-test only, not standalone | n/a | unverified |
| Restcomm/TeleStax SMSC Gateway simulator | Java/JSLEE | unverified | project largely dormant post-TeleStax | none published | 2776 (sample) | unverified, full carrier-grade stack |
| OsmoMSC SMPP interface | C | AGPL (Osmocom) | Actively maintained as part of Osmocom NITB/MSC, but it's a *client* interface for external ESMEs bound to a live MSC core, not a standalone SMSC simulator you spin up in Docker | none | configurable | unverified |
| smscsim.smpp.org (public) | closed | n/a | unverified, listed on smpp.org | hosted | 2775 | unverified, 2 SMS/s cap |
Ozeki NG SMS Gateway and NowSMS both ship free/trial tiers with an SMPP server mode, but are Windows-first, closed-source products; skipped per task scope beyond this line.
---
## 1. SMPPSim (Selenium Software)
1. **Source**: originally `seleniumsoftware.com/downloads.html`; that domain currently returns an HTTP 522 (Cloudflare "origin unreachable") when fetched directly — confirmed via both WebFetch and a raw `curl` from this research session, so the vendor's own site is effectively down right now. SourceForge mirror: [sourceforge.net/projects/smppsim](https://sourceforge.net/projects/smppsim/) — license **GPLv2**, "Currently hosted by Selenium Software", last file dated **2015-05-22**. A community fork with docs/props preserved: [komuw/smpp_server_docker](https://github.com/komuw/smpp_server_docker) (last push 2019-06-04), user-guide mirrored at [user-guide.htm](https://github.com/komuw/smpp_server_docker/blob/master/SMPPSim/docs/user-guide.htm). Not actively maintained by any single fork; version history in the mirrored guide references up to v2.6.6.
2. **Docker**: no maintainer-published image with a pinned version tag exists. Two build-yourself Dockerfiles found:
- [kwahome/smpp-sim-docker](https://github.com/kwahome/smpp-sim-docker) (repo last pushed 2024-01-09) — its own `Dockerfile` builds `openjdk:7-jre-alpine` + bundled `SMPPSim/`, but its `docker-compose.yml` instead pulls a pre-built `blackorder/smppsim:latest`**only a `latest` tag exists** on that Docker Hub repo (pushed ~2 years ago, single tag) [dockerhub](https://hub.docker.com/r/blackorder/smppsim/tags). Since no full-patch-version tag is published anywhere, to comply with "never floating tags" you must build the image yourself from the Dockerfile and apply your own version tag.
- `bitsensedev/smpp-sim` on Docker Hub: single `latest` tag, pushed **9+ years ago** — effectively abandoned, do not use.
- Ports (from the Dockerfile env defaults): SMPP **2775**, HTTP admin **8884** (some older docs say port 88 — verify against the image you build).
- Default credentials (env `SYSTEM_IDS` / `PASSWORDS`): `smppclient1,smppclient2,smppclient3` / `password,password,password` [Dockerfile](https://raw.githubusercontent.com/kwahome/smpp-sim-docker/master/Dockerfile).
3. **SMPP versions**: not explicitly stated in any surviving doc; behavior suggests 3.3/3.4-era PDU set. Whether it validates `interface_version` at bind — **unverified**, no mention in the mirrored user guide.
4. **Delivery receipts**: yes, when `registered_delivery_flag` is set on `submit_sm`. Controlled by percentage knobs: `PERCENTAGE_DELIVERED`, `PERCENTAGE_UNDELIVERABLE`, `PERCENTAGE_ACCEPTED`, `PERCENTAGE_REJECTED`, plus `PERCENTAGE_THAT_TRANSITION` (chance of an intermediate state before final), `DELAY_DELIVERY_RECEIPTS_BY` (delayed DLR, ms), `MAX_TIME_ENROUTE`. TLV vs text-body format not documented; message-id format documented only as "randomised … with configurable prefix" — hex/decimal notation unverified.
5. **Long messages**: UDH/`sar_*`/`message_payload` handling not documented in any surviving guide fragment — unverified, treat as untested.
6. **Encodings**: only one hard fact recovered — binary MO messages (hex-prefixed with `0x` in the inject form) get `data_coding` auto-set to 4. GSM7 packed-vs-unpacked, Latin-1, UCS-2 behavior — unverified.
7. **MO injection**: three documented methods — (a) web form `/inject_mo.htm`, (b) "loopback" mode that turns a `submit_sm` back into a `deliver_sm`, (c) MO service reading a `deliver_messages.csv` file. Also supports SMSC-initiated **outbind** (`OUTBIND_ENABLED=true` + `OUTBIND_ESME_*` env vars) to test your library's outbind handling.
8. **Fault injection**: queue-full simulation via `INBOUND_QUEUE_MAX_SIZE`/`OUTBOUND_QUEUE_MAX_SIZE` and `DELAYED_INBOUND_QUEUE_MAX_ATTEMPTS`; on `ESME_RMSGQFUL` the mirrored guide says SMPPSim "will attempt to deliver the MO message again, after a delay" (v2.6.0 note). `MO_DELIVERY_MESSAGES_PER_MINUTE`/`DELIVERY_MESSAGES_PER_MINUTE` throttles delivery rate. Explicit `ESME_RTHROTTLED` triggering — unverified.
9. **TLS**: not mentioned anywhere in the surviving docs — treat as unsupported.
10. **Minimal run** (build-yourself, since no pinned image exists):
```
git clone https://github.com/kwahome/smpp-sim-docker
cd smpp-sim-docker
docker build -t local/smppsim:<your-tag> .
docker run -p 2775:2775 -p 8884:8884 local/smppsim:<your-tag>
```
Bind with `system_id=smppclient1`, `password=password`, port 2775.
11. **Quirks**: primary vendor site down (522) at research time; ecosystem is a scatter of unofficial Docker forks with inconsistent port numbers (88 vs 8884) between older and newer docs — verify against whichever build you actually use.
## 2. Jasmin SMS Gateway
1. [jookies/jasmin](https://github.com/jookies/jasmin), license per repo (ISC-style, see `LICENSE`), Python/Twisted. Repo actively committed (last push 2026-04-26) but last **tagged release is 0.11.0, 2023-11-10** [releases](https://github.com/jookies/jasmin/releases). Docs: [docs.jasminsms.com](https://docs.jasminsms.com/).
2. **Docker**: `jookies/jasmin:0.11.0` (full patch tag exists and matches the GitHub release) [dockerhub tags](https://hub.docker.com/r/jookies/jasmin/tags). Needs RabbitMQ + Redis as separate containers — not standalone.
```
docker run -d -p 1401:1401 -p 2775:2775 -p 8990:8990 jookies/jasmin:0.11.0
```
Ports: **2775** SMPP server, 1401 HTTP API/panel, 8990 `jcli` telnet console [docker/README.md](https://github.com/jookies/jasmin/blob/master/docker/README.md). No default smpp-server credentials are pre-provisioned — you create a `system_id`/`password` via `jcli` (`smppccm`/`user -a` commands) after startup.
3. **SMPP version**: 3.4 [SMPP Server API docs](https://docs.jasminsms.com/en/latest/apis/smpp-server/index.html). `interface_version` enforcement at bind — not documented; **unverified**.
4. **Delivery receipts**: yes. Jasmin's DLR pipeline (`DLRLookup`/`DLRThrower`) tracks the message in Redis and, when the original submission came in over `smpps`, pushes the receipt back as a `deliver_sm`/`data_sm` on the same bind [messaging flows](https://docs.jasminsms.com/en/latest/messaging/index.html). Whether it's TLV (`receipted_message_id`/`message_state`) or text-body — not confirmed from docs; a `dlr_msgid` connector setting (0/1/2) exists specifically to reconcile hex-vs-decimal message-id mismatches between `submit_sm_resp` and `deliver_sm` — but that knob lives on **outbound SMPP client connectors** (Jasmin acting as ESME to an upstream real SMSC), not on the `smpps` server your library binds to [Google Groups thread](https://groups.google.com/g/jasmin-sms-gateway/c/KRqUA6e569w). When Jasmin itself is the SMSC (your library binds to its `smpps`), the message id it generates is its own internal **UUID**-based id [Google Groups](https://groups.google.com/g/jasmin-sms-gateway/c/KRqUA6e569w) and should therefore be self-consistent between `submit_sm_resp` and the DLR — good for testing your library's "normalise across submit_sm_resp vs receipt" logic in the *opposite* direction (assert it does NOT need to normalise when ids already match).
5. **Long messages**: two segmentation strategies documented for MT — **SAR** (`sar_msg_ref_num`/`sar_total_segments`/`sar_segment_seqnum`, "preferred by most SMSCs") and **UDH** (prepended header + `UDHI_INDICATOR_SET` in `esm_class`, "for older system compatibility") [DeepWiki summary of Jasmin SMPP protocol support](https://deepwiki.com/jookies/jasmin/3.3-smpp-protocol-support) (secondary source — treat as indicative, not primary).
6. **Encodings**: capacity figures imply **packed** GSM7 (160 chars / 153 segmented = the standard packed math), 8-bit binary 140/134 bytes, UCS-2 70/67 chars [same DeepWiki page]. Not a primary source — verify empirically before relying on it.
7. **MO injection**: bind your library as receiver/transceiver on `smpps`; anything Jasmin routes to that connector arrives as `deliver_sm`. Separately, Jasmin's HTTP API/interceptor stack can push MO content to an HTTP webhook (`deliverSmHttpThrower`) instead — not needed for SMSC-side ESME testing, only relevant if you want Jasmin to also fan MO out to HTTP [interception docs](https://docs.jasminsms.com/en/latest/interception/index.html).
8. **Fault injection**: `submit_throughput` exists as a per-connector throttling parameter but its exact unit/effect and how to disable it are not resolved even in the project's own issue tracker — [issue #913](https://github.com/jookies/jasmin/issues/913) is open/stale with no documented answer. No documented way to force `ESME_RTHROTTLED`/`ESME_RMSGQFUL`, refuse binds, or drop the TCP connection on demand — unverified/likely absent.
9. **TLS**: the `[smpp-server]` config section in the shipped `jasmin.cfg` has **no TLS/SSL directives at all** [jasmin.cfg](https://github.com/jookies/jasmin/blob/master/misc/config/jasmin.cfg) — the SMPP *server* role does not support TLS. (An outbound SMPP *client* connector does have `useSSL`/`SSLCertificateFile` options, irrelevant here.)
10. **Minimal config** — `[smpp-server]` section, defaults: port 2775, `sessionInitTimerSecs=30`, `enquireLinkTimerSecs=30`, `inactivityTimerSecs=300`, `responseTimerSecs=60`, `pduReadTimerSecs=10`, `dlr_expiry=86400` [jasmin.cfg](https://github.com/jookies/jasmin/blob/master/misc/config/jasmin.cfg). After boot, create a user via `jcli`:
```
jcli -h 127.0.0.1 -p 8990 # telnet console
> user -a
> uid myesme
> gid mygroup
> username myesme
> password mypassword
> ok
```
11. **Quirks**: no packaged Docker Compose bundling RabbitMQ+Redis is officially shipped — you must wire those yourself. No release/Docker tag newer than late 2023 despite ongoing source commits — verify current `master` behavior differs from 0.11.0 before trusting docs literally.
## 3. Kannel + opensmppbox (and why `fakesmsc` doesn't help)
1. **Kannel** core: [kannel.org](https://www.kannel.org/), C, BSD-style license. Confirmed via the vendor's own homepage: last **stable release 1.4.5, 2018-06-19**; nothing newer since [kannel.org news list, fetched directly]. Effectively unmaintained upstream.
**opensmppbox** (the actual SMPP-server add-on): canonical doc is the [OpenSMPPBox User's Guide](https://www.kannel.org/download/1.4.4/gateway-1.4.4/addons/opensmppbox/doc/userguide.xml) ("developed by Chimit Ltd, maintained by the Kannel Group"). The most complete GitHub mirror, [pruiz/kannel-opensmppbox](https://github.com/pruiz/kannel-opensmppbox), has its last commit **2014-04-22** — 11+ years stale.
2. **Docker**: no official image. Build from source against a Kannel `bearerbox` build; no maintained Dockerfile found. Default port **2345** (`opensmppbox-port`); credentials live in a flat file set via `smpp-logins` (username/password/system-type/IP-restriction per line).
3. **SMPP versions**: "compliance to SMPP v3.3, SMPP v3.4 & SMPPv5.0" per the user guide. `interface_version` bind-time validation — not documented, unverified.
4. **Delivery receipts**: opensmppbox stores/forwards DLRs from Kannel's `bearerbox` (multiple backends: internal, MySQL, PostgreSQL, Oracle, SQLite3, MS-SQL) and "rewrites [them] to appear that they originated from OpenSMPPBox" back to the bound ESME. TLV vs text, exact `esm_class`, intermediate/failure/delayed-DLR simulation — not documented in the guide; unverified.
5. **Long messages**: supports `message_payload` TLV as an alternative to UDH concatenation when explicitly enabled; UDH re-splitting behavior on the MO side — unverified.
6. **Encodings**: guide only states it can "define the data coding type of the short message" — packed vs unpacked GSM7 behavior unverified.
7. **MO injection**: MO routing targets a specific bound ESME "based on shortcode, SMSC id, or randomly if unconfigured" — i.e. inbound traffic through Kannel's normal SMSC drivers gets forwarded to whichever ESME opensmppbox picks; no direct CLI/HTTP "inject one MO now" tool documented for opensmppbox itself (Kannel's own `fakesmsc`, see below, cannot fill this gap because it doesn't speak SMPP).
8. **Fault injection / TLS**: not documented for opensmppbox in the guide; unverified — treat as unsupported until proven otherwise.
9. **`fakesmsc` clarification (important)**: Kannel ships a **separate** testing tool `test/fakesmsc` that connects to `bearerbox`'s core `smsc = fake` group. Per Kannel's own 1.4.5 user guide (fetched directly from kannel.org): *"Fake SMSC is a simple protocol to test out Kannel. It is not a real SMS center, and cannot be used to send or receive SMS messages from real phones. So, it is ONLY used for testing purposes."* Its wire format is a trivial line-based text protocol (`sender receiver type message...`), confirmed from the `fakesmsc.c` usage text — **it does not speak SMPP at all**. It exists only to test Kannel's own HTTP `sendsms` interface end-to-end without a real carrier link. It is **not usable** to interoperability-test an external SMPP client library, despite superficially sounding like an SMSC simulator. Use opensmppbox for that instead.
10. **Minimal opensmppbox config** (from the [user guide](https://www.kannel.org/download/1.4.4/gateway-1.4.4/addons/opensmppbox/doc/userguide.xml)):
```
group = smpp-logins
smpp-logins = /etc/opensmppbox/smpp-logins.txt
opensmppbox-port = 2345
```
and in `smpp-logins.txt`: `myuser mypass VMA 0.0.0.0/0`
11. **Quirks**: Kannel's core `smpp` SMSC driver (Kannel acting as *client* connecting outbound to a real SMSC) is well documented and supports `interface-version` (hex string, default `"34"`), `msg-id-type` (bit-flags for hex/decimal `submit_sm_resp` vs `deliver_sm`), `max-pending-submits` (window, default 10), `enquire-link-interval` (default 30s), `use-ssl`/`ssl-client-cert` — but that's the *wrong direction* for this task (it's Kannel-as-ESME, not Kannel-as-SMSC). Given both Kannel and opensmppbox have had no significant commits in a decade, this path is the highest-effort, lowest-payoff of the "must include" list.
## 4. Melrose Labs SMSC Simulator
Two distinct things share the name:
- **Hosted public/commercial service** at `smscsim.melroselabs.com:2775` — closed-source, run by Melrose Labs.
- **Old open-source code** at [melroselabs/smpp-smsc-simulator](https://github.com/melroselabs/smpp-smsc-simulator), MIT license, C++11, single-file (`smscsimulator.cpp`) — repo explicitly says it "represents old version of existing SMSC Simulator service available online … newer version of code not published." Last push 2023-07-15.
1. Homepages: [melroselabs.com/services/smsc-simulator](https://melroselabs.com/services/smsc-simulator/), technical details at [smsc-simulator-technical-details](https://melroselabs.com/services/smsc-simulator/smsc-simulator-technical-details/), dedicated-instance doc at [ssg docs](https://ssgdocs.melroselabs.com/docs/smsc-simulator) / [scrollhelp](https://melroselabs.scrollhelp.site/dss/dedicated-smsc-simulator).
2. **Docker**: OSS repo has a `docker-compose.yml`; no image tag confirmed (repo has only source + compose, no published registry tag found). Default port 2775 in both the OSS code and the hosted service.
3. **SMPP versions**: "SMPP v3.3, v3.4 and v5" for the hosted service [technical details page]. `submit_sm_resp` message-id length differs by version: **8 characters for v3.3, 64 characters for v3.4 and v5** [same page] — hex/decimal notation not stated.
4. **Delivery receipts**: hosted shared service — text `short_message` receipt *plus* TLVs `receipted_message_id` and `message_state` (value 2 = DELIVERED) [technical details page]; shared/free tier is "delivered" status only, <1s after `submit_sm_resp`. **Dedicated** instances are configurable via a `dlr.conf`: DELIVERED(2)/EXPIRED(3)/DELETED(4)/UNDELIVERABLE(5)/ACCEPTED(6)/UNKNOWN(7)/REJECTED(8) with percentages, plus `--dlrlatency` (default 3s) for delayed DLRs, and a per-ESME `shouldReturnUndeliveredReceipts` override [dedicated simulator docs](https://melroselabs.scrollhelp.site/dss/dedicated-smsc-simulator).
5. **Long messages**: `message_payload` TLV (0x0424) supported on `submit_sm`/`deliver_sm`/`data_sm` [technical details page]. UDH / `sar_*` handling not documented — unverified.
6. **Encodings**: not documented for either tier — unverified; test empirically.
7. **MO injection**: on the shared simulator, MO is triggered by encoding **the bound system_id's digits into the destination address** (prepend/append ≥2 digits, min 8 chars total) [technical details page] — an unusual, non-obvious convention, worth automating carefully. Dedicated instances configure acceptable MSISDNs per-ESME via `esme_<systemid>.config`.
8. **Fault injection**: dedicated instance's `dlr.conf`/`shouldReturnUndeliveredReceipts` cover DLR-level faults; explicit `ESME_RTHROTTLED`/`ESME_RMSGQFUL`/bind-refusal/connection-drop simulation not documented for either tier — unverified.
9. **TLS**: shared service — "TLS 1.1 and up are supported for SMPP sessions. SSL and TLS 1.0 are not supported," on a separate port **8775** [main service page]. Dedicated instances also offer TLS as a paid option.
10. **Minimal use**: bind to `smscsim.melroselabs.com:2775` with credentials issued after signing up for a free developer account ([python tutorial](https://developers.melroselabs.com/docs/send-sms-with-smpp-using-python) shows the PDU shape but not the signup flow itself).
11. **Quirks / limits**: shared service capped at 100 SMS/sec; a separate, apparently-independent free public simulator is listed at `smscsim.smpp.org:2775` (max 2 SMS/sec) via [smpp.org's testing page](https://smpp.org/smpp-testing-development.html) — relationship to Melrose Labs unverified. Melrose Labs also offers standalone browser tools worth knowing about: an [SMPP client](https://melroselabs.com/) (bind/submit without installing anything), [SMPP Load Test](https://melroselabs.com/), and an [SMPP Analyser](https://melroselabs.com/) that captures a session to a downloadable pcap.
## 5. ukarim/smscsim — best lightweight OSS option
1. [github.com/ukarim/smscsim](https://github.com/ukarim/smscsim), Go, MIT, "Lightweight, zero-dependency and stupid SMSc simulator." GitHub push activity is current (last push 2025-12-12) — the most recently-touched OSS candidate found besides Jasmin/jsmpp.
2. **Docker**: `ukarim/smscsim` on Docker Hub. Highest **semver** tag is `0.2.0` (pushed ~3 years ago per Docker Hub); newer builds exist only as commit-hash tags (e.g. `a2c646d`, ~2 years ago) with no semver — **use `ukarim/smscsim:0.2.0` for a reproducible pinned tag**, or pin the specific commit-hash tag if you need the newer build and accept it's not semver.
```
docker run -p 2775:2775 -p 12775:12775 ukarim/smscsim:0.2.0
```
Ports: SMPP `2775` (env `SMSC_PORT`), web UI `12775` (env `WEB_PORT`). No auth/credentials required by default.
3. **SMPP version**: implements "only a small subset of the SMPP3.4 specification." PDUs: `bind_transmitter`, `bind_receiver`, `bind_transceiver`, `unbind`, `submit_sm`, `enquire_link`, `deliver_sm_resp`. Explicitly: **"simulator does not perform PDU validation"** — so it will not reject a bad `interface_version` or malformed PDU; not useful for negative-path bind testing.
4. **Delivery receipts**: fixed — DLR always returned ~2s after `submit_sm` with `message_state` **always DELIVERED**; no way to simulate other states except via the `FAILED_SUBMITS` fault-injection flag below. TLV vs text not detailed in the README.
5. **Long messages**: not documented — given the minimal PDU set, treat UDH/`sar_*`/`message_payload` support as unverified/likely absent.
6. **Encodings**: not documented — unverified.
7. **MO injection**: web page at `http://localhost:12775` sends a `deliver_sm` to the active session — simplest MO-injection UX of any candidate here.
8. **Fault injection**: `FAILED_SUBMITS=1` env var — even sequence numbers get a `submit_sm` system-error response, odd sequence numbers get an undeliverable DLR. No throttling, no bind refusal, no connection-drop simulation.
9. **TLS**: not mentioned — unsupported.
10. **Minimal run**: shown above; no config file, everything is env vars.
11. **Quirks**: "does not perform PDU validation" is explicit in the README — good for happy-path/throughput testing, useless for asserting your library correctly handles a *rejecting* SMSC.
## 613. Other candidates (condensed)
| Candidate | Notes |
|---|---|
| [mdouchement/smsc3](https://github.com/mdouchement/smsc3) | Go, MIT, SMPP3.4-based, port 20001 (SMPP)/6000 (HTTP). MO injection via `POST /deliver` with JSON `{session, sender, recipient, message}`. Has Dockerfile+compose but built around integrating with Kannel specifically. Last push 2024-05-09, low adoption (10 stars). |
| [K2InformaticsGmbH/smsc_simulator](https://github.com/K2InformaticsGmbH/smsc_simulator) | Erlang, Apache-2.0, speaks both SMPP and UCP. Abandoned — **last commit 2018-08-31**. Configured/driven from the Erlang shell (`smsc_simulator:start(smpp,10000)`), no Docker. Skip unless you specifically need UCP too. |
| [MavoCz/smscsim](https://github.com/MavoCz/smscsim) | Java, built on cloudhopper-smpp, so inherits 3.3/3.4/5.0 support and real PDU validation. Multi-port CLI (`-p 34567 34568 34569`), sends DLRs after a configurable fixed/random delay round-robin to connected RX/TRX binds. No Docker; requires Maven build + editing Spring XML for anything beyond port/log-level. Last-commit date unverified. |
| [fizzed/cloudhopper-smpp](https://github.com/fizzed/cloudhopper-smpp) (successor to [twitter-archive/cloudhopper-smpp](https://github.com/twitter-archive/cloudhopper-smpp), which is archived) | Java library, Apache-2.0, not a packaged simulator — but ships runnable demo classes (`make server`, `make server-echo`, `make simulator`, `make ssl-server`) covering SSL and PDU-dump. Good as a base to *write* a custom fixture, not a drop-in server. Last push 2020-10-12. |
| [jsmpp](https://github.com/opentelecoms-org/jsmpp) `jsmpp-examples/.../SMPPServerSimulator.java` | Java, Apache-2.0. jsmpp itself is very actively maintained (**last push 2026-06-17**) — best-maintained *library* in this whole survey. The bundled `SMPPServerSimulator` example demonstrates DLR sending and SSL, but is example code, not a packaged/dockerized tool — expect to fork and extend it. |
| [farhadi/node-smpp](https://github.com/farhadi/node-smpp) | Node.js, MIT, implements SMPP v5 (backward compatible with 3.4), includes both client and server APIs, supports `ssmpp://` TLS, UDH via `message_payload`, and encoding auto-detection (ASCII/Latin1/UCS2). No packaged simulator binary — you write ~30 lines of server code yourself. Last push 2023-12-28. Small existing forks built on it: [tiltroom/fakesmpp](https://github.com/tiltroom/fakesmpp), [theodorosidmar/smpp-server-simulator](https://github.com/theodorosidmar/smpp-server-simulator) (returns error on 1-in-10 `submit_sm`) — neither independently verified for maintenance/quality here. |
| [MikeSafonov/smpp-server-mock](https://github.com/MikeSafonov/smpp-server-mock) | Java, MIT, JUnit5-extension/Spring-Boot-starter mock server for *your own* test suite (assert on captured `SubmitSm`s) — not a standalone server you point an arbitrary client at. Only useful if you also write JVM-side interop tests. Last push 2023-03-10. |
| Restcomm/TeleStax SMSC Gateway "SMPP Simulator" | Bundled GUI test tool inside the RestComm SMSC Gateway product (`$SMSC_HOME/tools/TelScale-smpp-simulator/bin/run.sh`), default `system_id=test/password=test`, port 2776, transceiver bind, address range `6666` [docs](https://github.com/RestComm/smscgateway/blob/master/docs/adminguide/sources/src/main/resources/en-US/Chapter-smpp-simulator.xml). This is really a load-test client for RestComm's own gateway, not an installable-alone SMSC simulator — and the whole product requires a JSLEE stack. High setup cost for low unique coverage; skip unless you're specifically validating against a carrier-grade SS7-adjacent stack. |
| OsmoMSC SMPP interface | C, part of Osmocom's core-network stack (actively maintained as infrastructure, not as a "test double"). Its SMPP interface lets an ESME bind to a *real* (if simulated-radio) mobile-network MSC to send/receive SMS to subscribers — valuable only if you're also running an Osmocom test network; far too heavy just to test an SMPP library. Treat as out of scope for this project. |
| `smscsim.smpp.org` | Free public SMSC simulator, host/port from [smpp.org's testing page](https://smpp.org/smpp-testing-development.html): `smscsim.smpp.org:2775`, capped at 2 SMS/sec, delivered-only DLRs, per-IP connection limits. Ownership/maintenance unverified — treat as a convenience fallback, not a primary target. |
| Ozeki NG SMS Gateway / NowSMS | Both have a free/trial tier with SMPP server capability, both are closed-source and Windows-first (Ozeki explicitly; NowSMS also ships Linux/Docker in some tiers but is paid-oriented) — out of scope per task instructions beyond this line. |
---
## Recommendation — set these up first
1. **ukarim/smscsim** (Docker `ukarim/smscsim:0.2.0`) — near-zero setup cost, fastest possible CI smoke test: bind, submit, get a DLR, inject an MO from the web UI. Its explicit "no PDU validation" and single fixed DLR state are limitations, not blockers, for a first pass.
2. **Jasmin SMS Gateway** (Docker `jookies/jasmin:0.11.0` + RabbitMQ + Redis) — the only candidate here that is a real, still-developed production SMS gateway with a proper SMPP **server** role, HTTP management, and a DLR pipeline distinct from a toy simulator. Exercises your library against genuinely different internals (Python/Twisted, AMQP-backed message routing) than the Go/Java toys.
3. **Melrose Labs hosted SMSC Simulator** (`smscsim.melroselabs.com:2775`, free tier) — a public, independently-run, closed-box target you don't control, which is exactly the point: it validates your library against an implementation you cannot special-case for. Its TLS port (8775) also covers your TLS interop case for free without standing up your own CA. Follow up with a **dedicated** instance (paid) once you need to script specific DLR failure/latency states via `dlr.conf`.
4. **SMPPSim, built yourself from `kwahome/smpp-sim-docker`** — despite the dead upstream site and lack of a pinned tag, it's the only candidate with rich, config-file-driven **fault injection** (percentage-based delivered/undeliverable/rejected/accepted, intermediate-state transition chance, delayed DLRs, outbind support, MO queue-full retry behavior). Worth the one-time cost of building and tagging your own image specifically to cover the failure- and delay-injection matrix nothing else here offers as cleanly.
Skip Kannel/opensmppbox and `fakesmsc` for now: both Kannel and opensmppbox have had no meaningful commits in a decade, opensmppbox's interop behavior (TLS, throttling, interface_version validation) is entirely undocumented, and `fakesmsc` — despite the name — doesn't speak SMPP at all, so it cannot test this library regardless of effort spent.