@stamhoofd/email 2.118.1 → 2.120.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +0,0 @@
1
- set foreign_key_checks=0;
2
- ALTER TABLE `email_addresses` CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
3
- ALTER TABLE `email_addresses` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
4
- ALTER DATABASE CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
5
- set foreign_key_checks=1;
@@ -1,160 +0,0 @@
1
- import { column, Database } from '@simonbackx/simple-database';
2
- import { QueueHandler } from '@stamhoofd/queues';
3
- import { QueryableModel, SQL } from '@stamhoofd/sql';
4
- import crypto from 'crypto';
5
- import { v4 as uuidv4 } from 'uuid';
6
- import { EmailInterfaceRecipient } from '../classes/Email.js';
7
-
8
- async function randomBytes(size: number): Promise<Buffer> {
9
- return new Promise((resolve, reject) => {
10
- crypto.randomBytes(size, (err: Error | null, buf: Buffer) => {
11
- if (err) {
12
- reject(err);
13
- return;
14
- }
15
- resolve(buf);
16
- });
17
- });
18
- }
19
-
20
- export class EmailAddress extends QueryableModel {
21
- static table = 'email_addresses';
22
-
23
- @column({
24
- primary: true, type: 'string', beforeSave(value) {
25
- return value ?? uuidv4();
26
- },
27
- })
28
- id!: string;
29
-
30
- @column({ type: 'string', nullable: true })
31
- organizationId: string | null = null;
32
-
33
- // Columns
34
- @column({ type: 'string' })
35
- email: string;
36
-
37
- @column({ type: 'boolean' })
38
- markedAsSpam = false;
39
-
40
- @column({ type: 'boolean' })
41
- hardBounce = false;
42
-
43
- @column({ type: 'boolean' })
44
- unsubscribedMarketing = false;
45
-
46
- @column({ type: 'boolean' })
47
- unsubscribedAll = false;
48
-
49
- @column({ type: 'string', nullable: true })
50
- token: string | null;
51
-
52
- /**
53
- * createdAt behaves more like createdAt for Challenge. Since every save is considered to have a new challenge
54
- */
55
- @column({
56
- type: 'datetime', beforeSave() {
57
- const date = new Date();
58
- date.setMilliseconds(0);
59
- return date;
60
- },
61
- })
62
- createdAt: Date;
63
-
64
- @column({
65
- type: 'datetime', beforeSave() {
66
- const date = new Date();
67
- date.setMilliseconds(0);
68
- return date;
69
- },
70
- })
71
- updatedAt: Date;
72
-
73
- static async getOrCreate(email: string, organizationId: string | null): Promise<EmailAddress> {
74
- // Prevent race conditions when checking the same email address at the same time, and creating a new one
75
- return await QueueHandler.schedule('email-address/create-' + email + '-' + organizationId, async () => {
76
- const existing = await this.getByEmail(email, organizationId);
77
- if (existing) {
78
- return existing;
79
- }
80
-
81
- const n = new EmailAddress();
82
- n.organizationId = organizationId;
83
- n.email = email;
84
- n.token = (await randomBytes(64)).toString('base64').toUpperCase();
85
-
86
- await n.save();
87
-
88
- return n;
89
- });
90
- }
91
-
92
- // Methods
93
- static async getByEmails(emails: string[], organizationId: string | null): Promise<EmailAddress[]> {
94
- if (emails.length > 1000) {
95
- // Normally an organization will never have so much bounces, so we'll request all emails and filter in them
96
- const all = await this.where({ organizationId }, { limit: 1000 });
97
- return all.filter(e => emails.includes(e.email));
98
- }
99
-
100
- if (emails.length === 0) {
101
- return [];
102
- }
103
-
104
- const query = EmailAddress.select().where(
105
- 'email', emails,
106
- );
107
- if (organizationId) {
108
- query.andWhere(
109
- SQL.where('organizationId', organizationId)
110
- .or('organizationId', null),
111
- );
112
- }
113
- else {
114
- query.andWhere(
115
- SQL.where('organizationId', null)
116
- .or('hardBounce', 1)
117
- .or('markedAsSpam', 1),
118
- );
119
- }
120
-
121
- return query.fetch();
122
- }
123
-
124
- // Methods
125
- static async getByEmail(email: string, organizationId: string | null): Promise<EmailAddress | undefined> {
126
- return (await this.where({ email, organizationId }, { limit: 1 }))[0];
127
- }
128
-
129
- /**
130
- * Search organization wide if this email has been marked as spam or hard bounced
131
- */
132
- static async getWhereHardBounceOrSpam(email: string): Promise<EmailAddress | null> {
133
- return await this.select().where(
134
- 'email', email,
135
- ).where(
136
- SQL.where('hardBounce', 1)
137
- .or('markedAsSpam', 1),
138
- ).first(false);
139
- }
140
-
141
- // Methods
142
- static async filterSendTo(recipients: EmailInterfaceRecipient[]): Promise<EmailInterfaceRecipient[]> {
143
- if (recipients.length === 0) {
144
- return [];
145
- }
146
-
147
- const emails = recipients.map(r => r.email);
148
- const [results] = await Database.select(
149
- `SELECT email FROM ${this.table} WHERE \`email\` IN (?) AND (\`hardBounce\` = 1 OR \`markedAsSpam\` = 1)`,
150
- [emails],
151
- );
152
-
153
- const remove = results.map(r => r[this.table]['email']);
154
- if (remove.length === 0) {
155
- return recipients;
156
- }
157
-
158
- return recipients.filter(r => !remove.includes(r.email));
159
- }
160
- }