From c3e608d82fc8d7459ca02865700ba99283cf348b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 3 May 2026 20:20:06 +0000
Subject: [PATCH] fix: add separate sender/recipient email fields for SMTP
notification channel
The email (SMTP) form previously used the `username` field as both SMTP
auth credential and the from/to address. This breaks services like
Postmark that use API tokens as SMTP credentials.
Changes:
- Added "Sender Email (From)" (optional) and "Recipient Email (To)"
(required) fields to the email channel configuration
- Renamed "Username / Email" to "SMTP Username" for clarity
- Updated `buildAppriseUrl` to build `mailto://user:pass@host/?to=...&from=...`
so recipient and sender are explicit query parameters
- Updated tests to match new field labels and added two new email URL tests
Agent-Logs-Url: https://github.com/christianlouis/InboxConverge/sessions/5be072bb-0824-436b-983b-1a6bb6df2699
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
---
.../components/NotificationWizard.test.tsx | 70 ++++++++++++++++++-
.../src/components/NotificationWizard.tsx | 30 ++++++--
2 files changed, 95 insertions(+), 5 deletions(-)
diff --git a/frontend/src/components/NotificationWizard.test.tsx b/frontend/src/components/NotificationWizard.test.tsx
index 7c00d78..c12be7c 100644
--- a/frontend/src/components/NotificationWizard.test.tsx
+++ b/frontend/src/components/NotificationWizard.test.tsx
@@ -79,10 +79,12 @@ describe('NotificationWizard', () => {
it('should show Email fields', () => {
goToStep2('Email');
- expect(screen.getByText('Username / Email')).toBeInTheDocument();
+ expect(screen.getByText('SMTP Username')).toBeInTheDocument();
expect(screen.getByText('SMTP Password')).toBeInTheDocument();
expect(screen.getByText('SMTP Host')).toBeInTheDocument();
expect(screen.getByText('SMTP Port')).toBeInTheDocument();
+ expect(screen.getByText('Sender Email (From)')).toBeInTheDocument();
+ expect(screen.getByText('Recipient Email (To)')).toBeInTheDocument();
});
it('should show Webhook fields', () => {
@@ -294,6 +296,72 @@ describe('NotificationWizard', () => {
});
expect(url).toBe('tgram://mybot/mychat/');
});
+
+ it('should build correct Email URL with recipient only', () => {
+ render();
+ fireEvent.click(screen.getByText('Email'));
+
+ fireEvent.change(screen.getByPlaceholderText('user@example.com or API key'), {
+ target: { value: 'apikey' },
+ });
+ fireEvent.change(document.querySelector('input[type="password"]')!, {
+ target: { value: 'secret' },
+ });
+ fireEvent.change(screen.getByPlaceholderText('smtp.example.com'), {
+ target: { value: 'smtp.postmarkapp.com' },
+ });
+ fireEvent.change(screen.getByPlaceholderText('587'), {
+ target: { value: '587' },
+ });
+ // sender left blank (optional)
+ fireEvent.change(screen.getByPlaceholderText('you@example.com'), {
+ target: { value: 'me@example.com' },
+ });
+
+ fireEvent.click(screen.getByText('Next').closest('button')!);
+ fireEvent.change(screen.getByPlaceholderText('e.g. My Telegram Alert'), {
+ target: { value: 'Postmark' },
+ });
+ fireEvent.click(screen.getByText('Save Channel').closest('button')!);
+
+ const url = mockOnComplete.mock.calls[0][0].apprise_url as string;
+ expect(url).toContain('mailto://apikey:secret@smtp.postmarkapp.com:587/');
+ expect(url).toContain('to=me%40example.com');
+ expect(url).not.toContain('from=');
+ });
+
+ it('should build correct Email URL with both sender and recipient', () => {
+ render();
+ fireEvent.click(screen.getByText('Email'));
+
+ fireEvent.change(screen.getByPlaceholderText('user@example.com or API key'), {
+ target: { value: 'apikey' },
+ });
+ fireEvent.change(document.querySelector('input[type="password"]')!, {
+ target: { value: 'secret' },
+ });
+ fireEvent.change(screen.getByPlaceholderText('smtp.example.com'), {
+ target: { value: 'smtp.postmarkapp.com' },
+ });
+ // port left blank (optional)
+ fireEvent.change(screen.getByPlaceholderText('noreply@example.com'), {
+ target: { value: 'noreply@example.com' },
+ });
+ fireEvent.change(screen.getByPlaceholderText('you@example.com'), {
+ target: { value: 'me@example.com' },
+ });
+
+ fireEvent.click(screen.getByText('Next').closest('button')!);
+ fireEvent.change(screen.getByPlaceholderText('e.g. My Telegram Alert'), {
+ target: { value: 'Postmark' },
+ });
+ fireEvent.click(screen.getByText('Save Channel').closest('button')!);
+
+ const url = mockOnComplete.mock.calls[0][0].apprise_url as string;
+ expect(url).toContain('mailto://apikey:secret@smtp.postmarkapp.com/');
+ expect(url).toContain('to=me%40example.com');
+ expect(url).toContain('from=noreply%40example.com');
+ });
});
// ── Edit mode (initialData) ────────────────────────────────────────────
diff --git a/frontend/src/components/NotificationWizard.tsx b/frontend/src/components/NotificationWizard.tsx
index 1a7e17c..d3789fe 100644
--- a/frontend/src/components/NotificationWizard.tsx
+++ b/frontend/src/components/NotificationWizard.tsx
@@ -71,10 +71,28 @@ const CHANNEL_FIELDS: Record = {
},
],
email: [
- { key: 'username', label: 'Username / Email', placeholder: 'user@example.com' },
+ {
+ key: 'username',
+ label: 'SMTP Username',
+ placeholder: 'user@example.com or API key',
+ hint: 'SMTP authentication username — can be an API key (e.g. Postmark)',
+ },
{ key: 'password', label: 'SMTP Password', placeholder: '••••••••', type: 'password' },
{ key: 'host', label: 'SMTP Host', placeholder: 'smtp.example.com' },
{ key: 'port', label: 'SMTP Port', placeholder: '587', optional: true },
+ {
+ key: 'sender',
+ label: 'Sender Email (From)',
+ placeholder: 'noreply@example.com',
+ hint: 'Email address shown as the sender. Defaults to SMTP Username if blank.',
+ optional: true,
+ },
+ {
+ key: 'recipient',
+ label: 'Recipient Email (To)',
+ placeholder: 'you@example.com',
+ hint: 'Email address where notifications are delivered.',
+ },
],
webhook: [
{
@@ -113,11 +131,15 @@ function buildAppriseUrl(channel: string, fields: Record): strin
if (match) return `slack://${match[1]}/${match[2]}/${match[3]}/`;
return '';
}
- case 'email':
- if (!fields.username || !fields.password || !fields.host) return '';
+ case 'email': {
+ if (!fields.username || !fields.password || !fields.host || !fields.recipient) return '';
+ const params = new URLSearchParams();
+ params.set('to', fields.recipient);
+ if (fields.sender) params.set('from', fields.sender);
return `mailto://${encodeURIComponent(fields.username)}:${encodeURIComponent(fields.password)}@${fields.host}${
fields.port ? `:${fields.port}` : ''
- }`;
+ }/?${params.toString()}`;
+ }
case 'webhook':
return fields.url || '';
case 'custom':