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':