Skip to content

Commit d8ceca3

Browse files
authored
Merge pull request #21 from PandaTechAM/development
modernized, multiplatform + better readme.md
2 parents e07fefd + e56c1f4 commit d8ceca3

File tree

5 files changed

+354
-355
lines changed

5 files changed

+354
-355
lines changed

.github/workflows/main.yml

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,73 @@
1-
name: Deploy NuGet Package
1+
name: Publish NuGet Package
22

33
env:
4-
PROJECT_PATH: './src/Communicator/Communicator.csproj'
5-
OUTPUT_DIR: 'nupkgs'
6-
NUGET_SOURCE: 'https://api.nuget.org/v3/index.json'
7-
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
4+
PROJECT_PATH: src/Communicator/Communicator.csproj
5+
OUTPUT_DIR: nupkgs
6+
NUGET_SOURCE: https://api.nuget.org/v3/index.json
87

98
on:
109
push:
11-
branches:
12-
- main
10+
branches: [main]
11+
12+
permissions:
13+
contents: read
14+
1315
jobs:
14-
deploy:
16+
publish:
1517
runs-on: ubuntu-latest
1618

1719
steps:
1820
- name: Checkout
19-
uses: actions/checkout@v6
21+
uses: actions/checkout@v4
2022

21-
- name: Setup .NET Core
22-
uses: actions/setup-dotnet@v5
23+
- name: Setup .NET
24+
uses: actions/setup-dotnet@v4
2325
with:
2426
global-json-file: global.json
2527

28+
- name: Restore
29+
run: dotnet restore ${{ env.PROJECT_PATH }}
30+
2631
- name: Build
27-
run: dotnet build ${{ env.PROJECT_PATH }}
32+
run: dotnet build ${{ env.PROJECT_PATH }} --no-restore --configuration Release
33+
34+
- name: Test
35+
run: dotnet test --no-build --configuration Release --verbosity normal
2836

2937
- name: Pack
30-
run: dotnet pack ${{ env.PROJECT_PATH }} --output ${{ env.OUTPUT_DIR }}
38+
run: dotnet pack ${{ env.PROJECT_PATH }} --no-build --configuration Release --output ${{ env.OUTPUT_DIR }}
3139

3240
- name: Publish
33-
run: dotnet nuget push ${{ env.OUTPUT_DIR }}/*.nupkg -k ${{ env.NUGET_API_KEY }} -s ${{ env.NUGET_SOURCE }}
41+
env:
42+
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
43+
shell: bash
44+
run: |
45+
set -euo pipefail
46+
47+
if [ -z "${NUGET_API_KEY:-}" ]; then
48+
echo "NUGET_API_KEY secret is not set."
49+
exit 1
50+
fi
51+
52+
shopt -s nullglob
53+
54+
nupkgs=( "${{ env.OUTPUT_DIR }}"/*.nupkg )
55+
snupkgs=( "${{ env.OUTPUT_DIR }}"/*.snupkg )
56+
57+
if [ ${#nupkgs[@]} -eq 0 ]; then
58+
echo "No .nupkg files found in ${{ env.OUTPUT_DIR }}"
59+
ls -la "${{ env.OUTPUT_DIR }}" || true
60+
exit 1
61+
fi
62+
63+
dotnet nuget push "${nupkgs[@]}" \
64+
--api-key "$NUGET_API_KEY" \
65+
--source "${{ env.NUGET_SOURCE }}" \
66+
--skip-duplicate
67+
68+
if [ ${#snupkgs[@]} -gt 0 ]; then
69+
dotnet nuget push "${snupkgs[@]}" \
70+
--api-key "$NUGET_API_KEY" \
71+
--source "${{ env.NUGET_SOURCE }}" \
72+
--skip-duplicate
73+
fi

README.md

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
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

Comments
 (0)