|
| 1 | +# Pandatech.Communicator |
| 2 | + |
| 3 | +Send email via SMTP and SMS via Dexatel or Twilio through a DI-friendly, multi-channel API. Supports both |
| 4 | +`appsettings.json` and programmatic configuration, named channels per transport, and a fake mode for local development. |
| 5 | + |
| 6 | +Targets **`net8.0`**, **`net9.0`**, and **`net10.0`**. |
| 7 | + |
| 8 | +--- |
| 9 | + |
| 10 | +## Table of Contents |
| 11 | + |
| 12 | +1. [Features](#features) |
| 13 | +2. [Installation](#installation) |
| 14 | +3. [Registration](#registration) |
| 15 | +4. [Configuration](#configuration) |
| 16 | +5. [Channels](#channels) |
| 17 | +6. [Sending Email](#sending-email) |
| 18 | +7. [Sending SMS](#sending-sms) |
| 19 | +8. [Fake Mode](#fake-mode) |
| 20 | + |
| 21 | +--- |
| 22 | + |
| 23 | +## Features |
| 24 | + |
| 25 | +- Email over SMTP using MailKit — TLS negotiation, optional authentication, CC/BCC, attachments, HTML body |
| 26 | +- SMS via Dexatel and Twilio with a unified `GeneralSmsResponse` |
| 27 | +- Named channels — configure multiple senders per transport (e.g. `TransactionalSender`, `MarketingSender`) and pick |
| 28 | + the right one per message |
| 29 | +- Validation on every send call — recipients, addresses, and phone numbers are checked before any network call |
| 30 | +- Fake mode — logs messages at `Critical` instead of sending; zero external calls in development or test environments |
| 31 | +- Supports both `WebApplicationBuilder` and plain `IServiceCollection` registration |
| 32 | + |
| 33 | +--- |
| 34 | + |
| 35 | +## Installation |
| 36 | + |
| 37 | +```bash |
| 38 | +dotnet add package Pandatech.Communicator |
| 39 | +``` |
| 40 | + |
| 41 | +--- |
| 42 | + |
| 43 | +## Registration |
| 44 | + |
| 45 | +### WebApplicationBuilder |
| 46 | + |
| 47 | +```csharp |
| 48 | +builder.AddCommunicator(); // reads from appsettings.json "Communicator" section |
| 49 | +// or |
| 50 | +builder.AddCommunicator(options => { /* programmatic setup */ }); |
| 51 | +``` |
| 52 | + |
| 53 | +### IServiceCollection |
| 54 | + |
| 55 | +```csharp |
| 56 | +services.AddCommunicator(configuration); |
| 57 | +// or |
| 58 | +services.AddCommunicator(configuration, options => { /* programmatic setup */ }); |
| 59 | +``` |
| 60 | + |
| 61 | +Both register `IEmailService` and `ISmsService` into DI as scoped services. |
| 62 | + |
| 63 | +--- |
| 64 | + |
| 65 | +## Configuration |
| 66 | + |
| 67 | +### appsettings.json |
| 68 | + |
| 69 | +```json |
| 70 | +{ |
| 71 | + "Communicator": { |
| 72 | + "EmailFake": false, |
| 73 | + "SmsFake": false, |
| 74 | + "EmailConfigurations": { |
| 75 | + "TransactionalSender": { |
| 76 | + "SmtpServer": "smtp.gmail.com", |
| 77 | + "SmtpPort": 587, |
| 78 | + "SmtpUsername": "you@example.com", |
| 79 | + "SmtpPassword": "app-password", |
| 80 | + "SenderEmail": "no-reply@example.com", |
| 81 | + "SenderName": "My App", |
| 82 | + "TimeoutMs": 10000 |
| 83 | + }, |
| 84 | + "MarketingSender": { |
| 85 | + "SmtpServer": "smtp.sendgrid.net", |
| 86 | + "SmtpPort": 587, |
| 87 | + "SmtpUsername": "apikey", |
| 88 | + "SmtpPassword": "SG.xxx", |
| 89 | + "SenderEmail": "marketing@example.com", |
| 90 | + "TimeoutMs": 10000 |
| 91 | + } |
| 92 | + }, |
| 93 | + "SmsConfigurations": { |
| 94 | + "TransactionalSender": { |
| 95 | + "Provider": "Dexatel", |
| 96 | + "From": "MyApp", |
| 97 | + "TimeoutMs": 10000, |
| 98 | + "Properties": { |
| 99 | + "X-Dexatel-Key": "your-dexatel-api-key" |
| 100 | + } |
| 101 | + }, |
| 102 | + "NotificationSender": { |
| 103 | + "Provider": "Twilio", |
| 104 | + "From": "+15550001234", |
| 105 | + "TimeoutMs": 10000, |
| 106 | + "Properties": { |
| 107 | + "SID": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", |
| 108 | + "AUTH_TOKEN": "your-auth-token" |
| 109 | + } |
| 110 | + } |
| 111 | + } |
| 112 | + } |
| 113 | +} |
| 114 | +``` |
| 115 | + |
| 116 | +### Programmatic |
| 117 | + |
| 118 | +```csharp |
| 119 | +builder.AddCommunicator(options => |
| 120 | +{ |
| 121 | + options.EmailConfigurations = new Dictionary<string, EmailConfiguration> |
| 122 | + { |
| 123 | + ["TransactionalSender"] = new() |
| 124 | + { |
| 125 | + SmtpServer = "smtp.gmail.com", |
| 126 | + SmtpPort = 587, |
| 127 | + SmtpUsername = "you@example.com", |
| 128 | + SmtpPassword = "app-password", |
| 129 | + SenderEmail = "no-reply@example.com", |
| 130 | + SenderName = "My App" |
| 131 | + } |
| 132 | + }; |
| 133 | + |
| 134 | + options.SmsConfigurations = new Dictionary<string, SmsConfiguration> |
| 135 | + { |
| 136 | + ["TransactionalSender"] = new() |
| 137 | + { |
| 138 | + Provider = "Dexatel", |
| 139 | + From = "MyApp", |
| 140 | + Properties = new() { ["X-Dexatel-Key"] = "your-key" } |
| 141 | + } |
| 142 | + }; |
| 143 | +}); |
| 144 | +``` |
| 145 | + |
| 146 | +--- |
| 147 | + |
| 148 | +## Channels |
| 149 | + |
| 150 | +Channel names are validated at startup against a fixed set of supported names: |
| 151 | + |
| 152 | +``` |
| 153 | +GeneralSender |
| 154 | +TransactionalSender |
| 155 | +NotificationSender |
| 156 | +MarketingSender |
| 157 | +SupportSender |
| 158 | +``` |
| 159 | + |
| 160 | +Each channel maps to exactly one configuration entry. The `Channel` property on `EmailMessage` and `SmsMessage` |
| 161 | +selects which configuration is used for that send call. |
| 162 | + |
| 163 | +--- |
| 164 | + |
| 165 | +## Sending Email |
| 166 | + |
| 167 | +```csharp |
| 168 | +public class NotificationService(IEmailService emailService) |
| 169 | +{ |
| 170 | + public async Task SendWelcomeAsync(string userEmail, CancellationToken ct) |
| 171 | + { |
| 172 | + var message = new EmailMessage |
| 173 | + { |
| 174 | + Recipients = [userEmail], |
| 175 | + Subject = "Welcome!", |
| 176 | + Body = "<h1>Thanks for signing up.</h1>", |
| 177 | + IsBodyHtml = true, |
| 178 | + Channel = EmailChannels.TransactionalSender, |
| 179 | + Cc = ["manager@example.com"], |
| 180 | + Attachments = [new EmailAttachment("terms.pdf", pdfBytes)] |
| 181 | + }; |
| 182 | + |
| 183 | + var response = await emailService.SendAsync(message, ct); |
| 184 | + } |
| 185 | +} |
| 186 | +``` |
| 187 | + |
| 188 | +`SendBulkAsync` accepts a list of messages, opens one SMTP connection per channel, and sends all messages for that |
| 189 | +channel on the same connection before moving to the next. |
| 190 | + |
| 191 | +--- |
| 192 | + |
| 193 | +## Sending SMS |
| 194 | + |
| 195 | +```csharp |
| 196 | +public class OtpService(ISmsService smsService) |
| 197 | +{ |
| 198 | + public async Task SendOtpAsync(string phoneNumber, string code, CancellationToken ct) |
| 199 | + { |
| 200 | + var message = new SmsMessage |
| 201 | + { |
| 202 | + Recipients = [phoneNumber], |
| 203 | + Message = $"Your code is {code}", |
| 204 | + Channel = SmsChannels.TransactionalSender |
| 205 | + }; |
| 206 | + |
| 207 | + var responses = await smsService.SendAsync(message, ct); |
| 208 | + } |
| 209 | +} |
| 210 | +``` |
| 211 | + |
| 212 | +Phone numbers are normalized before sending — `+`, `(`, `)`, and spaces are stripped, and Panda-formatted numbers |
| 213 | +like `(374)91123456` are handled automatically. |
| 214 | + |
| 215 | +### Provider-specific Properties |
| 216 | + |
| 217 | +| Provider | Required Properties | |
| 218 | +|-----------|----------------------------------| |
| 219 | +| Dexatel | `X-Dexatel-Key` | |
| 220 | +| Twilio | `SID`, `AUTH_TOKEN` | |
| 221 | + |
| 222 | +--- |
| 223 | + |
| 224 | +## Fake Mode |
| 225 | + |
| 226 | +Set `EmailFake: true` or `SmsFake: true` (or both) to replace the real services with fake implementations that log |
| 227 | +at `Critical` instead of making any network calls. The same validation still runs. |
| 228 | + |
| 229 | +```json |
| 230 | +{ |
| 231 | + "Communicator": { |
| 232 | + "EmailFake": true, |
| 233 | + "SmsFake": true |
| 234 | + } |
| 235 | +} |
| 236 | +``` |
| 237 | + |
| 238 | +Useful in local development and CI environments where you want to confirm messages are being sent without delivering |
| 239 | +them. |
| 240 | + |
| 241 | +--- |
| 242 | + |
| 243 | +## License |
| 244 | + |
| 245 | +MIT |
0 commit comments