1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-29 19:13:59 +02:00

Server: Allow setting email key to prevent the same email to be sent multiple times

This commit is contained in:
Laurent Cozic 2021-08-09 16:55:04 +01:00
parent b3cec3be37
commit 391204c31e
5 changed files with 82 additions and 1 deletions

Binary file not shown.

View File

@ -394,6 +394,7 @@ export interface Email extends WithDates {
sent_time?: number;
sent_success?: number;
error?: string;
key?: string;
}
export interface Token extends WithDates {
@ -549,6 +550,7 @@ export const databaseSchema: DatabaseTables = {
error: { type: 'string' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
key: { type: 'string' },
},
tokens: {
id: { type: 'number' },

View File

@ -0,0 +1,16 @@
import { Knex } from 'knex';
import { DbConnection } from '../db';
export async function up(db: DbConnection): Promise<any> {
await db.schema.alterTable('emails', function(table: Knex.CreateTableBuilder) {
table.text('key', 'mediumtext').defaultTo('').notNullable();
});
await db.schema.alterTable('emails', function(table: Knex.CreateTableBuilder) {
table.unique(['recipient_email', 'key']);
});
}
export async function down(_db: DbConnection): Promise<any> {
}

View File

@ -0,0 +1,48 @@
import { EmailSender } from '../db';
import { beforeAllDb, afterAllTests, beforeEachDb, models, createUserAndSession } from '../utils/testing/testUtils';
import paymentFailedTemplate from '../views/emails/paymentFailedTemplate';
describe('EmailModel', function() {
beforeAll(async () => {
await beforeAllDb('EmailModel');
});
afterAll(async () => {
await afterAllTests();
});
beforeEach(async () => {
await beforeEachDb();
});
test('should not send the same keyed email twice', async function() {
const { user } = await createUserAndSession();
const sendEmail = async (key: string) => {
await models().email().push({
...paymentFailedTemplate(),
recipient_email: user.email,
recipient_id: user.id,
recipient_name: user.full_name || '',
sender_id: EmailSender.Support,
key: key,
});
};
const beforeCount = (await models().email().all()).length;
await sendEmail('payment_failed_1');
expect((await models().email().all()).length).toBe(beforeCount + 1);
await sendEmail('payment_failed_1');
expect((await models().email().all()).length).toBe(beforeCount + 1);
await sendEmail('payment_failed_2');
expect((await models().email().all()).length).toBe(beforeCount + 2);
});
});

View File

@ -6,6 +6,7 @@ export interface EmailToSend {
recipient_email: string;
subject: string;
body: string;
key?: string;
recipient_name?: string;
recipient_id?: Uuid;
@ -26,12 +27,26 @@ export default class EmailModel extends BaseModel<Email> {
return false;
}
public async push(email: EmailToSend) {
public async push(email: EmailToSend): Promise<Email | null> {
if (email.key) {
const existingEmail = await this.byRecipientAndKey(email.recipient_email, email.key);
if (existingEmail) return null; // noop - the email has already been sent
}
const output = await super.save({ ...email });
EmailModel.eventEmitter.emit('queued');
return output;
}
private async byRecipientAndKey(recipientEmail: string, key: string): Promise<Email> {
if (!key) throw new Error('Key cannot be empty');
return this.db(this.tableName)
.where('recipient_email', '=', recipientEmail)
.where('key', '=', key)
.first();
}
public async needToBeSent(): Promise<Email[]> {
return this.db(this.tableName).where('sent_time', '=', 0);
}