@contember/engine-tenant-api 1.4.0-rc.1 → 1.4.0-rc.3
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.
- package/dist/src/TenantContainer.d.ts +3 -0
- package/dist/src/TenantContainer.d.ts.map +1 -1
- package/dist/src/TenantContainer.js +2 -0
- package/dist/src/TenantContainer.js.map +1 -1
- package/dist/src/migrations/2024-06-20-140000-mail-reply.d.ts +3 -0
- package/dist/src/migrations/2024-06-20-140000-mail-reply.d.ts.map +1 -0
- package/dist/src/migrations/2024-06-20-140000-mail-reply.js +11 -0
- package/dist/src/migrations/2024-06-20-140000-mail-reply.js.map +1 -0
- package/dist/src/migrations/runner.d.ts.map +1 -1
- package/dist/src/migrations/runner.js +2 -0
- package/dist/src/migrations/runner.js.map +1 -1
- package/dist/src/migrations/snapshot.d.ts.map +1 -1
- package/dist/src/migrations/snapshot.js +2 -1
- package/dist/src/migrations/snapshot.js.map +1 -1
- package/dist/src/model/authorization/PermissionActions.d.ts +5 -0
- package/dist/src/model/authorization/PermissionActions.d.ts.map +1 -1
- package/dist/src/model/authorization/PermissionActions.js +1 -0
- package/dist/src/model/authorization/PermissionActions.js.map +1 -1
- package/dist/src/model/authorization/PermissionsFactory.d.ts.map +1 -1
- package/dist/src/model/authorization/PermissionsFactory.js +1 -0
- package/dist/src/model/authorization/PermissionsFactory.js.map +1 -1
- package/dist/src/model/commands/mailTemplate/AddMailTemplateCommand.d.ts.map +1 -1
- package/dist/src/model/commands/mailTemplate/AddMailTemplateCommand.js +3 -2
- package/dist/src/model/commands/mailTemplate/AddMailTemplateCommand.js.map +1 -1
- package/dist/src/model/commands/mailTemplate/RemoveMailTemplateCommand.js +1 -2
- package/dist/src/model/commands/mailTemplate/RemoveMailTemplateCommand.js.map +1 -1
- package/dist/src/model/mailing/UserMailer.d.ts +1 -1
- package/dist/src/model/mailing/UserMailer.d.ts.map +1 -1
- package/dist/src/model/mailing/UserMailer.js +8 -2
- package/dist/src/model/mailing/UserMailer.js.map +1 -1
- package/dist/src/model/mailing/type.d.ts +5 -1
- package/dist/src/model/mailing/type.d.ts.map +1 -1
- package/dist/src/model/mailing/type.js +17 -1
- package/dist/src/model/mailing/type.js.map +1 -1
- package/dist/src/model/queries/mailTemplate/MailTemplateListQuery.d.ts +9 -0
- package/dist/src/model/queries/mailTemplate/MailTemplateListQuery.d.ts.map +1 -0
- package/dist/src/model/queries/mailTemplate/MailTemplateListQuery.js +15 -0
- package/dist/src/model/queries/mailTemplate/MailTemplateListQuery.js.map +1 -0
- package/dist/src/model/queries/mailTemplate/MailTemplateQuery.d.ts +4 -10
- package/dist/src/model/queries/mailTemplate/MailTemplateQuery.d.ts.map +1 -1
- package/dist/src/model/queries/mailTemplate/MailTemplateQuery.js +3 -8
- package/dist/src/model/queries/mailTemplate/MailTemplateQuery.js.map +1 -1
- package/dist/src/model/queries/mailTemplate/helpers.d.ts +7 -0
- package/dist/src/model/queries/mailTemplate/helpers.d.ts.map +1 -0
- package/dist/src/model/queries/mailTemplate/helpers.js +16 -0
- package/dist/src/model/queries/mailTemplate/helpers.js.map +1 -0
- package/dist/src/model/queries/mailTemplate/index.d.ts +1 -0
- package/dist/src/model/queries/mailTemplate/index.d.ts.map +1 -1
- package/dist/src/model/queries/mailTemplate/index.js +1 -0
- package/dist/src/model/queries/mailTemplate/index.js.map +1 -1
- package/dist/src/model/service/PasswordResetManager.js +2 -1
- package/dist/src/model/service/PasswordResetManager.js.map +1 -1
- package/dist/src/resolvers/ResolverFactory.d.ts +3 -1
- package/dist/src/resolvers/ResolverFactory.d.ts.map +1 -1
- package/dist/src/resolvers/ResolverFactory.js +1 -0
- package/dist/src/resolvers/ResolverFactory.js.map +1 -1
- package/dist/src/resolvers/mutation/mailTemplates/MailTemplateMutationResolver.d.ts +1 -2
- package/dist/src/resolvers/mutation/mailTemplates/MailTemplateMutationResolver.d.ts.map +1 -1
- package/dist/src/resolvers/mutation/mailTemplates/MailTemplateMutationResolver.js +12 -12
- package/dist/src/resolvers/mutation/mailTemplates/MailTemplateMutationResolver.js.map +1 -1
- package/dist/src/resolvers/query/MailTemplateQueryResolver.d.ts +9 -0
- package/dist/src/resolvers/query/MailTemplateQueryResolver.d.ts.map +1 -0
- package/dist/src/resolvers/query/MailTemplateQueryResolver.js +22 -0
- package/dist/src/resolvers/query/MailTemplateQueryResolver.js.map +1 -0
- package/dist/src/schema/index.d.ts +27 -1
- package/dist/src/schema/index.d.ts.map +1 -1
- package/dist/src/schema/tenant.graphql.d.ts.map +1 -1
- package/dist/src/schema/tenant.graphql.js +14 -1
- package/dist/src/schema/tenant.graphql.js.map +1 -1
- package/dist/src/tsconfig.tsbuildinfo +1 -1
- package/dist/tests/cases/integration/mocked/sql/getMailTemplateSql.js +2 -2
- package/dist/tests/tsconfig.tsbuildinfo +1 -1
- package/package.json +12 -12
- package/src/TenantContainer.ts +3 -0
- package/src/migrations/2024-06-20-140000-mail-reply.ts +10 -0
- package/src/migrations/runner.ts +2 -0
- package/src/migrations/snapshot.ts +2 -1
- package/src/model/authorization/PermissionActions.ts +1 -0
- package/src/model/authorization/PermissionsFactory.ts +1 -0
- package/src/model/commands/mailTemplate/AddMailTemplateCommand.ts +3 -1
- package/src/model/commands/mailTemplate/RemoveMailTemplateCommand.ts +1 -1
- package/src/model/mailing/UserMailer.ts +11 -5
- package/src/model/mailing/type.ts +20 -1
- package/src/model/queries/mailTemplate/MailTemplateListQuery.ts +16 -0
- package/src/model/queries/mailTemplate/MailTemplateQuery.ts +7 -20
- package/src/model/queries/mailTemplate/helpers.ts +20 -0
- package/src/model/queries/mailTemplate/index.ts +1 -0
- package/src/model/service/PasswordResetManager.ts +1 -1
- package/src/resolvers/ResolverFactory.ts +11 -5
- package/src/resolvers/mutation/mailTemplates/MailTemplateMutationResolver.ts +11 -17
- package/src/resolvers/query/MailTemplateQueryResolver.ts +23 -0
- package/src/schema/index.ts +29 -0
- package/src/schema/tenant.graphql.ts +14 -1
- package/tests/cases/integration/mocked/sql/getMailTemplateSql.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contember/engine-tenant-api",
|
|
3
|
-
"version": "1.4.0-rc.
|
|
3
|
+
"version": "1.4.0-rc.3",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"typings": "dist/src/index.d.ts",
|
|
@@ -13,16 +13,16 @@
|
|
|
13
13
|
"test": "vitest --dir ./tests/cases --no-threads"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@contember/authorization": "1.4.0-rc.
|
|
17
|
-
"@contember/config-loader": "1.4.0-rc.
|
|
18
|
-
"@contember/database": "1.4.0-rc.
|
|
19
|
-
"@contember/database-migrations": "1.4.0-rc.
|
|
20
|
-
"@contember/dic": "1.4.0-rc.
|
|
21
|
-
"@contember/graphql-utils": "1.4.0-rc.
|
|
22
|
-
"@contember/logger": "1.4.0-rc.
|
|
23
|
-
"@contember/schema": "1.4.0-rc.
|
|
24
|
-
"@contember/schema-utils": "1.4.0-rc.
|
|
25
|
-
"@contember/typesafe": "1.4.0-rc.
|
|
16
|
+
"@contember/authorization": "1.4.0-rc.3",
|
|
17
|
+
"@contember/config-loader": "1.4.0-rc.3",
|
|
18
|
+
"@contember/database": "1.4.0-rc.3",
|
|
19
|
+
"@contember/database-migrations": "1.4.0-rc.3",
|
|
20
|
+
"@contember/dic": "1.4.0-rc.3",
|
|
21
|
+
"@contember/graphql-utils": "1.4.0-rc.3",
|
|
22
|
+
"@contember/logger": "1.4.0-rc.3",
|
|
23
|
+
"@contember/schema": "1.4.0-rc.3",
|
|
24
|
+
"@contember/schema-utils": "1.4.0-rc.3",
|
|
25
|
+
"@contember/typesafe": "1.4.0-rc.3",
|
|
26
26
|
"@graphql-tools/utils": "^10.0.8",
|
|
27
27
|
"graphql-tag": "^2.12.6",
|
|
28
28
|
"jose": "^5.1.1",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"otpauth": "^9.1.5"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@contember/database-tester": "1.4.0-rc.
|
|
35
|
+
"@contember/database-tester": "1.4.0-rc.3",
|
|
36
36
|
"@graphql-codegen/cli": "^5.0.0",
|
|
37
37
|
"@graphql-codegen/typescript": "^4.0.1",
|
|
38
38
|
"@graphql-codegen/typescript-operations": "^4.0.1",
|
package/src/TenantContainer.ts
CHANGED
|
@@ -78,6 +78,7 @@ import { IDPQueryResolver } from './resolvers/query/IDPQueryResolver'
|
|
|
78
78
|
import { UpdateIDPMutationResolver } from './resolvers/mutation/idp/UpdateIDPMutationResolver'
|
|
79
79
|
import { TenantCredentials, TenantMigrationsRunner } from './migrations'
|
|
80
80
|
import { DisablePersonMutationResolver } from './resolvers/mutation/person/DisablePersonMutationResolver'
|
|
81
|
+
import { MailTemplateQueryResolver } from './resolvers/query/MailTemplateQueryResolver'
|
|
81
82
|
|
|
82
83
|
export interface TenantContainer {
|
|
83
84
|
projectMemberManager: ProjectMemberManager
|
|
@@ -217,6 +218,8 @@ export class TenantContainerFactory {
|
|
|
217
218
|
new ProjectQueryResolver(projectManager))
|
|
218
219
|
.addService('projectMembersQueryResolver', ({ projectManager, projectMemberManager }) =>
|
|
219
220
|
new ProjectMembersQueryResolver(projectManager, projectMemberManager))
|
|
221
|
+
.addService('mailTemplateQueryResolver', ({ mailTemplateManager }) =>
|
|
222
|
+
new MailTemplateQueryResolver(mailTemplateManager))
|
|
220
223
|
.addService('signUpMutationResolver', ({ signUpManager, apiKeyManager }) =>
|
|
221
224
|
new SignUpMutationResolver(signUpManager, apiKeyManager))
|
|
222
225
|
.addService('signInMutationResolver', ({ signInManager, signInResponseFactory }) =>
|
package/src/migrations/runner.ts
CHANGED
|
@@ -36,6 +36,7 @@ import _20221108projectupdatenotification from './2022-11-08-project-update-noti
|
|
|
36
36
|
import _20230120135500personemailnormalize from './2023-01-20-135500-person-email-normalize'
|
|
37
37
|
import _20230603104000persondisabled from './2023-06-03-104000-person-disabled'
|
|
38
38
|
import _20230901165000idpreturnconfig from './2023-09-01-165000-idp-return-config'
|
|
39
|
+
import _20240620140000mailreply from './2024-06-20-140000-mail-reply'
|
|
39
40
|
|
|
40
41
|
import snapshot from './snapshot'
|
|
41
42
|
import { computeTokenHash, Providers } from '../model'
|
|
@@ -85,6 +86,7 @@ const migrations = {
|
|
|
85
86
|
'2023-01-20-135500-person-email-normalize': _20230120135500personemailnormalize,
|
|
86
87
|
'2023-06-03-104000-person-disabled': _20230603104000persondisabled,
|
|
87
88
|
'2023-09-01-165000-idp-return-config': _20230901165000idpreturnconfig,
|
|
89
|
+
'2024-06-20-140000-mail-reply': _20240620140000mailreply,
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
export class TenantMigrationsRunner {
|
|
@@ -64,7 +64,8 @@ CREATE TABLE "mail_template" (
|
|
|
64
64
|
"variant" "text" NOT NULL,
|
|
65
65
|
"subject" "text" NOT NULL,
|
|
66
66
|
"content" "text" NOT NULL,
|
|
67
|
-
"use_layout" boolean NOT NULL
|
|
67
|
+
"use_layout" boolean NOT NULL,
|
|
68
|
+
"reply_to" "text"
|
|
68
69
|
);
|
|
69
70
|
CREATE TABLE "person" (
|
|
70
71
|
"id" "uuid" NOT NULL,
|
|
@@ -54,6 +54,7 @@ namespace PermissionActions {
|
|
|
54
54
|
|
|
55
55
|
export const MAIL_TEMPLATE_ADD = Authorizator.createAction(Resources.mailTemplate, 'add')
|
|
56
56
|
export const MAIL_TEMPLATE_REMOVE = Authorizator.createAction(Resources.mailTemplate, 'remove')
|
|
57
|
+
export const MAIL_TEMPLATE_LIST = Authorizator.createAction(Resources.mailTemplate, 'list')
|
|
57
58
|
|
|
58
59
|
export const IDP_ADD = Authorizator.createAction(Resources.idp, 'add')
|
|
59
60
|
export const IDP_UPDATE = Authorizator.createAction(Resources.idp, 'update')
|
|
@@ -54,6 +54,7 @@ class PermissionsFactory {
|
|
|
54
54
|
|
|
55
55
|
permissions.allow(TenantRole.PROJECT_ADMIN, PermissionActions.MAIL_TEMPLATE_ADD)
|
|
56
56
|
permissions.allow(TenantRole.PROJECT_ADMIN, PermissionActions.MAIL_TEMPLATE_REMOVE)
|
|
57
|
+
permissions.allow(TenantRole.PROJECT_ADMIN, PermissionActions.MAIL_TEMPLATE_LIST)
|
|
57
58
|
|
|
58
59
|
permissions.allow(TenantRole.PROJECT_ADMIN, PermissionActions.IDP_ADD)
|
|
59
60
|
permissions.allow(TenantRole.PROJECT_ADMIN, PermissionActions.IDP_UPDATE)
|
|
@@ -10,12 +10,13 @@ export class AddMailTemplateCommand implements Command<void> {
|
|
|
10
10
|
.into('mail_template')
|
|
11
11
|
.values({
|
|
12
12
|
id: providers.uuid(),
|
|
13
|
-
project_id: this.mailTemplate.projectId
|
|
13
|
+
project_id: this.mailTemplate.projectId,
|
|
14
14
|
mail_type: this.mailTemplate.type,
|
|
15
15
|
variant: this.mailTemplate.variant,
|
|
16
16
|
subject: this.mailTemplate.subject,
|
|
17
17
|
content: this.mailTemplate.content,
|
|
18
18
|
use_layout: this.mailTemplate.useLayout,
|
|
19
|
+
reply_to: this.mailTemplate.replyTo,
|
|
19
20
|
})
|
|
20
21
|
.onConflict(ConflictActionType.update, this.mailTemplate.projectId ? {
|
|
21
22
|
columns: ['project_id', 'mail_type', 'variant'],
|
|
@@ -24,6 +25,7 @@ export class AddMailTemplateCommand implements Command<void> {
|
|
|
24
25
|
columns: ['mail_type', 'variant'],
|
|
25
26
|
where: it => it.isNull('project_id'),
|
|
26
27
|
}, {
|
|
28
|
+
reply_to: expr => expr.select(['excluded', 'reply_to']),
|
|
27
29
|
subject: expr => expr.select(['excluded', 'subject']),
|
|
28
30
|
content: expr => expr.select(['excluded', 'content']),
|
|
29
31
|
use_layout: expr => expr.select(['excluded', 'use_layout']),
|
|
@@ -10,7 +10,7 @@ export class RemoveMailTemplateCommand implements Command<boolean> {
|
|
|
10
10
|
(await DeleteBuilder.create()
|
|
11
11
|
.from('mail_template')
|
|
12
12
|
.where({
|
|
13
|
-
project_id: this.mailTemplateIdentifier.projectId
|
|
13
|
+
project_id: this.mailTemplateIdentifier.projectId,
|
|
14
14
|
mail_type: this.mailTemplateIdentifier.type,
|
|
15
15
|
variant: this.mailTemplateIdentifier.variant,
|
|
16
16
|
})
|
|
@@ -21,6 +21,7 @@ export class UserMailer {
|
|
|
21
21
|
const template = (await this.getCustomTemplate(dbContext, { type: MailType.newUserInvited, ...customMailOptions })) || {
|
|
22
22
|
subject: 'You have been invited to {{project}}',
|
|
23
23
|
content: NewUserInvited,
|
|
24
|
+
replyTo: null,
|
|
24
25
|
}
|
|
25
26
|
await this.sendTemplate(template, mailArguments)
|
|
26
27
|
}
|
|
@@ -33,6 +34,7 @@ export class UserMailer {
|
|
|
33
34
|
const template = (await this.getCustomTemplate(dbContext, { type: MailType.existingUserInvited, ...customMailOptions })) || {
|
|
34
35
|
subject: 'You have been invited to {{project}}',
|
|
35
36
|
content: ExistingUserInvited,
|
|
37
|
+
replyTo: null,
|
|
36
38
|
}
|
|
37
39
|
await this.sendTemplate(template, mailArguments)
|
|
38
40
|
}
|
|
@@ -40,17 +42,18 @@ export class UserMailer {
|
|
|
40
42
|
async sendPasswordResetEmail(
|
|
41
43
|
dbContext: DatabaseContext,
|
|
42
44
|
mailArguments: { email: string; token: string; project?: string; projectSlug?: string },
|
|
43
|
-
customMailOptions: { projectId
|
|
45
|
+
customMailOptions: { projectId: string | null; variant: string },
|
|
44
46
|
): Promise<void> {
|
|
45
47
|
const template = (await this.getCustomTemplate(dbContext, { type: MailType.passwordReset, ...customMailOptions })) || {
|
|
46
48
|
subject: 'Password reset',
|
|
47
49
|
content: PasswordReset,
|
|
50
|
+
replyTo: null,
|
|
48
51
|
}
|
|
49
52
|
await this.sendTemplate(template, mailArguments)
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
private async sendTemplate(
|
|
53
|
-
template: Pick<MailTemplateData, 'subject' | 'content'>,
|
|
56
|
+
template: Pick<MailTemplateData, 'subject' | 'content' | 'replyTo'>,
|
|
54
57
|
mailArguments: Record<string, any>,
|
|
55
58
|
) {
|
|
56
59
|
const html = await this.templateRenderer.render(template.content, mailArguments)
|
|
@@ -58,21 +61,24 @@ export class UserMailer {
|
|
|
58
61
|
to: mailArguments.email,
|
|
59
62
|
subject: await this.templateRenderer.render(template.subject, mailArguments),
|
|
60
63
|
html,
|
|
64
|
+
...(template.replyTo ? {
|
|
65
|
+
replyTo: template.replyTo,
|
|
66
|
+
} : {}),
|
|
61
67
|
})
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
private async getCustomTemplate(
|
|
65
71
|
dbContext: DatabaseContext,
|
|
66
72
|
identifier: MailTemplateIdentifier,
|
|
67
|
-
): Promise<Pick<MailTemplateData, 'subject' | 'content'> | null> {
|
|
73
|
+
): Promise<Pick<MailTemplateData, 'subject' | 'content' | 'replyTo'> | null> {
|
|
68
74
|
const customTemplate =
|
|
69
75
|
(await dbContext.queryHandler.fetch(new MailTemplateQuery(identifier)))
|
|
70
|
-
?? (await dbContext.queryHandler.fetch(new MailTemplateQuery({ ...identifier, projectId:
|
|
76
|
+
?? (await dbContext.queryHandler.fetch(new MailTemplateQuery({ ...identifier, projectId: null })))
|
|
71
77
|
|
|
72
78
|
if (!customTemplate) {
|
|
73
79
|
return null
|
|
74
80
|
}
|
|
75
81
|
const content = customTemplate.useLayout ? Layout(customTemplate.content) : customTemplate.content
|
|
76
|
-
return { content, subject: customTemplate.subject }
|
|
82
|
+
return { content, subject: customTemplate.subject, replyTo: customTemplate.replyTo }
|
|
77
83
|
}
|
|
78
84
|
}
|
|
@@ -1,11 +1,29 @@
|
|
|
1
|
+
import { MailType as SchemaMailType } from '../../schema'
|
|
2
|
+
|
|
1
3
|
export enum MailType {
|
|
2
4
|
existingUserInvited = 'existingUserInvited',
|
|
3
5
|
newUserInvited = 'newUserInvited',
|
|
4
6
|
passwordReset = 'passwordReset',
|
|
5
7
|
}
|
|
6
8
|
|
|
9
|
+
export const mailTypeFromSchemaToDb = (type: SchemaMailType): MailType => {
|
|
10
|
+
return {
|
|
11
|
+
EXISTING_USER_INVITED: MailType.existingUserInvited,
|
|
12
|
+
NEW_USER_INVITED: MailType.newUserInvited,
|
|
13
|
+
RESET_PASSWORD_REQUEST: MailType.passwordReset,
|
|
14
|
+
}[type]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const mailTypeFromDbToSchema = (type: MailType): SchemaMailType => {
|
|
18
|
+
return {
|
|
19
|
+
[MailType.existingUserInvited]: 'EXISTING_USER_INVITED' as const,
|
|
20
|
+
[MailType.newUserInvited]: 'NEW_USER_INVITED' as const,
|
|
21
|
+
[MailType.passwordReset]: 'RESET_PASSWORD_REQUEST' as const,
|
|
22
|
+
}[type]
|
|
23
|
+
}
|
|
24
|
+
|
|
7
25
|
export interface MailTemplateIdentifier {
|
|
8
|
-
projectId
|
|
26
|
+
projectId: string | null
|
|
9
27
|
type: MailType
|
|
10
28
|
variant: string
|
|
11
29
|
}
|
|
@@ -14,6 +32,7 @@ export interface MailTemplateData {
|
|
|
14
32
|
subject: string
|
|
15
33
|
content: string
|
|
16
34
|
useLayout: boolean
|
|
35
|
+
replyTo: string | null
|
|
17
36
|
}
|
|
18
37
|
|
|
19
38
|
export interface MailTemplate extends MailTemplateIdentifier, MailTemplateData {}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { DatabaseQuery, DatabaseQueryable } from '@contember/database'
|
|
2
|
+
import { createMailTemplateQuery, MailTemplateRow } from './helpers'
|
|
3
|
+
|
|
4
|
+
export type MailTemplateListRow =
|
|
5
|
+
& MailTemplateRow
|
|
6
|
+
& {
|
|
7
|
+
projectSlug: string | null
|
|
8
|
+
}
|
|
9
|
+
export class MailTemplateListQuery extends DatabaseQuery<MailTemplateListRow[]> {
|
|
10
|
+
async fetch({ db }: DatabaseQueryable): Promise<MailTemplateListRow[]> {
|
|
11
|
+
return await createMailTemplateQuery<MailTemplateListRow>()
|
|
12
|
+
.leftJoin('project', undefined, expr => expr.columnsEq(['project', 'id'], ['mail_template', 'project_id']))
|
|
13
|
+
.select(['project', 'slug'], 'projectSlug')
|
|
14
|
+
.getResult(db)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -1,22 +1,18 @@
|
|
|
1
|
-
import { DatabaseQuery, DatabaseQueryable
|
|
2
|
-
import {
|
|
1
|
+
import { DatabaseQuery, DatabaseQueryable } from '@contember/database'
|
|
2
|
+
import { MailTemplateIdentifier } from '../../mailing'
|
|
3
|
+
import { createMailTemplateQuery, MailTemplateRow } from './helpers'
|
|
3
4
|
|
|
4
|
-
class MailTemplateQuery extends DatabaseQuery<
|
|
5
|
+
export class MailTemplateQuery extends DatabaseQuery<MailTemplateRow | null> {
|
|
5
6
|
constructor(
|
|
6
7
|
private mailTemplateIdentifier: MailTemplateIdentifier,
|
|
7
8
|
) {
|
|
8
9
|
super()
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
async fetch({ db }: DatabaseQueryable): Promise<
|
|
12
|
-
const rows = await
|
|
13
|
-
.select('id')
|
|
14
|
-
.select('subject')
|
|
15
|
-
.select('content')
|
|
16
|
-
.select('use_layout', 'useLayout')
|
|
17
|
-
.from('mail_template')
|
|
12
|
+
async fetch({ db }: DatabaseQueryable): Promise<MailTemplateRow | null> {
|
|
13
|
+
const rows = await createMailTemplateQuery()
|
|
18
14
|
.where({
|
|
19
|
-
project_id: this.mailTemplateIdentifier.projectId
|
|
15
|
+
project_id: this.mailTemplateIdentifier.projectId,
|
|
20
16
|
mail_type: this.mailTemplateIdentifier.type,
|
|
21
17
|
variant: this.mailTemplateIdentifier.variant,
|
|
22
18
|
})
|
|
@@ -25,12 +21,3 @@ class MailTemplateQuery extends DatabaseQuery<MailTemplateQuery.Result> {
|
|
|
25
21
|
return this.fetchOneOrNull(rows)
|
|
26
22
|
}
|
|
27
23
|
}
|
|
28
|
-
|
|
29
|
-
namespace MailTemplateQuery {
|
|
30
|
-
export type Row = {
|
|
31
|
-
readonly id: string
|
|
32
|
-
} & MailTemplateData
|
|
33
|
-
export type Result = null | Row
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export { MailTemplateQuery }
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { SelectBuilder } from '@contember/database'
|
|
2
|
+
import { MailTemplateData, MailTemplateIdentifier } from '../../mailing'
|
|
3
|
+
|
|
4
|
+
export type MailTemplateRow =
|
|
5
|
+
& {
|
|
6
|
+
readonly id: string
|
|
7
|
+
}
|
|
8
|
+
& MailTemplateData
|
|
9
|
+
& MailTemplateIdentifier
|
|
10
|
+
|
|
11
|
+
export const createMailTemplateQuery = <T extends MailTemplateRow>() => SelectBuilder.create<T>()
|
|
12
|
+
.select(['mail_template', 'id'])
|
|
13
|
+
.select('subject')
|
|
14
|
+
.select('content')
|
|
15
|
+
.select('use_layout', 'useLayout')
|
|
16
|
+
.select('reply_to', 'replyTo')
|
|
17
|
+
.select('project_id', 'projectId')
|
|
18
|
+
.select('mail_type', 'type')
|
|
19
|
+
.select('variant')
|
|
20
|
+
.from('mail_template')
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AddIDPMutationResolver,
|
|
2
3
|
AddProjectMemberMutationResolver,
|
|
3
|
-
ChangeProfileMutationResolver,
|
|
4
4
|
ChangePasswordMutationResolver,
|
|
5
|
+
ChangeProfileMutationResolver,
|
|
5
6
|
CreateApiKeyMutationResolver,
|
|
6
7
|
CreateProjectMutationResolver,
|
|
7
|
-
DisableApiKeyMutationResolver,
|
|
8
|
+
DisableApiKeyMutationResolver,
|
|
9
|
+
DisableIDPMutationResolver,
|
|
10
|
+
EnableIDPMutationResolver,
|
|
11
|
+
IdentityGlobalRolesMutationResolver,
|
|
8
12
|
IDPMutationResolver,
|
|
9
13
|
InviteMutationResolver,
|
|
10
14
|
MailTemplateMutationResolver,
|
|
11
|
-
OtpMutationResolver,
|
|
15
|
+
OtpMutationResolver,
|
|
12
16
|
RemoveProjectMemberMutationResolver,
|
|
13
17
|
ResetPasswordMutationResolver,
|
|
14
18
|
SetProjectSecretMutationResolver,
|
|
@@ -16,7 +20,7 @@ import {
|
|
|
16
20
|
SignOutMutationResolver,
|
|
17
21
|
SignUpMutationResolver,
|
|
18
22
|
UpdateProjectMemberMutationResolver,
|
|
19
|
-
UpdateProjectMutationResolver,
|
|
23
|
+
UpdateProjectMutationResolver,
|
|
20
24
|
} from './mutation'
|
|
21
25
|
|
|
22
26
|
import { Resolvers } from '../schema'
|
|
@@ -26,7 +30,7 @@ import { DateTimeType, JSONType } from '@contember/graphql-utils'
|
|
|
26
30
|
import { IDPQueryResolver } from './query/IDPQueryResolver'
|
|
27
31
|
import { UpdateIDPMutationResolver } from './mutation/idp/UpdateIDPMutationResolver'
|
|
28
32
|
import { DisablePersonMutationResolver } from './mutation/person/DisablePersonMutationResolver'
|
|
29
|
-
import { } from './
|
|
33
|
+
import { MailTemplateQueryResolver } from './query/MailTemplateQueryResolver'
|
|
30
34
|
|
|
31
35
|
class ResolverFactory {
|
|
32
36
|
public constructor(
|
|
@@ -36,6 +40,7 @@ class ResolverFactory {
|
|
|
36
40
|
projectQueryResolver: ProjectQueryResolver
|
|
37
41
|
projectMembersQueryResolver: ProjectMembersQueryResolver
|
|
38
42
|
idpQueryResolver: IDPQueryResolver
|
|
43
|
+
mailTemplateQueryResolver: MailTemplateQueryResolver
|
|
39
44
|
|
|
40
45
|
signUpMutationResolver: SignUpMutationResolver
|
|
41
46
|
signInMutationResolver: SignInMutationResolver
|
|
@@ -96,6 +101,7 @@ class ResolverFactory {
|
|
|
96
101
|
projects: this.resolvers.projectQueryResolver.projects.bind(this.resolvers.projectQueryResolver),
|
|
97
102
|
projectMemberships: this.resolvers.projectMembersQueryResolver.projectMemberships.bind(this.resolvers.projectMembersQueryResolver),
|
|
98
103
|
identityProviders: this.resolvers.idpQueryResolver.identityProviders.bind(this.resolvers.idpQueryResolver),
|
|
104
|
+
mailTemplates: this.resolvers.mailTemplateQueryResolver.mailTemplates.bind(this.resolvers.mailTemplateQueryResolver),
|
|
99
105
|
checkResetPasswordToken: () => {
|
|
100
106
|
throw new Error('not implemented')
|
|
101
107
|
},
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
|
-
AddMailTemplateErrorCode,
|
|
3
2
|
AddMailTemplateResponse,
|
|
4
|
-
MailType as SchemaMailType,
|
|
5
3
|
MutationAddProjectMailTemplateArgs,
|
|
6
4
|
MutationRemoveProjectMailTemplateArgs,
|
|
7
5
|
MutationResolvers,
|
|
8
|
-
RemoveMailTemplateErrorCode,
|
|
9
6
|
RemoveMailTemplateResponse,
|
|
10
7
|
} from '../../../schema'
|
|
11
8
|
import { TenantResolverContext } from '../../TenantResolverContext'
|
|
12
|
-
import { MailTemplateManager,
|
|
9
|
+
import { MailTemplateManager, mailTypeFromSchemaToDb, PermissionActions, ProjectManager } from '../../../model'
|
|
13
10
|
import { createErrorResponse, createProjectNotFoundResponse } from '../../errorUtils'
|
|
11
|
+
import { validateEmail } from '../../../model/utils/email'
|
|
14
12
|
|
|
15
13
|
export class MailTemplateMutationResolver implements MutationResolvers {
|
|
16
14
|
constructor(
|
|
@@ -20,7 +18,7 @@ export class MailTemplateMutationResolver implements MutationResolvers {
|
|
|
20
18
|
|
|
21
19
|
async addMailTemplate(
|
|
22
20
|
parent: any,
|
|
23
|
-
{ template: { content, projectSlug, subject, type, useLayout, variant } }: MutationAddProjectMailTemplateArgs,
|
|
21
|
+
{ template: { content, projectSlug, subject, type, useLayout, variant, replyTo } }: MutationAddProjectMailTemplateArgs,
|
|
24
22
|
context: TenantResolverContext,
|
|
25
23
|
): Promise<AddMailTemplateResponse> {
|
|
26
24
|
const project = projectSlug ? await this.projectManager.getProjectBySlug(context.db, projectSlug) : null
|
|
@@ -32,14 +30,18 @@ export class MailTemplateMutationResolver implements MutationResolvers {
|
|
|
32
30
|
if (projectSlug && !project) {
|
|
33
31
|
return createProjectNotFoundResponse('PROJECT_NOT_FOUND', projectSlug)
|
|
34
32
|
}
|
|
33
|
+
if (replyTo && !validateEmail(replyTo)) {
|
|
34
|
+
return createErrorResponse('INVALID_REPLY_EMAIL_FORMAT', 'Reply-to email address is not in a valid format')
|
|
35
|
+
}
|
|
35
36
|
|
|
36
37
|
await this.mailTemplateManager.addMailTemplate(context.db, {
|
|
37
38
|
content,
|
|
38
|
-
projectId: project?.id,
|
|
39
|
+
projectId: project?.id ?? null,
|
|
39
40
|
subject,
|
|
40
41
|
useLayout: typeof useLayout === 'boolean' ? useLayout : true,
|
|
41
42
|
variant: variant || '',
|
|
42
|
-
type:
|
|
43
|
+
type: mailTypeFromSchemaToDb(type),
|
|
44
|
+
replyTo: replyTo?.trim() || null,
|
|
43
45
|
})
|
|
44
46
|
|
|
45
47
|
return {
|
|
@@ -64,9 +66,9 @@ export class MailTemplateMutationResolver implements MutationResolvers {
|
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
const removed = await this.mailTemplateManager.removeMailTemplate(context.db, {
|
|
67
|
-
projectId: project?.id,
|
|
69
|
+
projectId: project?.id ?? null,
|
|
68
70
|
variant: variant || '',
|
|
69
|
-
type:
|
|
71
|
+
type: mailTypeFromSchemaToDb(type),
|
|
70
72
|
})
|
|
71
73
|
if (!removed) {
|
|
72
74
|
return createErrorResponse('PROJECT_NOT_FOUND', 'Mail template not found')
|
|
@@ -77,12 +79,4 @@ export class MailTemplateMutationResolver implements MutationResolvers {
|
|
|
77
79
|
errors: [],
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
|
-
|
|
81
|
-
private mapMailType(type: SchemaMailType): MailType {
|
|
82
|
-
return {
|
|
83
|
-
EXISTING_USER_INVITED: MailType.existingUserInvited,
|
|
84
|
-
NEW_USER_INVITED: MailType.newUserInvited,
|
|
85
|
-
RESET_PASSWORD_REQUEST: MailType.passwordReset,
|
|
86
|
-
}[type]
|
|
87
|
-
}
|
|
88
82
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { MailTemplateData, QueryResolvers } from '../../schema'
|
|
2
|
+
import { MailTemplateListQuery, MailTemplateManager, mailTypeFromDbToSchema, PermissionActions } from '../../model'
|
|
3
|
+
import { TenantResolverContext } from '../TenantResolverContext'
|
|
4
|
+
|
|
5
|
+
export class MailTemplateQueryResolver implements QueryResolvers {
|
|
6
|
+
|
|
7
|
+
constructor(
|
|
8
|
+
private readonly mailTemplateManager: MailTemplateManager,
|
|
9
|
+
) {
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async mailTemplates(parent: unknown, args: unknown, context: TenantResolverContext): Promise<MailTemplateData[]> {
|
|
13
|
+
await context.requireAccess({
|
|
14
|
+
action: PermissionActions.IDP_LIST,
|
|
15
|
+
message: 'You are not allowed to list IDP',
|
|
16
|
+
})
|
|
17
|
+
return (await context.db.queryHandler.fetch(new MailTemplateListQuery())).map(it => ({
|
|
18
|
+
...it,
|
|
19
|
+
variant: it.variant || null,
|
|
20
|
+
type: mailTypeFromDbToSchema(it.type),
|
|
21
|
+
}))
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/schema/index.ts
CHANGED
|
@@ -66,6 +66,7 @@ export type AddMailTemplateError = {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
export type AddMailTemplateErrorCode =
|
|
69
|
+
| 'INVALID_REPLY_EMAIL_FORMAT'
|
|
69
70
|
| 'MISSING_VARIABLE'
|
|
70
71
|
| 'PROJECT_NOT_FOUND'
|
|
71
72
|
|
|
@@ -517,6 +518,7 @@ export type InviteResult = {
|
|
|
517
518
|
export type MailTemplate = {
|
|
518
519
|
readonly content: Scalars['String']['input']
|
|
519
520
|
readonly projectSlug?: InputMaybe<Scalars['String']['input']>
|
|
521
|
+
readonly replyTo?: InputMaybe<Scalars['String']['input']>
|
|
520
522
|
readonly subject: Scalars['String']['input']
|
|
521
523
|
readonly type: MailType
|
|
522
524
|
readonly useLayout?: InputMaybe<Scalars['Boolean']['input']>
|
|
@@ -524,6 +526,17 @@ export type MailTemplate = {
|
|
|
524
526
|
readonly variant?: InputMaybe<Scalars['String']['input']>
|
|
525
527
|
}
|
|
526
528
|
|
|
529
|
+
export type MailTemplateData = {
|
|
530
|
+
readonly __typename?: 'MailTemplateData'
|
|
531
|
+
readonly content: Scalars['String']['output']
|
|
532
|
+
readonly projectSlug?: Maybe<Scalars['String']['output']>
|
|
533
|
+
readonly replyTo?: Maybe<Scalars['String']['output']>
|
|
534
|
+
readonly subject: Scalars['String']['output']
|
|
535
|
+
readonly type: MailType
|
|
536
|
+
readonly useLayout: Scalars['Boolean']['output']
|
|
537
|
+
readonly variant?: Maybe<Scalars['String']['output']>
|
|
538
|
+
}
|
|
539
|
+
|
|
527
540
|
export type MailTemplateIdentifier = {
|
|
528
541
|
readonly projectSlug?: InputMaybe<Scalars['String']['input']>
|
|
529
542
|
readonly type: MailType
|
|
@@ -904,6 +917,7 @@ export type Query = {
|
|
|
904
917
|
readonly __typename?: 'Query'
|
|
905
918
|
readonly checkResetPasswordToken: CheckResetPasswordTokenCode
|
|
906
919
|
readonly identityProviders: ReadonlyArray<IdentityProvider>
|
|
920
|
+
readonly mailTemplates: ReadonlyArray<MailTemplateData>
|
|
907
921
|
readonly me: Identity
|
|
908
922
|
readonly personById?: Maybe<Person>
|
|
909
923
|
readonly projectBySlug?: Maybe<Project>
|
|
@@ -1397,6 +1411,7 @@ export type ResolversTypes = {
|
|
|
1397
1411
|
InviteResult: ResolverTypeWrapper<InviteResult>
|
|
1398
1412
|
Json: ResolverTypeWrapper<Scalars['Json']['output']>
|
|
1399
1413
|
MailTemplate: MailTemplate
|
|
1414
|
+
MailTemplateData: ResolverTypeWrapper<MailTemplateData>
|
|
1400
1415
|
MailTemplateIdentifier: MailTemplateIdentifier
|
|
1401
1416
|
MailType: MailType
|
|
1402
1417
|
MemberType: MemberType
|
|
@@ -1530,6 +1545,7 @@ export type ResolversParentTypes = {
|
|
|
1530
1545
|
InviteResult: InviteResult
|
|
1531
1546
|
Json: Scalars['Json']['output']
|
|
1532
1547
|
MailTemplate: MailTemplate
|
|
1548
|
+
MailTemplateData: MailTemplateData
|
|
1533
1549
|
MailTemplateIdentifier: MailTemplateIdentifier
|
|
1534
1550
|
Membership: Membership
|
|
1535
1551
|
MembershipInput: MembershipInput
|
|
@@ -1945,6 +1961,17 @@ export interface JsonScalarConfig extends GraphQLScalarTypeConfig<ResolversTypes
|
|
|
1945
1961
|
name: 'Json'
|
|
1946
1962
|
}
|
|
1947
1963
|
|
|
1964
|
+
export type MailTemplateDataResolvers<ContextType = any, ParentType extends ResolversParentTypes['MailTemplateData'] = ResolversParentTypes['MailTemplateData']> = {
|
|
1965
|
+
content?: Resolver<ResolversTypes['String'], ParentType, ContextType>
|
|
1966
|
+
projectSlug?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>
|
|
1967
|
+
replyTo?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>
|
|
1968
|
+
subject?: Resolver<ResolversTypes['String'], ParentType, ContextType>
|
|
1969
|
+
type?: Resolver<ResolversTypes['MailType'], ParentType, ContextType>
|
|
1970
|
+
useLayout?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>
|
|
1971
|
+
variant?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>
|
|
1972
|
+
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1948
1975
|
export type MembershipResolvers<ContextType = any, ParentType extends ResolversParentTypes['Membership'] = ResolversParentTypes['Membership']> = {
|
|
1949
1976
|
role?: Resolver<ResolversTypes['String'], ParentType, ContextType>
|
|
1950
1977
|
variables?: Resolver<ReadonlyArray<ResolversTypes['VariableEntry']>, ParentType, ContextType>
|
|
@@ -2037,6 +2064,7 @@ export type ProjectIdentityRelationResolvers<ContextType = any, ParentType exten
|
|
|
2037
2064
|
export type QueryResolvers<ContextType = any, ParentType extends ResolversParentTypes['Query'] = ResolversParentTypes['Query']> = {
|
|
2038
2065
|
checkResetPasswordToken?: Resolver<ResolversTypes['CheckResetPasswordTokenCode'], ParentType, ContextType, RequireFields<QueryCheckResetPasswordTokenArgs, 'requestId' | 'token'>>
|
|
2039
2066
|
identityProviders?: Resolver<ReadonlyArray<ResolversTypes['IdentityProvider']>, ParentType, ContextType>
|
|
2067
|
+
mailTemplates?: Resolver<ReadonlyArray<ResolversTypes['MailTemplateData']>, ParentType, ContextType>
|
|
2040
2068
|
me?: Resolver<ResolversTypes['Identity'], ParentType, ContextType>
|
|
2041
2069
|
personById?: Resolver<Maybe<ResolversTypes['Person']>, ParentType, ContextType, RequireFields<QueryPersonByIdArgs, 'id'>>
|
|
2042
2070
|
projectBySlug?: Resolver<Maybe<ResolversTypes['Project']>, ParentType, ContextType, RequireFields<QueryProjectBySlugArgs, 'slug'>>
|
|
@@ -2321,6 +2349,7 @@ export type Resolvers<ContextType = any> = {
|
|
|
2321
2349
|
InviteResponse?: InviteResponseResolvers<ContextType>
|
|
2322
2350
|
InviteResult?: InviteResultResolvers<ContextType>
|
|
2323
2351
|
Json?: GraphQLScalarType
|
|
2352
|
+
MailTemplateData?: MailTemplateDataResolvers<ContextType>
|
|
2324
2353
|
Membership?: MembershipResolvers<ContextType>
|
|
2325
2354
|
MembershipValidationError?: MembershipValidationErrorResolvers<ContextType>
|
|
2326
2355
|
Mutation?: MutationResolvers<ContextType>
|
|
@@ -19,6 +19,7 @@ const schema: DocumentNode = gql`
|
|
|
19
19
|
checkResetPasswordToken(requestId: String!, token: String!): CheckResetPasswordTokenCode!
|
|
20
20
|
|
|
21
21
|
identityProviders: [IdentityProvider!]!
|
|
22
|
+
mailTemplates: [MailTemplateData!]!
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
type Mutation {
|
|
@@ -808,7 +809,17 @@ const schema: DocumentNode = gql`
|
|
|
808
809
|
}
|
|
809
810
|
|
|
810
811
|
# === mails ===
|
|
811
|
-
|
|
812
|
+
|
|
813
|
+
type MailTemplateData {
|
|
814
|
+
projectSlug: String
|
|
815
|
+
type: MailType!
|
|
816
|
+
variant: String
|
|
817
|
+
subject: String!
|
|
818
|
+
content: String!
|
|
819
|
+
useLayout: Boolean!
|
|
820
|
+
replyTo: String
|
|
821
|
+
}
|
|
822
|
+
|
|
812
823
|
input MailTemplate {
|
|
813
824
|
projectSlug: String
|
|
814
825
|
type: MailType!
|
|
@@ -817,6 +828,7 @@ const schema: DocumentNode = gql`
|
|
|
817
828
|
subject: String!
|
|
818
829
|
content: String!
|
|
819
830
|
useLayout: Boolean
|
|
831
|
+
replyTo: String
|
|
820
832
|
}
|
|
821
833
|
|
|
822
834
|
enum MailType {
|
|
@@ -846,6 +858,7 @@ const schema: DocumentNode = gql`
|
|
|
846
858
|
enum AddMailTemplateErrorCode {
|
|
847
859
|
MISSING_VARIABLE
|
|
848
860
|
PROJECT_NOT_FOUND
|
|
861
|
+
INVALID_REPLY_EMAIL_FORMAT
|
|
849
862
|
}
|
|
850
863
|
|
|
851
864
|
type RemoveMailTemplateResponse {
|
|
@@ -3,7 +3,7 @@ import { ExpectedQuery } from '@contember/database-tester'
|
|
|
3
3
|
export const getMailTemplateSql = (
|
|
4
4
|
args: { type: string; projectId: string | null },
|
|
5
5
|
): ExpectedQuery => args.projectId ? {
|
|
6
|
-
sql: `
|
|
6
|
+
sql: `select "mail_template"."id", "subject", "content", "use_layout" as "useLayout", "reply_to" as "replyTo", "project_id" as "projectId", "mail_type" as "type", "variant"
|
|
7
7
|
FROM "tenant"."mail_template"
|
|
8
8
|
WHERE "project_id" = ?
|
|
9
9
|
AND "mail_type" = ?
|
|
@@ -14,7 +14,7 @@ export const getMailTemplateSql = (
|
|
|
14
14
|
},
|
|
15
15
|
} :
|
|
16
16
|
{
|
|
17
|
-
sql: `
|
|
17
|
+
sql: `select "mail_template"."id", "subject", "content", "use_layout" as "useLayout", "reply_to" as "replyTo", "project_id" as "projectId", "mail_type" as "type", "variant"
|
|
18
18
|
FROM "tenant"."mail_template"
|
|
19
19
|
WHERE "project_id" IS NULL
|
|
20
20
|
AND "mail_type" = ?
|