mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
feat(email): configurable reply-to address (#3140)
This commit is contained in:
@@ -4,6 +4,7 @@ export interface EmailConfig {
|
||||
enabled: boolean;
|
||||
fromEmail: string;
|
||||
fromName: string;
|
||||
replyTo?: string | null;
|
||||
appBaseUrl: string;
|
||||
marketingBaseUrl: string;
|
||||
}
|
||||
@@ -14,6 +15,7 @@ export interface EmailMessage {
|
||||
email: string;
|
||||
name: string;
|
||||
};
|
||||
replyTo?: string;
|
||||
subject: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {EmailI18nService} from '@pkgs/email/src/EmailI18nService';
|
||||
import type {EmailConfig, EmailMessage, IEmailProvider} from '@pkgs/email/src/EmailProviderTypes';
|
||||
import {EmailService} from '@pkgs/email/src/EmailService';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
const CONFIG: EmailConfig = {
|
||||
enabled: true,
|
||||
fromEmail: '[email protected]',
|
||||
fromName: 'Fluxer',
|
||||
appBaseUrl: 'https://example.com',
|
||||
marketingBaseUrl: 'https://example.com',
|
||||
};
|
||||
|
||||
async function sendWith(config: EmailConfig): Promise<EmailMessage> {
|
||||
const sent: Array<EmailMessage> = [];
|
||||
const provider: IEmailProvider = {
|
||||
sendEmail: async (message) => {
|
||||
sent.push(message);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
const service = new EmailService(config, new EmailI18nService(), provider);
|
||||
await expect(service.sendRegistrationApprovedEmail('[email protected]', 'testuser', 'en-US')).resolves.toBe(true);
|
||||
expect(sent).toHaveLength(1);
|
||||
return sent[0];
|
||||
}
|
||||
|
||||
describe('EmailService reply-to', () => {
|
||||
it('sets the configured reply-to address on every message', async () => {
|
||||
const message = await sendWith({...CONFIG, replyTo: '[email protected]'});
|
||||
expect(message.replyTo).toBe('[email protected]');
|
||||
expect(message.from).toEqual({email: '[email protected]', name: 'Fluxer'});
|
||||
});
|
||||
|
||||
it.each([undefined, null, ''])('omits the reply-to address when it is %j', async (replyTo) => {
|
||||
const message = await sendWith({...CONFIG, replyTo});
|
||||
expect(message).not.toHaveProperty('replyTo');
|
||||
});
|
||||
});
|
||||
@@ -375,6 +375,7 @@ export class EmailService implements IEmailService {
|
||||
return this.provider.sendEmail({
|
||||
to: email,
|
||||
from: {email: this.config.fromEmail, name: this.config.fromName},
|
||||
...(this.config.replyTo ? {replyTo: this.config.replyTo} : {}),
|
||||
subject,
|
||||
text: body,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {SmtpEmailProvider} from '@pkgs/email/src/SmtpEmailProvider';
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
const {sendMail} = vi.hoisted(() => ({sendMail: vi.fn()}));
|
||||
|
||||
vi.mock('nodemailer', () => ({
|
||||
default: {createTransport: () => ({sendMail, verify: vi.fn()})},
|
||||
}));
|
||||
|
||||
const MESSAGE = {
|
||||
to: '[email protected]',
|
||||
from: {email: '[email protected]', name: 'Fluxer'},
|
||||
subject: 'Subject',
|
||||
text: 'Body',
|
||||
};
|
||||
|
||||
function createProvider(): SmtpEmailProvider {
|
||||
return new SmtpEmailProvider({host: 'smtp.example.com', port: 587, username: 'user', password: 'pass'});
|
||||
}
|
||||
|
||||
describe('SmtpEmailProvider', () => {
|
||||
beforeEach(() => {
|
||||
sendMail.mockReset();
|
||||
sendMail.mockResolvedValue({});
|
||||
});
|
||||
|
||||
it('passes the reply-to address to nodemailer', async () => {
|
||||
await expect(createProvider().sendEmail({...MESSAGE, replyTo: '[email protected]'})).resolves.toBe(true);
|
||||
expect(sendMail).toHaveBeenCalledWith({
|
||||
to: '[email protected]',
|
||||
from: 'Fluxer <[email protected]>',
|
||||
replyTo: '[email protected]',
|
||||
subject: 'Subject',
|
||||
text: 'Body',
|
||||
});
|
||||
});
|
||||
|
||||
it('omits the reply-to address when the message has none', async () => {
|
||||
await expect(createProvider().sendEmail(MESSAGE)).resolves.toBe(true);
|
||||
expect(sendMail.mock.calls[0][0]).not.toHaveProperty('replyTo');
|
||||
});
|
||||
});
|
||||
@@ -45,6 +45,7 @@ export class SmtpEmailProvider implements IEmailProvider {
|
||||
await this.transporter.sendMail({
|
||||
to: message.to,
|
||||
from: `${message.from.name} <${message.from.email}>`,
|
||||
...(message.replyTo ? {replyTo: message.replyTo} : {}),
|
||||
subject: message.subject,
|
||||
text: message.text,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user