mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-22 07:36:33 +01:00
* allow smtp server that does not have authentication * fix: align checkEmailConfig with optional SMTP credentials and add tests Remove EMAIL_USERNAME/EMAIL_PASSWORD requirements from the hasSMTPConfig predicate in checkEmailConfig() so the rest of the codebase (login, startup checks, invite-user) correctly recognizes unauthenticated SMTP as a valid email configuration. Add a warning when only one of the two credential env vars is set, in both sendEmail.js and checkEmailConfig(), to catch partial misconfigurations early. Add test coverage for both the transporter auth assembly in sendEmail.js and the checkEmailConfig predicate in packages/api. Document in .env.example that credentials are optional for unauthenticated SMTP relays. --------- Co-authored-by: Danny Avila <danny@librechat.ai>
143 lines
4.8 KiB
JavaScript
143 lines
4.8 KiB
JavaScript
const nodemailer = require('nodemailer');
|
|
const { readFileAsString } = require('@librechat/api');
|
|
|
|
jest.mock('nodemailer');
|
|
jest.mock('@librechat/data-schemas', () => ({
|
|
logger: { debug: jest.fn(), warn: jest.fn(), error: jest.fn() },
|
|
}));
|
|
jest.mock('@librechat/api', () => ({
|
|
logAxiosError: jest.fn(),
|
|
isEnabled: jest.fn((val) => val === 'true' || val === true),
|
|
readFileAsString: jest.fn(),
|
|
}));
|
|
|
|
const savedEnv = { ...process.env };
|
|
|
|
const mockSendMail = jest.fn().mockResolvedValue({ messageId: 'test-id' });
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
process.env = { ...savedEnv };
|
|
process.env.EMAIL_HOST = 'smtp.example.com';
|
|
process.env.EMAIL_PORT = '587';
|
|
process.env.EMAIL_FROM = 'noreply@example.com';
|
|
process.env.APP_TITLE = 'TestApp';
|
|
delete process.env.EMAIL_USERNAME;
|
|
delete process.env.EMAIL_PASSWORD;
|
|
delete process.env.MAILGUN_API_KEY;
|
|
delete process.env.MAILGUN_DOMAIN;
|
|
delete process.env.EMAIL_SERVICE;
|
|
delete process.env.EMAIL_ENCRYPTION;
|
|
delete process.env.EMAIL_ENCRYPTION_HOSTNAME;
|
|
delete process.env.EMAIL_ALLOW_SELFSIGNED;
|
|
|
|
readFileAsString.mockResolvedValue({ content: '<p>{{name}}</p>' });
|
|
nodemailer.createTransport.mockReturnValue({ sendMail: mockSendMail });
|
|
});
|
|
|
|
afterAll(() => {
|
|
process.env = savedEnv;
|
|
});
|
|
|
|
/** Loads a fresh copy of sendEmail so process.env reads are re-evaluated. */
|
|
function loadSendEmail() {
|
|
jest.resetModules();
|
|
jest.mock('nodemailer', () => ({
|
|
createTransport: jest.fn().mockReturnValue({ sendMail: mockSendMail }),
|
|
}));
|
|
jest.mock('@librechat/data-schemas', () => ({
|
|
logger: { debug: jest.fn(), warn: jest.fn(), error: jest.fn() },
|
|
}));
|
|
jest.mock('@librechat/api', () => ({
|
|
logAxiosError: jest.fn(),
|
|
isEnabled: jest.fn((val) => val === 'true' || val === true),
|
|
readFileAsString: jest.fn().mockResolvedValue({ content: '<p>{{name}}</p>' }),
|
|
}));
|
|
return require('../sendEmail');
|
|
}
|
|
|
|
const baseParams = {
|
|
email: 'user@example.com',
|
|
subject: 'Test',
|
|
payload: { name: 'User' },
|
|
template: 'test.handlebars',
|
|
};
|
|
|
|
describe('sendEmail SMTP auth assembly', () => {
|
|
it('includes auth when both EMAIL_USERNAME and EMAIL_PASSWORD are set', async () => {
|
|
process.env.EMAIL_USERNAME = 'smtp_user';
|
|
process.env.EMAIL_PASSWORD = 'smtp_pass';
|
|
const sendEmail = loadSendEmail();
|
|
const { createTransport } = require('nodemailer');
|
|
|
|
await sendEmail(baseParams);
|
|
|
|
expect(createTransport).toHaveBeenCalledTimes(1);
|
|
const transporterOptions = createTransport.mock.calls[0][0];
|
|
expect(transporterOptions.auth).toEqual({
|
|
user: 'smtp_user',
|
|
pass: 'smtp_pass',
|
|
});
|
|
});
|
|
|
|
it('omits auth when both EMAIL_USERNAME and EMAIL_PASSWORD are absent', async () => {
|
|
const sendEmail = loadSendEmail();
|
|
const { createTransport } = require('nodemailer');
|
|
|
|
await sendEmail(baseParams);
|
|
|
|
expect(createTransport).toHaveBeenCalledTimes(1);
|
|
const transporterOptions = createTransport.mock.calls[0][0];
|
|
expect(transporterOptions.auth).toBeUndefined();
|
|
});
|
|
|
|
it('omits auth and logs a warning when only EMAIL_USERNAME is set', async () => {
|
|
process.env.EMAIL_USERNAME = 'smtp_user';
|
|
const sendEmail = loadSendEmail();
|
|
const { createTransport } = require('nodemailer');
|
|
const { logger: freshLogger } = require('@librechat/data-schemas');
|
|
|
|
await sendEmail(baseParams);
|
|
|
|
const transporterOptions = createTransport.mock.calls[0][0];
|
|
expect(transporterOptions.auth).toBeUndefined();
|
|
expect(freshLogger.warn).toHaveBeenCalledWith(
|
|
expect.stringContaining('EMAIL_USERNAME and EMAIL_PASSWORD must both be set'),
|
|
);
|
|
});
|
|
|
|
it('omits auth and logs a warning when only EMAIL_PASSWORD is set', async () => {
|
|
process.env.EMAIL_PASSWORD = 'smtp_pass';
|
|
const sendEmail = loadSendEmail();
|
|
const { createTransport } = require('nodemailer');
|
|
const { logger: freshLogger } = require('@librechat/data-schemas');
|
|
|
|
await sendEmail(baseParams);
|
|
|
|
const transporterOptions = createTransport.mock.calls[0][0];
|
|
expect(transporterOptions.auth).toBeUndefined();
|
|
expect(freshLogger.warn).toHaveBeenCalledWith(
|
|
expect.stringContaining('EMAIL_USERNAME and EMAIL_PASSWORD must both be set'),
|
|
);
|
|
});
|
|
|
|
it('does not log a warning when both credentials are properly set', async () => {
|
|
process.env.EMAIL_USERNAME = 'smtp_user';
|
|
process.env.EMAIL_PASSWORD = 'smtp_pass';
|
|
const sendEmail = loadSendEmail();
|
|
const { logger: freshLogger } = require('@librechat/data-schemas');
|
|
|
|
await sendEmail(baseParams);
|
|
|
|
expect(freshLogger.warn).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not log a warning when both credentials are absent', async () => {
|
|
const sendEmail = loadSendEmail();
|
|
const { logger: freshLogger } = require('@librechat/data-schemas');
|
|
|
|
await sendEmail(baseParams);
|
|
|
|
expect(freshLogger.warn).not.toHaveBeenCalled();
|
|
});
|
|
});
|