@hasna/emails 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/index.html +479 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +55403 -0
- package/dist/db/addresses.d.ts +10 -0
- package/dist/db/addresses.d.ts.map +1 -0
- package/dist/db/database.d.ts +8 -0
- package/dist/db/database.d.ts.map +1 -0
- package/dist/db/domains.d.ts +10 -0
- package/dist/db/domains.d.ts.map +1 -0
- package/dist/db/emails.d.ts +8 -0
- package/dist/db/emails.d.ts.map +1 -0
- package/dist/db/events.d.ts +16 -0
- package/dist/db/events.d.ts.map +1 -0
- package/dist/db/providers.d.ts +11 -0
- package/dist/db/providers.d.ts.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +54149 -0
- package/dist/lib/dns.d.ts +5 -0
- package/dist/lib/dns.d.ts.map +1 -0
- package/dist/lib/stats.d.ts +5 -0
- package/dist/lib/stats.d.ts.map +1 -0
- package/dist/lib/sync.d.ts +4 -0
- package/dist/lib/sync.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +58282 -0
- package/dist/providers/index.d.ts +7 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/interface.d.ts +37 -0
- package/dist/providers/interface.d.ts.map +1 -0
- package/dist/providers/resend.d.ts +24 -0
- package/dist/providers/resend.d.ts.map +1 -0
- package/dist/providers/ses.d.ts +23 -0
- package/dist/providers/ses.d.ts.map +1 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +54208 -0
- package/dist/server/serve.d.ts +6 -0
- package/dist/server/serve.d.ts.map +1 -0
- package/dist/types/index.d.ts +202 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Open Emails Dashboard</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; }
|
|
10
|
+
nav { background: #1e293b; border-bottom: 1px solid #334155; padding: 0 24px; display: flex; align-items: center; gap: 8px; height: 52px; }
|
|
11
|
+
nav h1 { font-size: 16px; font-weight: 700; color: #f1f5f9; margin-right: 16px; }
|
|
12
|
+
nav button { background: none; border: none; color: #94a3b8; cursor: pointer; padding: 6px 14px; border-radius: 6px; font-size: 13px; transition: all .15s; }
|
|
13
|
+
nav button:hover, nav button.active { background: #334155; color: #f1f5f9; }
|
|
14
|
+
.main { padding: 24px; max-width: 1200px; margin: 0 auto; }
|
|
15
|
+
.page { display: none; }
|
|
16
|
+
.page.active { display: block; }
|
|
17
|
+
h2 { font-size: 18px; font-weight: 600; margin-bottom: 20px; color: #f1f5f9; }
|
|
18
|
+
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 16px; margin-bottom: 28px; }
|
|
19
|
+
.card { background: #1e293b; border: 1px solid #334155; border-radius: 10px; padding: 18px; }
|
|
20
|
+
.card .label { font-size: 12px; color: #94a3b8; margin-bottom: 6px; text-transform: uppercase; letter-spacing: .5px; }
|
|
21
|
+
.card .value { font-size: 28px; font-weight: 700; color: #f1f5f9; }
|
|
22
|
+
.card .sub { font-size: 12px; color: #64748b; margin-top: 4px; }
|
|
23
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
24
|
+
th { text-align: left; padding: 10px 12px; color: #94a3b8; font-weight: 500; border-bottom: 1px solid #334155; font-size: 12px; text-transform: uppercase; letter-spacing: .4px; }
|
|
25
|
+
td { padding: 10px 12px; border-bottom: 1px solid #1e293b; }
|
|
26
|
+
tr:last-child td { border-bottom: none; }
|
|
27
|
+
tr:hover td { background: #1e293b; }
|
|
28
|
+
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }
|
|
29
|
+
.badge.verified, .badge.delivered { background: #166534; color: #86efac; }
|
|
30
|
+
.badge.pending, .badge.sent { background: #713f12; color: #fde68a; }
|
|
31
|
+
.badge.failed, .badge.bounced, .badge.complained { background: #7f1d1d; color: #fca5a5; }
|
|
32
|
+
.badge.resend { background: #1e3a5f; color: #93c5fd; }
|
|
33
|
+
.badge.ses { background: #2d1b69; color: #c4b5fd; }
|
|
34
|
+
.btn { padding: 7px 14px; border-radius: 6px; font-size: 13px; font-weight: 500; cursor: pointer; border: 1px solid; transition: all .15s; }
|
|
35
|
+
.btn-primary { background: #3b82f6; border-color: #3b82f6; color: #fff; }
|
|
36
|
+
.btn-primary:hover { background: #2563eb; }
|
|
37
|
+
.btn-danger { background: transparent; border-color: #ef4444; color: #ef4444; }
|
|
38
|
+
.btn-danger:hover { background: #7f1d1d; }
|
|
39
|
+
.btn-sm { padding: 4px 10px; font-size: 12px; }
|
|
40
|
+
.toolbar { display: flex; gap: 12px; align-items: center; margin-bottom: 16px; }
|
|
41
|
+
.toolbar select, .toolbar input { background: #1e293b; border: 1px solid #334155; color: #e2e8f0; padding: 7px 12px; border-radius: 6px; font-size: 13px; }
|
|
42
|
+
.error { background: #7f1d1d; border: 1px solid #ef4444; color: #fca5a5; padding: 12px 16px; border-radius: 8px; margin-bottom: 16px; font-size: 13px; }
|
|
43
|
+
.empty { text-align: center; color: #64748b; padding: 48px; font-size: 14px; }
|
|
44
|
+
.panel { background: #1e293b; border: 1px solid #334155; border-radius: 10px; padding: 0; overflow: hidden; }
|
|
45
|
+
.pagination { display: flex; gap: 8px; align-items: center; margin-top: 14px; font-size: 13px; color: #94a3b8; }
|
|
46
|
+
.pagination button { background: #1e293b; border: 1px solid #334155; color: #e2e8f0; padding: 5px 12px; border-radius: 5px; cursor: pointer; font-size: 12px; }
|
|
47
|
+
.pagination button:disabled { opacity: .4; cursor: not-allowed; }
|
|
48
|
+
.modal-bg { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.7); z-index: 100; align-items: center; justify-content: center; }
|
|
49
|
+
.modal-bg.open { display: flex; }
|
|
50
|
+
.modal { background: #1e293b; border: 1px solid #334155; border-radius: 12px; padding: 24px; width: 480px; max-width: 94vw; }
|
|
51
|
+
.modal h3 { font-size: 15px; font-weight: 600; margin-bottom: 16px; }
|
|
52
|
+
.modal label { display: block; font-size: 12px; color: #94a3b8; margin-bottom: 4px; margin-top: 12px; }
|
|
53
|
+
.modal input, .modal select, .modal textarea { width: 100%; background: #0f172a; border: 1px solid #334155; color: #e2e8f0; padding: 8px 12px; border-radius: 6px; font-size: 13px; }
|
|
54
|
+
.modal textarea { resize: vertical; min-height: 80px; }
|
|
55
|
+
.modal .actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px; }
|
|
56
|
+
.id { font-family: monospace; font-size: 11px; color: #64748b; }
|
|
57
|
+
.loading { color: #64748b; font-size: 13px; padding: 24px; text-align: center; }
|
|
58
|
+
</style>
|
|
59
|
+
</head>
|
|
60
|
+
<body>
|
|
61
|
+
|
|
62
|
+
<nav>
|
|
63
|
+
<h1>✉ Emails</h1>
|
|
64
|
+
<button class="active" onclick="showPage('overview')">Overview</button>
|
|
65
|
+
<button onclick="showPage('providers')">Providers</button>
|
|
66
|
+
<button onclick="showPage('domains')">Domains</button>
|
|
67
|
+
<button onclick="showPage('addresses')">Addresses</button>
|
|
68
|
+
<button onclick="showPage('emails')">Emails</button>
|
|
69
|
+
<button onclick="showPage('events')">Events</button>
|
|
70
|
+
</nav>
|
|
71
|
+
|
|
72
|
+
<div class="main">
|
|
73
|
+
|
|
74
|
+
<!-- OVERVIEW -->
|
|
75
|
+
<div id="page-overview" class="page active">
|
|
76
|
+
<h2>Overview</h2>
|
|
77
|
+
<div class="cards" id="stats-cards"><div class="loading">Loading...</div></div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<!-- PROVIDERS -->
|
|
81
|
+
<div id="page-providers" class="page">
|
|
82
|
+
<h2>Providers</h2>
|
|
83
|
+
<div class="toolbar">
|
|
84
|
+
<button class="btn btn-primary" onclick="openModal('modal-provider')">+ Add Provider</button>
|
|
85
|
+
</div>
|
|
86
|
+
<div id="error-providers"></div>
|
|
87
|
+
<div class="panel"><table id="table-providers"><tr><td class="loading">Loading...</td></tr></table></div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<!-- DOMAINS -->
|
|
91
|
+
<div id="page-domains" class="page">
|
|
92
|
+
<h2>Domains</h2>
|
|
93
|
+
<div class="toolbar">
|
|
94
|
+
<button class="btn btn-primary" onclick="openModal('modal-domain')">+ Add Domain</button>
|
|
95
|
+
</div>
|
|
96
|
+
<div id="error-domains"></div>
|
|
97
|
+
<div class="panel"><table id="table-domains"><tr><td class="loading">Loading...</td></tr></table></div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<!-- ADDRESSES -->
|
|
101
|
+
<div id="page-addresses" class="page">
|
|
102
|
+
<h2>Sender Addresses</h2>
|
|
103
|
+
<div class="toolbar">
|
|
104
|
+
<button class="btn btn-primary" onclick="openModal('modal-address')">+ Add Address</button>
|
|
105
|
+
</div>
|
|
106
|
+
<div id="error-addresses"></div>
|
|
107
|
+
<div class="panel"><table id="table-addresses"><tr><td class="loading">Loading...</td></tr></table></div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<!-- EMAILS -->
|
|
111
|
+
<div id="page-emails" class="page">
|
|
112
|
+
<h2>Emails</h2>
|
|
113
|
+
<div class="toolbar">
|
|
114
|
+
<select id="filter-status" onchange="loadEmails()">
|
|
115
|
+
<option value="">All statuses</option>
|
|
116
|
+
<option value="sent">Sent</option>
|
|
117
|
+
<option value="delivered">Delivered</option>
|
|
118
|
+
<option value="bounced">Bounced</option>
|
|
119
|
+
<option value="complained">Complained</option>
|
|
120
|
+
<option value="failed">Failed</option>
|
|
121
|
+
</select>
|
|
122
|
+
</div>
|
|
123
|
+
<div id="error-emails"></div>
|
|
124
|
+
<div class="panel"><table id="table-emails"><tr><td class="loading">Loading...</td></tr></table></div>
|
|
125
|
+
<div class="pagination">
|
|
126
|
+
<button id="btn-prev" onclick="emailsPage('prev')" disabled>← Prev</button>
|
|
127
|
+
<span id="emails-page-info">Page 1</span>
|
|
128
|
+
<button id="btn-next" onclick="emailsPage('next')">→ Next</button>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<!-- EVENTS -->
|
|
133
|
+
<div id="page-events" class="page">
|
|
134
|
+
<h2>Recent Events</h2>
|
|
135
|
+
<div class="toolbar">
|
|
136
|
+
<select id="filter-event-type" onchange="loadEvents()">
|
|
137
|
+
<option value="">All types</option>
|
|
138
|
+
<option value="delivered">Delivered</option>
|
|
139
|
+
<option value="bounced">Bounced</option>
|
|
140
|
+
<option value="complained">Complained</option>
|
|
141
|
+
<option value="opened">Opened</option>
|
|
142
|
+
<option value="clicked">Clicked</option>
|
|
143
|
+
<option value="unsubscribed">Unsubscribed</option>
|
|
144
|
+
</select>
|
|
145
|
+
</div>
|
|
146
|
+
<div id="error-events"></div>
|
|
147
|
+
<div class="panel"><table id="table-events"><tr><td class="loading">Loading...</td></tr></table></div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<!-- MODALS -->
|
|
153
|
+
<div class="modal-bg" id="modal-provider">
|
|
154
|
+
<div class="modal">
|
|
155
|
+
<h3>Add Provider</h3>
|
|
156
|
+
<label>Name</label><input id="prov-name" placeholder="My Resend Account" />
|
|
157
|
+
<label>Type</label>
|
|
158
|
+
<select id="prov-type" onchange="toggleProviderFields()">
|
|
159
|
+
<option value="resend">Resend</option>
|
|
160
|
+
<option value="ses">AWS SES</option>
|
|
161
|
+
</select>
|
|
162
|
+
<div id="prov-resend-fields">
|
|
163
|
+
<label>API Key</label><input id="prov-api-key" type="password" placeholder="re_..." />
|
|
164
|
+
</div>
|
|
165
|
+
<div id="prov-ses-fields" style="display:none">
|
|
166
|
+
<label>Region</label><input id="prov-region" placeholder="us-east-1" />
|
|
167
|
+
<label>Access Key ID</label><input id="prov-access-key" placeholder="AKIA..." />
|
|
168
|
+
<label>Secret Access Key</label><input id="prov-secret-key" type="password" />
|
|
169
|
+
</div>
|
|
170
|
+
<div class="actions">
|
|
171
|
+
<button class="btn" onclick="closeModal('modal-provider')">Cancel</button>
|
|
172
|
+
<button class="btn btn-primary" onclick="addProvider()">Add Provider</button>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div class="modal-bg" id="modal-domain">
|
|
178
|
+
<div class="modal">
|
|
179
|
+
<h3>Add Domain</h3>
|
|
180
|
+
<label>Provider</label><select id="dom-provider"></select>
|
|
181
|
+
<label>Domain</label><input id="dom-name" placeholder="example.com" />
|
|
182
|
+
<div class="actions">
|
|
183
|
+
<button class="btn" onclick="closeModal('modal-domain')">Cancel</button>
|
|
184
|
+
<button class="btn btn-primary" onclick="addDomain()">Add Domain</button>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<div class="modal-bg" id="modal-address">
|
|
190
|
+
<div class="modal">
|
|
191
|
+
<h3>Add Sender Address</h3>
|
|
192
|
+
<label>Provider</label><select id="addr-provider"></select>
|
|
193
|
+
<label>Email Address</label><input id="addr-email" placeholder="noreply@example.com" />
|
|
194
|
+
<label>Display Name (optional)</label><input id="addr-name" placeholder="My App" />
|
|
195
|
+
<div class="actions">
|
|
196
|
+
<button class="btn" onclick="closeModal('modal-address')">Cancel</button>
|
|
197
|
+
<button class="btn btn-primary" onclick="addAddress()">Add Address</button>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<script>
|
|
203
|
+
const API = '';
|
|
204
|
+
|
|
205
|
+
let emailsOffset = 0;
|
|
206
|
+
const emailsLimit = 25;
|
|
207
|
+
|
|
208
|
+
async function api(path, opts = {}) {
|
|
209
|
+
const res = await fetch(API + path, opts);
|
|
210
|
+
if (!res.ok) {
|
|
211
|
+
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
212
|
+
throw new Error(err.error || res.statusText);
|
|
213
|
+
}
|
|
214
|
+
return res.json();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function showPage(name) {
|
|
218
|
+
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
|
219
|
+
document.querySelectorAll('nav button').forEach(b => b.classList.remove('active'));
|
|
220
|
+
document.getElementById('page-' + name).classList.add('active');
|
|
221
|
+
event.target.classList.add('active');
|
|
222
|
+
if (name === 'overview') loadStats();
|
|
223
|
+
if (name === 'providers') loadProviders();
|
|
224
|
+
if (name === 'domains') loadDomains();
|
|
225
|
+
if (name === 'addresses') loadAddresses();
|
|
226
|
+
if (name === 'emails') { emailsOffset = 0; loadEmails(); }
|
|
227
|
+
if (name === 'events') loadEvents();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function openModal(id) {
|
|
231
|
+
document.getElementById(id).classList.add('open');
|
|
232
|
+
if (id === 'modal-domain' || id === 'modal-address') loadProviderSelects();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function closeModal(id) {
|
|
236
|
+
document.getElementById(id).classList.remove('open');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function toggleProviderFields() {
|
|
240
|
+
const type = document.getElementById('prov-type').value;
|
|
241
|
+
document.getElementById('prov-resend-fields').style.display = type === 'resend' ? '' : 'none';
|
|
242
|
+
document.getElementById('prov-ses-fields').style.display = type === 'ses' ? '' : 'none';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function badge(text, type) {
|
|
246
|
+
return `<span class="badge ${type}">${text}</span>`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function fmt(ts) {
|
|
250
|
+
if (!ts) return '-';
|
|
251
|
+
return new Date(ts).toLocaleString();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ─── STATS ───────────────────────────────────────────────────────────────────
|
|
255
|
+
async function loadStats() {
|
|
256
|
+
try {
|
|
257
|
+
const stats = await api('/api/stats?period=30d');
|
|
258
|
+
document.getElementById('stats-cards').innerHTML = `
|
|
259
|
+
<div class="card"><div class="label">Sent</div><div class="value">${stats.sent}</div><div class="sub">Last 30 days</div></div>
|
|
260
|
+
<div class="card"><div class="label">Delivered</div><div class="value">${stats.delivered}</div><div class="sub">${stats.delivery_rate.toFixed(1)}% rate</div></div>
|
|
261
|
+
<div class="card"><div class="label">Bounced</div><div class="value">${stats.bounced}</div><div class="sub">${stats.bounce_rate.toFixed(1)}% rate</div></div>
|
|
262
|
+
<div class="card"><div class="label">Complained</div><div class="value">${stats.complained}</div><div class="sub"> </div></div>
|
|
263
|
+
<div class="card"><div class="label">Opened</div><div class="value">${stats.opened}</div><div class="sub">${stats.open_rate.toFixed(1)}% rate</div></div>
|
|
264
|
+
<div class="card"><div class="label">Clicked</div><div class="value">${stats.clicked}</div><div class="sub"> </div></div>
|
|
265
|
+
`;
|
|
266
|
+
} catch(e) {
|
|
267
|
+
document.getElementById('stats-cards').innerHTML = `<div class="error">${e.message}</div>`;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ─── PROVIDERS ───────────────────────────────────────────────────────────────
|
|
272
|
+
async function loadProviders() {
|
|
273
|
+
try {
|
|
274
|
+
const providers = await api('/api/providers');
|
|
275
|
+
const table = document.getElementById('table-providers');
|
|
276
|
+
if (!providers.length) { table.innerHTML = `<tr><td class="empty">No providers configured.</td></tr>`; return; }
|
|
277
|
+
table.innerHTML = `
|
|
278
|
+
<thead><tr><th>ID</th><th>Name</th><th>Type</th><th>Status</th><th>Created</th><th></th></tr></thead>
|
|
279
|
+
<tbody>${providers.map(p => `
|
|
280
|
+
<tr>
|
|
281
|
+
<td class="id">${p.id.slice(0,8)}</td>
|
|
282
|
+
<td>${p.name}</td>
|
|
283
|
+
<td>${badge(p.type, p.type)}</td>
|
|
284
|
+
<td>${badge(p.active ? 'active' : 'inactive', p.active ? 'verified' : 'pending')}</td>
|
|
285
|
+
<td>${fmt(p.created_at)}</td>
|
|
286
|
+
<td><button class="btn btn-danger btn-sm" onclick="removeProvider('${p.id}','${p.name}')">Remove</button></td>
|
|
287
|
+
</tr>`).join('')}</tbody>`;
|
|
288
|
+
} catch(e) {
|
|
289
|
+
document.getElementById('error-providers').innerHTML = `<div class="error">${e.message}</div>`;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function addProvider() {
|
|
294
|
+
const type = document.getElementById('prov-type').value;
|
|
295
|
+
const body = {
|
|
296
|
+
name: document.getElementById('prov-name').value,
|
|
297
|
+
type,
|
|
298
|
+
api_key: type === 'resend' ? document.getElementById('prov-api-key').value : undefined,
|
|
299
|
+
region: type === 'ses' ? document.getElementById('prov-region').value : undefined,
|
|
300
|
+
access_key: type === 'ses' ? document.getElementById('prov-access-key').value : undefined,
|
|
301
|
+
secret_key: type === 'ses' ? document.getElementById('prov-secret-key').value : undefined,
|
|
302
|
+
};
|
|
303
|
+
try {
|
|
304
|
+
await api('/api/providers', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) });
|
|
305
|
+
closeModal('modal-provider');
|
|
306
|
+
loadProviders();
|
|
307
|
+
} catch(e) { alert(e.message); }
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function removeProvider(id, name) {
|
|
311
|
+
if (!confirm(`Remove provider "${name}"?`)) return;
|
|
312
|
+
try { await api('/api/providers/' + id, { method: 'DELETE' }); loadProviders(); }
|
|
313
|
+
catch(e) { alert(e.message); }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ─── DOMAINS ─────────────────────────────────────────────────────────────────
|
|
317
|
+
async function loadDomains() {
|
|
318
|
+
try {
|
|
319
|
+
const domains = await api('/api/domains');
|
|
320
|
+
const table = document.getElementById('table-domains');
|
|
321
|
+
if (!domains.length) { table.innerHTML = `<tr><td class="empty">No domains configured.</td></tr>`; return; }
|
|
322
|
+
table.innerHTML = `
|
|
323
|
+
<thead><tr><th>ID</th><th>Domain</th><th>DKIM</th><th>SPF</th><th>DMARC</th><th>Verified</th><th></th></tr></thead>
|
|
324
|
+
<tbody>${domains.map(d => `
|
|
325
|
+
<tr>
|
|
326
|
+
<td class="id">${d.id.slice(0,8)}</td>
|
|
327
|
+
<td>${d.domain}</td>
|
|
328
|
+
<td>${badge(d.dkim_status, d.dkim_status)}</td>
|
|
329
|
+
<td>${badge(d.spf_status, d.spf_status)}</td>
|
|
330
|
+
<td>${badge(d.dmarc_status, d.dmarc_status)}</td>
|
|
331
|
+
<td>${d.verified_at ? fmt(d.verified_at) : '-'}</td>
|
|
332
|
+
<td>
|
|
333
|
+
<button class="btn btn-sm" onclick="verifyDomain('${d.id}')" style="border-color:#334155;color:#94a3b8;margin-right:4px">Verify</button>
|
|
334
|
+
<button class="btn btn-danger btn-sm" onclick="removeDomain('${d.id}','${d.domain}')">Remove</button>
|
|
335
|
+
</td>
|
|
336
|
+
</tr>`).join('')}</tbody>`;
|
|
337
|
+
} catch(e) {
|
|
338
|
+
document.getElementById('error-domains').innerHTML = `<div class="error">${e.message}</div>`;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function addDomain() {
|
|
343
|
+
const body = { provider_id: document.getElementById('dom-provider').value, domain: document.getElementById('dom-name').value };
|
|
344
|
+
try {
|
|
345
|
+
await api('/api/domains', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) });
|
|
346
|
+
closeModal('modal-domain');
|
|
347
|
+
loadDomains();
|
|
348
|
+
} catch(e) { alert(e.message); }
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function verifyDomain(id) {
|
|
352
|
+
try { await api('/api/domains/' + id + '/verify', { method: 'POST' }); loadDomains(); }
|
|
353
|
+
catch(e) { alert(e.message); }
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function removeDomain(id, name) {
|
|
357
|
+
if (!confirm(`Remove domain "${name}"?`)) return;
|
|
358
|
+
try { await api('/api/domains/' + id, { method: 'DELETE' }); loadDomains(); }
|
|
359
|
+
catch(e) { alert(e.message); }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ─── ADDRESSES ───────────────────────────────────────────────────────────────
|
|
363
|
+
async function loadAddresses() {
|
|
364
|
+
try {
|
|
365
|
+
const addresses = await api('/api/addresses');
|
|
366
|
+
const table = document.getElementById('table-addresses');
|
|
367
|
+
if (!addresses.length) { table.innerHTML = `<tr><td class="empty">No addresses configured.</td></tr>`; return; }
|
|
368
|
+
table.innerHTML = `
|
|
369
|
+
<thead><tr><th>ID</th><th>Email</th><th>Display Name</th><th>Verified</th><th>Created</th><th></th></tr></thead>
|
|
370
|
+
<tbody>${addresses.map(a => `
|
|
371
|
+
<tr>
|
|
372
|
+
<td class="id">${a.id.slice(0,8)}</td>
|
|
373
|
+
<td>${a.email}</td>
|
|
374
|
+
<td>${a.display_name || '-'}</td>
|
|
375
|
+
<td>${badge(a.verified ? 'verified' : 'unverified', a.verified ? 'verified' : 'pending')}</td>
|
|
376
|
+
<td>${fmt(a.created_at)}</td>
|
|
377
|
+
<td><button class="btn btn-danger btn-sm" onclick="removeAddress('${a.id}','${a.email}')">Remove</button></td>
|
|
378
|
+
</tr>`).join('')}</tbody>`;
|
|
379
|
+
} catch(e) {
|
|
380
|
+
document.getElementById('error-addresses').innerHTML = `<div class="error">${e.message}</div>`;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function addAddress() {
|
|
385
|
+
const body = { provider_id: document.getElementById('addr-provider').value, email: document.getElementById('addr-email').value, display_name: document.getElementById('addr-name').value || undefined };
|
|
386
|
+
try {
|
|
387
|
+
await api('/api/addresses', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) });
|
|
388
|
+
closeModal('modal-address');
|
|
389
|
+
loadAddresses();
|
|
390
|
+
} catch(e) { alert(e.message); }
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function removeAddress(id, email) {
|
|
394
|
+
if (!confirm(`Remove address "${email}"?`)) return;
|
|
395
|
+
try { await api('/api/addresses/' + id, { method: 'DELETE' }); loadAddresses(); }
|
|
396
|
+
catch(e) { alert(e.message); }
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ─── EMAILS ──────────────────────────────────────────────────────────────────
|
|
400
|
+
async function loadEmails() {
|
|
401
|
+
const status = document.getElementById('filter-status').value;
|
|
402
|
+
const params = new URLSearchParams({ limit: emailsLimit, offset: emailsOffset });
|
|
403
|
+
if (status) params.set('status', status);
|
|
404
|
+
try {
|
|
405
|
+
const emails = await api('/api/emails?' + params);
|
|
406
|
+
const table = document.getElementById('table-emails');
|
|
407
|
+
document.getElementById('emails-page-info').textContent = `Page ${Math.floor(emailsOffset / emailsLimit) + 1}`;
|
|
408
|
+
document.getElementById('btn-prev').disabled = emailsOffset === 0;
|
|
409
|
+
document.getElementById('btn-next').disabled = emails.length < emailsLimit;
|
|
410
|
+
|
|
411
|
+
if (!emails.length) { table.innerHTML = `<tr><td class="empty">No emails found.</td></tr>`; return; }
|
|
412
|
+
table.innerHTML = `
|
|
413
|
+
<thead><tr><th>ID</th><th>From</th><th>To</th><th>Subject</th><th>Status</th><th>Sent</th></tr></thead>
|
|
414
|
+
<tbody>${emails.map(e => `
|
|
415
|
+
<tr>
|
|
416
|
+
<td class="id">${e.id.slice(0,8)}</td>
|
|
417
|
+
<td>${e.from_address}</td>
|
|
418
|
+
<td>${(e.to_addresses || []).join(', ')}</td>
|
|
419
|
+
<td>${e.subject}</td>
|
|
420
|
+
<td>${badge(e.status, e.status)}</td>
|
|
421
|
+
<td>${fmt(e.sent_at)}</td>
|
|
422
|
+
</tr>`).join('')}</tbody>`;
|
|
423
|
+
} catch(e) {
|
|
424
|
+
document.getElementById('error-emails').innerHTML = `<div class="error">${e.message}</div>`;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function emailsPage(dir) {
|
|
429
|
+
if (dir === 'next') emailsOffset += emailsLimit;
|
|
430
|
+
else if (dir === 'prev' && emailsOffset > 0) emailsOffset -= emailsLimit;
|
|
431
|
+
loadEmails();
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ─── EVENTS ──────────────────────────────────────────────────────────────────
|
|
435
|
+
async function loadEvents() {
|
|
436
|
+
const type = document.getElementById('filter-event-type').value;
|
|
437
|
+
const params = new URLSearchParams({ limit: 100 });
|
|
438
|
+
if (type) params.set('type', type);
|
|
439
|
+
try {
|
|
440
|
+
const events = await api('/api/events?' + params);
|
|
441
|
+
const table = document.getElementById('table-events');
|
|
442
|
+
if (!events.length) { table.innerHTML = `<tr><td class="empty">No events found.</td></tr>`; return; }
|
|
443
|
+
table.innerHTML = `
|
|
444
|
+
<thead><tr><th>ID</th><th>Type</th><th>Recipient</th><th>Email ID</th><th>Occurred</th></tr></thead>
|
|
445
|
+
<tbody>${events.map(e => `
|
|
446
|
+
<tr>
|
|
447
|
+
<td class="id">${e.id.slice(0,8)}</td>
|
|
448
|
+
<td>${badge(e.type, ['delivered','verified'].includes(e.type) ? 'verified' : ['bounced','complained'].includes(e.type) ? 'bounced' : 'pending')}</td>
|
|
449
|
+
<td>${e.recipient || '-'}</td>
|
|
450
|
+
<td class="id">${e.email_id ? e.email_id.slice(0,8) : '-'}</td>
|
|
451
|
+
<td>${fmt(e.occurred_at)}</td>
|
|
452
|
+
</tr>`).join('')}</tbody>`;
|
|
453
|
+
} catch(e) {
|
|
454
|
+
document.getElementById('error-events').innerHTML = `<div class="error">${e.message}</div>`;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ─── PROVIDER SELECTS ────────────────────────────────────────────────────────
|
|
459
|
+
async function loadProviderSelects() {
|
|
460
|
+
try {
|
|
461
|
+
const providers = await api('/api/providers');
|
|
462
|
+
const options = providers.map(p => `<option value="${p.id}">${p.name} [${p.type}]</option>`).join('');
|
|
463
|
+
['dom-provider','addr-provider'].forEach(id => {
|
|
464
|
+
const el = document.getElementById(id);
|
|
465
|
+
if (el) el.innerHTML = options || '<option value="">No providers</option>';
|
|
466
|
+
});
|
|
467
|
+
} catch {}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ─── Close modal on bg click ─────────────────────────────────────────────────
|
|
471
|
+
document.querySelectorAll('.modal-bg').forEach(bg => {
|
|
472
|
+
bg.addEventListener('click', e => { if (e.target === bg) bg.classList.remove('open'); });
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// Init
|
|
476
|
+
loadStats();
|
|
477
|
+
</script>
|
|
478
|
+
</body>
|
|
479
|
+
</html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.tsx"],"names":[],"mappings":""}
|