@stamhoofd/email 2.60.0 → 2.61.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,4 +1,5 @@
|
|
|
1
1
|
import { Model } from '@simonbackx/simple-database';
|
|
2
|
+
import { SQLSelect } from '@stamhoofd/sql';
|
|
2
3
|
import { EmailInterfaceRecipient } from '../classes/Email';
|
|
3
4
|
export declare class EmailAddress extends Model {
|
|
4
5
|
static table: string;
|
|
@@ -18,6 +19,14 @@ export declare class EmailAddress extends Model {
|
|
|
18
19
|
static getOrCreate(email: string, organizationId: string | null): Promise<EmailAddress>;
|
|
19
20
|
static getByEmails(emails: string[], organizationId: string | null): Promise<EmailAddress[]>;
|
|
20
21
|
static getByEmail(email: string, organizationId: string | null): Promise<EmailAddress | undefined>;
|
|
22
|
+
/**
|
|
23
|
+
* Search organization wide if this email has been marked as spam or hard bounced
|
|
24
|
+
*/
|
|
25
|
+
static getWhereHardBounceOrSpam(email: string): Promise<EmailAddress | null>;
|
|
21
26
|
static filterSendTo(recipients: EmailInterfaceRecipient[]): Promise<EmailInterfaceRecipient[]>;
|
|
27
|
+
/**
|
|
28
|
+
* Experimental: needs to move to library
|
|
29
|
+
*/
|
|
30
|
+
static select(): SQLSelect<EmailAddress>;
|
|
22
31
|
}
|
|
23
32
|
//# sourceMappingURL=EmailAddress.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EmailAddress.d.ts","sourceRoot":"","sources":["../../src/models/EmailAddress.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,
|
|
1
|
+
{"version":3,"file":"EmailAddress.d.ts","sourceRoot":"","sources":["../../src/models/EmailAddress.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,EAA0B,MAAM,6BAA6B,CAAC;AAE9F,OAAO,EAAO,SAAS,EAAY,MAAM,gBAAgB,CAAC;AAG1D,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAc3D,qBAAa,YAAa,SAAQ,KAAK;IACnC,MAAM,CAAC,KAAK,SAAqB;IAOjC,EAAE,EAAG,MAAM,CAAC;IAGZ,cAAc,EAAE,MAAM,GAAG,IAAI,CAAQ;IAIrC,KAAK,EAAE,MAAM,CAAC;IAGd,YAAY,UAAS;IAGrB,UAAU,UAAS;IAGnB,qBAAqB,UAAS;IAG9B,eAAe,UAAS;IAGxB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAErB;;OAEG;IAQH,SAAS,EAAE,IAAI,CAAC;IAShB,SAAS,EAAE,IAAI,CAAC;WAEH,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,YAAY,CAAC;WAoBhF,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;WA6BrF,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC;IAIxG;;OAEG;WACU,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;WAUrE,YAAY,CAAC,UAAU,EAAE,uBAAuB,EAAE,GAAG,OAAO,CAAC,uBAAuB,EAAE,CAAC;IAmBpG;;OAEG;IACH,MAAM,CAAC,MAAM;CAchB"}
|
|
@@ -4,6 +4,7 @@ exports.EmailAddress = void 0;
|
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const simple_database_1 = require("@simonbackx/simple-database");
|
|
6
6
|
const queues_1 = require("@stamhoofd/queues");
|
|
7
|
+
const sql_1 = require("@stamhoofd/sql");
|
|
7
8
|
const crypto_1 = tslib_1.__importDefault(require("crypto"));
|
|
8
9
|
const uuid_1 = require("uuid");
|
|
9
10
|
async function randomBytes(size) {
|
|
@@ -18,7 +19,7 @@ async function randomBytes(size) {
|
|
|
18
19
|
});
|
|
19
20
|
}
|
|
20
21
|
class EmailAddress extends simple_database_1.Model {
|
|
21
|
-
static table =
|
|
22
|
+
static table = 'email_addresses';
|
|
22
23
|
id;
|
|
23
24
|
organizationId = null;
|
|
24
25
|
// Columns
|
|
@@ -35,7 +36,7 @@ class EmailAddress extends simple_database_1.Model {
|
|
|
35
36
|
updatedAt;
|
|
36
37
|
static async getOrCreate(email, organizationId) {
|
|
37
38
|
// Prevent race conditions when checking the same email address at the same time, and creating a new one
|
|
38
|
-
return await queues_1.QueueHandler.schedule(
|
|
39
|
+
return await queues_1.QueueHandler.schedule('email-address/create-' + email + '-' + organizationId, async () => {
|
|
39
40
|
const existing = await this.getByEmail(email, organizationId);
|
|
40
41
|
if (existing) {
|
|
41
42
|
return existing;
|
|
@@ -43,7 +44,7 @@ class EmailAddress extends simple_database_1.Model {
|
|
|
43
44
|
const n = new EmailAddress();
|
|
44
45
|
n.organizationId = organizationId;
|
|
45
46
|
n.email = email;
|
|
46
|
-
n.token = (await randomBytes(64)).toString(
|
|
47
|
+
n.token = (await randomBytes(64)).toString('base64').toUpperCase();
|
|
47
48
|
await n.save();
|
|
48
49
|
return n;
|
|
49
50
|
});
|
|
@@ -69,65 +70,86 @@ class EmailAddress extends simple_database_1.Model {
|
|
|
69
70
|
static async getByEmail(email, organizationId) {
|
|
70
71
|
return (await this.where({ email, organizationId }, { limit: 1 }))[0];
|
|
71
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Search organization wide if this email has been marked as spam or hard bounced
|
|
75
|
+
*/
|
|
76
|
+
static async getWhereHardBounceOrSpam(email) {
|
|
77
|
+
return await this.select().where('email', email).where(sql_1.SQL.where('hardBounce', 1)
|
|
78
|
+
.or('markedAsSpam', 1)).first(false);
|
|
79
|
+
}
|
|
72
80
|
// Methods
|
|
73
81
|
static async filterSendTo(recipients) {
|
|
74
|
-
if (recipients.length
|
|
82
|
+
if (recipients.length === 0) {
|
|
75
83
|
return [];
|
|
76
84
|
}
|
|
77
85
|
const emails = recipients.map(r => r.email);
|
|
78
86
|
const [results] = await simple_database_1.Database.select(`SELECT email FROM ${this.table} WHERE \`email\` IN (?) AND (\`hardBounce\` = 1 OR \`markedAsSpam\` = 1)`, [emails]);
|
|
79
87
|
const remove = results.map(r => r[this.table]['email']);
|
|
80
|
-
if (remove.length
|
|
88
|
+
if (remove.length === 0) {
|
|
81
89
|
return recipients;
|
|
82
90
|
}
|
|
83
91
|
return recipients.filter(r => !remove.includes(r.email));
|
|
84
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Experimental: needs to move to library
|
|
95
|
+
*/
|
|
96
|
+
static select() {
|
|
97
|
+
const transformer = (row) => {
|
|
98
|
+
const d = this.fromRow(row[this.table]);
|
|
99
|
+
if (!d) {
|
|
100
|
+
throw new Error('EmailTemplate not found');
|
|
101
|
+
}
|
|
102
|
+
return d;
|
|
103
|
+
};
|
|
104
|
+
const select = new sql_1.SQLSelect(transformer, sql_1.SQL.wildcard());
|
|
105
|
+
return select.from(sql_1.SQL.table(this.table));
|
|
106
|
+
}
|
|
85
107
|
}
|
|
86
108
|
exports.EmailAddress = EmailAddress;
|
|
87
109
|
tslib_1.__decorate([
|
|
88
110
|
(0, simple_database_1.column)({
|
|
89
|
-
primary: true, type:
|
|
111
|
+
primary: true, type: 'string', beforeSave(value) {
|
|
90
112
|
return value ?? (0, uuid_1.v4)();
|
|
91
|
-
}
|
|
113
|
+
},
|
|
92
114
|
})
|
|
93
115
|
], EmailAddress.prototype, "id", void 0);
|
|
94
116
|
tslib_1.__decorate([
|
|
95
|
-
(0, simple_database_1.column)({ type:
|
|
117
|
+
(0, simple_database_1.column)({ type: 'string', nullable: true })
|
|
96
118
|
], EmailAddress.prototype, "organizationId", void 0);
|
|
97
119
|
tslib_1.__decorate([
|
|
98
|
-
(0, simple_database_1.column)({ type:
|
|
120
|
+
(0, simple_database_1.column)({ type: 'string' })
|
|
99
121
|
], EmailAddress.prototype, "email", void 0);
|
|
100
122
|
tslib_1.__decorate([
|
|
101
|
-
(0, simple_database_1.column)({ type:
|
|
123
|
+
(0, simple_database_1.column)({ type: 'boolean' })
|
|
102
124
|
], EmailAddress.prototype, "markedAsSpam", void 0);
|
|
103
125
|
tslib_1.__decorate([
|
|
104
|
-
(0, simple_database_1.column)({ type:
|
|
126
|
+
(0, simple_database_1.column)({ type: 'boolean' })
|
|
105
127
|
], EmailAddress.prototype, "hardBounce", void 0);
|
|
106
128
|
tslib_1.__decorate([
|
|
107
|
-
(0, simple_database_1.column)({ type:
|
|
129
|
+
(0, simple_database_1.column)({ type: 'boolean' })
|
|
108
130
|
], EmailAddress.prototype, "unsubscribedMarketing", void 0);
|
|
109
131
|
tslib_1.__decorate([
|
|
110
|
-
(0, simple_database_1.column)({ type:
|
|
132
|
+
(0, simple_database_1.column)({ type: 'boolean' })
|
|
111
133
|
], EmailAddress.prototype, "unsubscribedAll", void 0);
|
|
112
134
|
tslib_1.__decorate([
|
|
113
|
-
(0, simple_database_1.column)({ type:
|
|
135
|
+
(0, simple_database_1.column)({ type: 'string', nullable: true })
|
|
114
136
|
], EmailAddress.prototype, "token", void 0);
|
|
115
137
|
tslib_1.__decorate([
|
|
116
138
|
(0, simple_database_1.column)({
|
|
117
|
-
type:
|
|
139
|
+
type: 'datetime', beforeSave() {
|
|
118
140
|
const date = new Date();
|
|
119
141
|
date.setMilliseconds(0);
|
|
120
142
|
return date;
|
|
121
|
-
}
|
|
143
|
+
},
|
|
122
144
|
})
|
|
123
145
|
], EmailAddress.prototype, "createdAt", void 0);
|
|
124
146
|
tslib_1.__decorate([
|
|
125
147
|
(0, simple_database_1.column)({
|
|
126
|
-
type:
|
|
148
|
+
type: 'datetime', beforeSave() {
|
|
127
149
|
const date = new Date();
|
|
128
150
|
date.setMilliseconds(0);
|
|
129
151
|
return date;
|
|
130
|
-
}
|
|
152
|
+
},
|
|
131
153
|
})
|
|
132
154
|
], EmailAddress.prototype, "updatedAt", void 0);
|
|
133
155
|
//# sourceMappingURL=EmailAddress.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EmailAddress.js","sourceRoot":"","sources":["../../src/models/EmailAddress.ts"],"names":[],"mappings":";;;;AAAA,
|
|
1
|
+
{"version":3,"file":"EmailAddress.js","sourceRoot":"","sources":["../../src/models/EmailAddress.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/email",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.61.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"license": "UNLICENCED",
|
|
@@ -25,5 +25,5 @@
|
|
|
25
25
|
"publishConfig": {
|
|
26
26
|
"access": "public"
|
|
27
27
|
},
|
|
28
|
-
"gitHead": "
|
|
28
|
+
"gitHead": "3df41cac9090d5e99abdbc51e4a4a02e9ea1784a"
|
|
29
29
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { column, Database, Model } from '@simonbackx/simple-database';
|
|
1
|
+
import { column, Database, Model, SQLResultNamespacedRow } from '@simonbackx/simple-database';
|
|
2
2
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import { SQL, SQLSelect, SQLWhere } from '@stamhoofd/sql';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
5
6
|
import { EmailInterfaceRecipient } from '../classes/Email';
|
|
6
7
|
|
|
7
8
|
async function randomBytes(size: number): Promise<Buffer> {
|
|
@@ -17,74 +18,74 @@ async function randomBytes(size: number): Promise<Buffer> {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export class EmailAddress extends Model {
|
|
20
|
-
static table =
|
|
21
|
+
static table = 'email_addresses';
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
primary: true, type:
|
|
23
|
+
@column({
|
|
24
|
+
primary: true, type: 'string', beforeSave(value) {
|
|
24
25
|
return value ?? uuidv4();
|
|
25
|
-
}
|
|
26
|
+
},
|
|
26
27
|
})
|
|
27
28
|
id!: string;
|
|
28
29
|
|
|
29
|
-
@column({ type:
|
|
30
|
+
@column({ type: 'string', nullable: true })
|
|
30
31
|
organizationId: string | null = null;
|
|
31
32
|
|
|
32
33
|
// Columns
|
|
33
|
-
@column({ type:
|
|
34
|
+
@column({ type: 'string' })
|
|
34
35
|
email: string;
|
|
35
36
|
|
|
36
|
-
@column({ type:
|
|
37
|
+
@column({ type: 'boolean' })
|
|
37
38
|
markedAsSpam = false;
|
|
38
39
|
|
|
39
|
-
@column({ type:
|
|
40
|
+
@column({ type: 'boolean' })
|
|
40
41
|
hardBounce = false;
|
|
41
42
|
|
|
42
|
-
@column({ type:
|
|
43
|
+
@column({ type: 'boolean' })
|
|
43
44
|
unsubscribedMarketing = false;
|
|
44
45
|
|
|
45
|
-
@column({ type:
|
|
46
|
+
@column({ type: 'boolean' })
|
|
46
47
|
unsubscribedAll = false;
|
|
47
48
|
|
|
48
|
-
@column({ type:
|
|
49
|
+
@column({ type: 'string', nullable: true })
|
|
49
50
|
token: string | null;
|
|
50
51
|
|
|
51
52
|
/**
|
|
52
53
|
* createdAt behaves more like createdAt for Challenge. Since every save is considered to have a new challenge
|
|
53
54
|
*/
|
|
54
55
|
@column({
|
|
55
|
-
type:
|
|
56
|
-
const date = new Date()
|
|
57
|
-
date.setMilliseconds(0)
|
|
58
|
-
return date
|
|
59
|
-
}
|
|
56
|
+
type: 'datetime', beforeSave() {
|
|
57
|
+
const date = new Date();
|
|
58
|
+
date.setMilliseconds(0);
|
|
59
|
+
return date;
|
|
60
|
+
},
|
|
60
61
|
})
|
|
61
|
-
createdAt: Date
|
|
62
|
+
createdAt: Date;
|
|
62
63
|
|
|
63
64
|
@column({
|
|
64
|
-
type:
|
|
65
|
-
const date = new Date()
|
|
66
|
-
date.setMilliseconds(0)
|
|
67
|
-
return date
|
|
68
|
-
}
|
|
65
|
+
type: 'datetime', beforeSave() {
|
|
66
|
+
const date = new Date();
|
|
67
|
+
date.setMilliseconds(0);
|
|
68
|
+
return date;
|
|
69
|
+
},
|
|
69
70
|
})
|
|
70
|
-
updatedAt: Date
|
|
71
|
+
updatedAt: Date;
|
|
71
72
|
|
|
72
73
|
static async getOrCreate(email: string, organizationId: string | null): Promise<EmailAddress> {
|
|
73
74
|
// Prevent race conditions when checking the same email address at the same time, and creating a new one
|
|
74
|
-
return await QueueHandler.schedule(
|
|
75
|
-
const existing = await this.getByEmail(email, organizationId)
|
|
75
|
+
return await QueueHandler.schedule('email-address/create-' + email + '-' + organizationId, async () => {
|
|
76
|
+
const existing = await this.getByEmail(email, organizationId);
|
|
76
77
|
if (existing) {
|
|
77
|
-
return existing
|
|
78
|
+
return existing;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
const n = new EmailAddress()
|
|
81
|
-
n.organizationId = organizationId
|
|
82
|
-
n.email = email
|
|
83
|
-
n.token = (await randomBytes(64)).toString(
|
|
81
|
+
const n = new EmailAddress();
|
|
82
|
+
n.organizationId = organizationId;
|
|
83
|
+
n.email = email;
|
|
84
|
+
n.token = (await randomBytes(64)).toString('base64').toUpperCase();
|
|
84
85
|
|
|
85
|
-
await n.save()
|
|
86
|
+
await n.save();
|
|
86
87
|
|
|
87
|
-
return n
|
|
88
|
+
return n;
|
|
88
89
|
});
|
|
89
90
|
}
|
|
90
91
|
|
|
@@ -92,18 +93,18 @@ export class EmailAddress extends Model {
|
|
|
92
93
|
static async getByEmails(emails: string[], organizationId: string | null): Promise<EmailAddress[]> {
|
|
93
94
|
if (emails.length > 30) {
|
|
94
95
|
// Normally an organization will never have so much bounces, so we'll request all emails and filter in them
|
|
95
|
-
const all = await this.where({ organizationId }, { limit: 1000 })
|
|
96
|
-
return all.filter(e => emails.includes(e.email))
|
|
96
|
+
const all = await this.where({ organizationId }, { limit: 1000 });
|
|
97
|
+
return all.filter(e => emails.includes(e.email));
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
if (emails.length == 0) {
|
|
100
|
-
return []
|
|
101
|
+
return [];
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
if (organizationId === null) {
|
|
104
105
|
const [rows] = await Database.select(
|
|
105
106
|
`SELECT ${this.getDefaultSelect()} FROM ${this.table} WHERE \`email\` IN (?) AND \`organizationId\` is NULL`,
|
|
106
|
-
[emails]
|
|
107
|
+
[emails],
|
|
107
108
|
);
|
|
108
109
|
|
|
109
110
|
return this.fromRows(rows, this.table);
|
|
@@ -111,7 +112,7 @@ export class EmailAddress extends Model {
|
|
|
111
112
|
|
|
112
113
|
const [rows] = await Database.select(
|
|
113
114
|
`SELECT ${this.getDefaultSelect()} FROM ${this.table} WHERE \`email\` IN (?) AND \`organizationId\` = ?`,
|
|
114
|
-
[emails, organizationId]
|
|
115
|
+
[emails, organizationId],
|
|
115
116
|
);
|
|
116
117
|
|
|
117
118
|
return this.fromRows(rows, this.table);
|
|
@@ -119,26 +120,56 @@ export class EmailAddress extends Model {
|
|
|
119
120
|
|
|
120
121
|
// Methods
|
|
121
122
|
static async getByEmail(email: string, organizationId: string | null): Promise<EmailAddress | undefined> {
|
|
122
|
-
return (await this.where({ email, organizationId }, { limit: 1 }))[0]
|
|
123
|
+
return (await this.where({ email, organizationId }, { limit: 1 }))[0];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Search organization wide if this email has been marked as spam or hard bounced
|
|
128
|
+
*/
|
|
129
|
+
static async getWhereHardBounceOrSpam(email: string): Promise<EmailAddress | null> {
|
|
130
|
+
return await this.select().where(
|
|
131
|
+
'email', email,
|
|
132
|
+
).where(
|
|
133
|
+
SQL.where('hardBounce', 1)
|
|
134
|
+
.or('markedAsSpam', 1),
|
|
135
|
+
).first(false);
|
|
123
136
|
}
|
|
124
137
|
|
|
125
138
|
// Methods
|
|
126
139
|
static async filterSendTo(recipients: EmailInterfaceRecipient[]): Promise<EmailInterfaceRecipient[]> {
|
|
127
|
-
if (recipients.length
|
|
128
|
-
return []
|
|
140
|
+
if (recipients.length === 0) {
|
|
141
|
+
return [];
|
|
129
142
|
}
|
|
130
143
|
|
|
131
|
-
const emails = recipients.map(r => r.email)
|
|
144
|
+
const emails = recipients.map(r => r.email);
|
|
132
145
|
const [results] = await Database.select(
|
|
133
146
|
`SELECT email FROM ${this.table} WHERE \`email\` IN (?) AND (\`hardBounce\` = 1 OR \`markedAsSpam\` = 1)`,
|
|
134
|
-
[emails]
|
|
147
|
+
[emails],
|
|
135
148
|
);
|
|
136
149
|
|
|
137
|
-
const remove = results.map(r => r[this.table]['email'])
|
|
138
|
-
if (remove.length
|
|
139
|
-
return recipients
|
|
150
|
+
const remove = results.map(r => r[this.table]['email']);
|
|
151
|
+
if (remove.length === 0) {
|
|
152
|
+
return recipients;
|
|
140
153
|
}
|
|
141
154
|
|
|
142
|
-
return recipients.filter(r => !remove.includes(r.email))
|
|
155
|
+
return recipients.filter(r => !remove.includes(r.email));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Experimental: needs to move to library
|
|
160
|
+
*/
|
|
161
|
+
static select() {
|
|
162
|
+
const transformer = (row: SQLResultNamespacedRow): EmailAddress => {
|
|
163
|
+
const d = (this as typeof EmailAddress & typeof Model).fromRow(row[this.table] as any) as EmailAddress | undefined;
|
|
164
|
+
|
|
165
|
+
if (!d) {
|
|
166
|
+
throw new Error('EmailTemplate not found');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return d;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const select = new SQLSelect(transformer, SQL.wildcard());
|
|
173
|
+
return select.from(SQL.table(this.table));
|
|
143
174
|
}
|
|
144
175
|
}
|