SMSGate 1.2.0
dotnet add package SMSGate --version 1.2.0
NuGet\Install-Package SMSGate -Version 1.2.0
<PackageReference Include="SMSGate" Version="1.2.0" />
<PackageVersion Include="SMSGate" Version="1.2.0" />
<PackageReference Include="SMSGate" />
paket add SMSGate --version 1.2.0
#r "nuget: SMSGate, 1.2.0"
#:package SMSGate@1.2.0
#addin nuget:?package=SMSGate&version=1.2.0
#tool nuget:?package=SMSGate&version=1.2.0
SMSGate - SMPP client and server for .NET

SMSGate is a lightweight SMPP 3.4 client and server library for .NET 10 and .NET Framework 4.7.2+. Build SMS services with asynchronous submissions, delivery tracking, and control over incoming message acknowledgements. The .NET 10 target has no package dependencies; Framework uses Microsoft compatibility packages.
- Send and receive: client and server sessions, multipart SMS, GSM-7, Unicode, binary messages, and WAP Push.
- Track delivery: per-segment provider IDs, text and TLV receipts, and aggregate multipart delivery status.
- Control traffic: bounded async queues, PDU rate and window limits, and explicit submission outcomes.
- Operate services: optional TLS, storage before acknowledgement, graceful shutdown, structured diagnostics, and metrics.
Quick start · Supported platforms · Service integration · Provider integration · Changelog
Install
dotnet add package SMSGate --version 1.2.0
The NuGet package is named SMSGate; the assembly and namespaces are Smpp. Version 1.2.0 contains net10.0 and net472 assemblies; NuGet selects the appropriate target. For Framework applications, use PackageReference and enable automatic binding redirects. See the Framework guide for project setup, TLS options, and runtime validation.
Supported platforms
| Operating system | .NET 10 (net10.0) |
.NET Framework 4.7.2+ (net472) |
Validation |
|---|---|---|---|
| Supported | Supported in 1.2.0 | Debug/Release CI and package examples; Framework package validated on actual 4.7.2 and newer 4.8.1 runtimes | |
| Supported | Windows only | Debug/Release .NET 10 tests and package examples in Ubuntu CI | |
| Not yet qualified | Windows only | No macOS CI lane |
Framework 4.8 and 4.8.1 applications consume the net472 assembly. See the Framework guide for setup and the validation report for actual 4.7.2 and newer-runtime results. CI does not include dedicated ARM64 qualification. Use an operating system supported by your selected runtime. The package contains no native libraries; Mono, browser, and mobile runtimes are not qualified.
Quick start
Create a .NET 10 console application, install the package, and set SMPP_HOST, SMPP_PORT, SMPP_SYSTEM_ID, SMPP_PASSWORD, SMPP_SENDER, and SMPP_RECIPIENT for your provider account. The same async APIs work on Framework; enable C# 9 or later and put the code inside an async Task Main method. The example connects over TCP, waits for a successful bind, submits a message through the bounded async queue, and prints each segment's provider response.
using System;
using System.Threading;
using Smpp;
using Smpp.Events;
static string Setting(string name) =>
Environment.GetEnvironmentVariable(name)
?? throw new InvalidOperationException($"Set {name} before running.");
var eventsHub = new GateEvents();
await using var gateway = new SmppGateway(eventsHub);
var client = gateway.AddClient(new SmppClientOptions
{
ChannelName = "provider",
Host = Setting("SMPP_HOST"),
Port = int.Parse(Setting("SMPP_PORT")),
SystemId = Setting("SMPP_SYSTEM_ID"),
Password = Setting("SMPP_PASSWORD"),
Submission = new SubmissionOptions
{
QueueCapacity = 100,
MaxInFlightPdus = 5,
PdusPerSecond = 20
}
});
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var bind = await client.ConnectAndBindAsync(deadline.Token);
if (!bind.Bound)
throw new InvalidOperationException($"SMPP bind failed: {bind.CommandStatus}");
var result = await client.SubmitAsync(new OutboundMessage(
MessageId: 42,
Sender: Setting("SMPP_SENDER"),
Recipient: Setting("SMPP_RECIPIENT"),
Body: "Hello from SMSGate"), deadline.Token);
Console.WriteLine($"Submission: {result.Outcome}");
if (!result.Accepted)
Console.WriteLine($"May have been accepted: {result.MayHaveBeenAccepted}");
foreach (var part in result.Parts)
Console.WriteLine($"Part {part.PartNumber}: {part.ProviderMessageId}, SMPP {part.CommandStatus}");
Accepted confirms successful provider submission. Handset delivery is reported separately through MessageDeliveryStatusEvent; keep your service connected to receive receipts. An unknown outcome may already have been accepted by the provider, so check MayHaveBeenAccepted before deciding how to retry. await using drains pending work and closes the gateway when the example ends.
For a TLS endpoint, configure TlsOptions with the provider's certificate hostname and port. Match the provider's TON/NPI, GSM-7, rate, and window settings using the provider guide. Credentials belong in your application's configuration or secret provider.
Runnable examples
These examples use a local SMPP peer and need no provider account:
| Example | Demonstrates |
|---|---|
| WorkerService | SmppGateway, SubmitAsync, Generic Host/DI, delivery tracking, and graceful shutdown |
| ProviderFeatures | Validated TLS, GSM-7 multipart messages, and storage before acknowledgement |
| LocalExchange | A complete client/server exchange using the legacy Gate API |
| FrameworkExchange | A net472 package consumer using SmppGateway, multipart SubmitAsync, delivery tracking, and graceful shutdown |
| InteropPeer | Independent Python SMPP wire exchanges in both directions |
dotnet run --project examples/WorkerService --configuration Release
Run that command from a clone of this repository. See the service integration guide for lifecycle, delivery tracking, diagnostics, and persistence guidance.
Upgrading from 1.0.0
SMSGate 1.1.0 adds opt-in GSM-7 encoding, complete multipart delivery tracking, TLS, and application-controlled acknowledgements. Existing APIs, event signatures, plain TCP, automatic acknowledgements, and inbound alphabet defaults remain available. Enable UseGsm7 only when your provider uses the GSM default alphabet for DCS 0. Configure TLS and an InboundMessageHandler explicitly when required. Legacy SubmitSm bypasses the new queue limits; use SubmitAsync consistently when those limits must apply to all submissions. Queues and delivery tracking are held in memory, so keep a durable application outbox and duplicate policy when recovery across process restarts is required.
API overview
SmppGatewayowns client connections and provides graceful shutdown for services and dependency injection.SmppClientOptionsvalidates provider configuration before creating a client.Client.ConnectAndBindAsync()waits for the bind response;Client.SubmitAsync()queues submissions and returns per-PDU outcomes.Gateis the legacy entry point used to register connections and share a singleGateEventsinstance.Serverregisters accepted credentials and hosts a static TCP listener that authenticates inbound binds.
Connection Model
Client
Create an instance-owned client with SmppGateway.AddClient(...) and await ConnectAndBindAsync() before calling SubmitAsync(). The returned BindResult.Bound confirms the provider accepted the bind.
The legacy Gate.AddClientConnection(...), Connect(), and ConnectAsync() APIs remain available. A true return value from Connect() or ConnectAsync() means the TCP connection opened and the bind request was sent. Wait for connected == true before submitting messages through that API; it becomes true only after the matching successful bind response. Concurrent connect calls share one attempt. Connection attempts run outside the session lock, and Quit() cancels an in-progress attempt.
EOF, socket errors, rejected binds, invalid frame lengths, and unanswered bind or keepalive requests trigger a reconnect after reconnectDelay. Only one retry is scheduled per connection. A late message acknowledgement expires that submission without disconnecting or timing out other messages. Quit() cancels retries, closes the session, and unregisters the client permanently.
Binding mode is selected from these flags before calling Connect():
direction_in = true,direction_out = true→ bind as transceiverdirection_in = false,direction_out = true→ bind as transmitterdirection_in = true,direction_out = false→ bind as receiver
Server
Create one or more logical server channels with Gate.AddServerConnection(...), then start the shared listener with Server.StartAsync(ip, port).
Each registered server channel acts as an allowed credential pair. During bind, the listener authenticates the inbound system_id and password against the configured server channels and routes the accepted session to the matching server object. Remote unbind, EOF, and timeout close the session while preserving its credential registration for a later bind. Explicit Quit() or Server.Stop() unregisters it.
StartAsync starts the listener before returning; startup errors are thrown to the caller. A second start while running is rejected. Each channel accepts one active session at a time, and a concurrent bind is rejected without replacing the existing socket. Transient accept errors retry with a short backoff. Unauthenticated binds use asynchronous reads with one ten-second deadline for the whole handshake; Stop() cancels pending handshakes.
Sending Messages
For client services, use Client.SubmitAsync(...) as shown in the quick start. It applies the configured queue, rate, and window limits and returns a SubmissionResult with every segment's response. See the service guide for outcome handling and retry rules.
Both Client and Server also expose the legacy direct-send SubmitSm(...) API below. It bypasses the async queue's limits.
int result = client.SubmitSm(
message_id: 101,
sender: "15550001",
recipient: "15550002",
body: "Test message",
body_format: "ascii",
delivery_report: true);
Return codes:
0- accepted for sending1- no active connection2- throttled by the configured send limit3- sequence reservation failed (occupied ranges are normally skipped automatically)502- transport write failed
For server-originated traffic:
use_deliver_sm = falsesendsdata_smuse_deliver_sm = truesendsdeliver_sm
Body Formats
The library maps these body format names internally:
ascii- text using the library's existing single-byte mappinggsm7- the GSM 03.38 default alphabet and extension table, sent withdata_coding = 0auto- onClient.SubmitSmandServer.SubmitSm, selects GSM-7 when possible, otherwise Unicodebinary- hexadecimal octets, such as00FF80; decoded bodies are uppercase hexlatin- ISO-8859-1 textunicode- big-endian UTF-16 textwap_push- a URL or an existing WAP Push hexadecimal payload
For submit_sm and deliver_sm, large_message selects multipart (1), a message_payload TLV (2), truncation (3), or an exception when the single-message limit is exceeded (4). Single-message limits are 160 ASCII characters, 70 UTF-16 code units, or 140 Latin/binary octets. Multipart bodies use up to 153 ASCII characters, 63 UTF-16 code units, or 134 Latin/binary octets per segment. Surrogate pairs stay together. More than 255 segments or a payload larger than 65,535 octets is rejected before sending. data_sm always uses a payload TLV.
Long WAP Push bodies must use payload mode (large_message = 2); multipart mode rejects them before sending so that the port-addressing UDH remains valid.
For GSM-7 providers, set UseGsm7 = true on the receiving connection and send with body_format: "gsm7" or "auto". Extension characters such as €, {, and ^ count as two septets; multipart splitting keeps escape sequences together. Explicit gsm7 rejects unsupported characters instead of replacing them. Existing ascii behavior remains available for compatibility.
Multipart reassembly supports submit_sm, deliver_sm, and data_sm and separates sender, recipient, format, and command type as well as the reference number. Already-acknowledged segments survive transient disconnects and rebinds. Duplicate segments do not count twice; incomplete assemblies expire after five minutes, and explicit Quit() clears them. Reusing the same reference for overlapping messages with identical addressing and format remains ambiguous on the wire.
Key Configuration Fields
For new clients, start with validated SmppClientOptions and SubmissionOptions. The fields below remain available on Client or Server for legacy configuration; set them before traffic begins.
| Field | Default | Purpose |
|---|---|---|
debug |
false |
Includes raw PDU payloads in ChannelEvent logs; outgoing bind credentials are omitted |
system_type |
"" |
SMPP system type |
source_ton / source_npi |
0 / 0 |
Source addressing configuration |
destination_ton / destination_npi |
0 / 0 |
Destination addressing configuration |
registered_delivery |
1 |
Requests delivery reporting when supported |
large_message |
1 |
Large-message handling: multipart, payload, truncate, or fail |
use_deliver_sm |
false |
Makes server-originated sends use deliver_sm instead of data_sm |
use8bit |
false |
Packs ASCII septets into octets; configure both sending and receiving peers consistently |
split_base_ascii |
153 |
ASCII multipart split threshold |
multypart_delay |
100 |
Delay in milliseconds between multipart segments |
reconnectDelay |
10000 |
Delay before reconnect attempts in most reconnect paths |
keep_alive |
60000 |
Enquire-link interval in milliseconds; must be positive |
timeout |
10000 |
Bind, command, and message-ACK timeout in milliseconds; must be positive |
Events
GateEvents is the main integration surface for application code.
Connection events are dispatched asynchronously in order per connection, outside the session lock. A handler can call connection methods or wait for another thread to do so without blocking protocol reads or timers. Exceptions from handlers are logged and later events continue. Different connections can invoke the shared event hub concurrently; application handlers should synchronize shared state. Method return does not imply that its event handlers have finished.
| Event | Purpose |
|---|---|
Event |
General informational and error messages |
ChannelEvent |
Low-level SMPP channel activity, including bind, send, receive, and disconnect messages |
MessageStatusEvent |
Message lifecycle changes for submitted messages |
MessageDeliveryReportEvent |
Delivery reports parsed from inbound deliver_sm PDUs |
NewMessageEvent |
Application-facing inbound messages with sender, recipient, body, and format |
MessagePartStatusEvent |
Every PDU acknowledgement or timeout, with segment number, provider ID, and raw SMPP response code |
DeliveryReceiptEvent |
Parsed receipt fields, error details, and message/segment correlation when available |
MessageDeliveryStatusEvent |
Multipart delivery progress and one terminal result with a snapshot of all parts |
Each submission emits its initial waiting/routed status once. A multipart submission reports sent only after all segments succeed, using the first segment's SMSC message ID. The first nonzero response reports rejected, including unrecognized vendor errors; later successes or missing acknowledgements cannot replace that rejection. Delivery receipts require a valid ID and recognized status. Missing, truncated, or unknown receipt fields are logged instead of being treated as successful delivery.
The legacy events retain these semantics. The new tracking events expose every segment ID and support standard receipt TLVs on both deliver_sm and data_sm, with text receipts as a fallback. A complete message is reported delivered only after every segment is delivered. See the integration guide for tracking retention limits, early receipts, and acknowledgement control.
Complete message requests that cannot be decoded receive an error generic_nack with their sequence number. Invalid frame lengths close the stream because its boundaries cannot be recovered. alert_notification and outbind on an established session receive no response.
Useful message states come from Common.MessageStatus, including submitted_waiting_for_ACK, sent, delivered_ACK_received, queued, timeout, rejected, and message_undeliverable.
Tested Scenarios
The test suite includes the original command and integration tests plus regression fixtures for:
- every byte split position in representative PDUs, coalesced frames, malformed trailing frames, and maximum payloads
- ASCII, Latin, Unicode, binary, packed ASCII, and WAP Push encoding, including length boundaries and wire-level length/UDH assertions
- multipart reference collisions, duplicates, reordering, malformed segment counts, expiration, and 255-part limits
- all three bind modes, incorrect credentials, unmatched responses, EOF/reset recovery, timeouts, rebinds, and cancelled reconnects
- concurrent multipart submissions, sequence allocation, send failures, and acknowledgement correlation
- every response ordering for three-part submissions, reconnects between acknowledged segments, and isolated message timeouts
- transient accept failures, slow bind deadlines, in-progress connect cancellation, and callbacks that block, throw, or send replies
- sustained buffered traffic, dense framing allocation limits, malformed-request error replies, and notification commands without responses
- response error encoding and valid, malformed, Unicode, multipart, and long delivery reports
Run the complete suite in both configurations:
dotnet test SMSGate.sln --configuration Debug -warnaserror
dotnet test SMSGate.sln --configuration Release -warnaserror
Current Limitations
The implementation is usable, but there are several behaviors worth knowing before adopting it in production:
- Legacy
Gateconnection registries are stored in static global collections, so multipleGateinstances share process-wide state.SmppGatewayowns its outbound clients independently; the server listener remains static. - Connection configuration and the public registries remain mutable. Set configuration before traffic and avoid modifying the registries directly while connections are active.
- Packed ASCII has no separate septet count in the PDU; a trailing zero septet is treated as padding.
- Packed GSM-7 uses CR padding; a genuine trailing CR can be ambiguous without a separate septet count. Unpacked GSM-7 is recommended for SMPP providers.
- Automatic acknowledgements remain the compatibility default. Set
InboundMessageHandlerto persist or reject each incoming PDU before responding; ordinary event handlers remain observational.
Development Notes
Build from source with the .NET 10 SDK matching global.json:
dotnet restore SMSGate.sln -warnaserror
dotnet build SMSGate.sln -warnaserror
The solution contains the library in Smpp/ and NUnit tests in Smpp.Tests/. The fastest feedback loop when extending the library is:
dotnet test SMSGate.sln -warnaserror
That exercises both the command-level tests and the smoke/integration scenarios shipped with the solution. Tests use local TCP listeners and need no SMS provider account or external service. Run configurations sequentially and keep loopback port 2775 free.
To build the NuGet package and verify the example against it locally:
dotnet pack Smpp/Smpp.csproj --configuration Release -warnaserror --output artifacts
dotnet restore examples/LocalExchange/LocalExchange.csproj --source "$PWD/artifacts" -warnaserror
dotnet run --project examples/LocalExchange/LocalExchange.csproj --configuration Release --no-restore
The package includes the SMSGate icon, this README, the MIT license, release notes, XML API documentation, and a companion symbols package with Source Link. GitHub Actions validates both build configurations and the examples before publishing a version tag through NuGet Trusted Publishing. Maintainers can follow the release guide. Editable logos, PNG exports, and platform icon attribution are in the branding assets.
Contributing
Issues and pull requests are welcome. Include a reproducible example for protocol or session bugs, keep changes compatible with the documented API, and run the Debug and Release suites before submitting a change. Existing tests cover wire encoding, session recovery, and local client/server exchanges; add focused coverage when changing those behaviors.
License
SMSGate is distributed under the MIT license.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Framework | net472 is compatible. net48 was computed. net481 was computed. |
-
.NETFramework 4.7.2
- Microsoft.Bcl.AsyncInterfaces (>= 10.0.13)
- System.Diagnostics.DiagnosticSource (>= 10.0.13)
- System.Memory (>= 4.6.3)
- System.Threading.Channels (>= 10.0.13)
-
net10.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Adds .NET Framework 4.7.2 and later alongside .NET 10, including asynchronous submissions, delivery tracking, diagnostics, cancellation, and TLS. Framework-only compatibility dependencies and portable TLS options preserve the existing .NET 10 API. See https://github.com/kirakosyan/smsgate/blob/v1.2.0/CHANGELOG.md.