mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 21:07:28 -08:00
⚡ Add resource Custom Verification Email to AWS SES (#1405)
* ⚡ Add resource Custom Verification Email to AWS SES * ⚡ Minor improvements to AWS SES Node Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
parent
ba7c35dbf5
commit
894a4b950a
|
@ -18,7 +18,7 @@ import {
|
||||||
|
|
||||||
function setParameter(params: string[], base: string, values: string[]) {
|
function setParameter(params: string[], base: string, values: string[]) {
|
||||||
for (let i = 0; i < values.length; i++) {
|
for (let i = 0; i < values.length; i++) {
|
||||||
params.push(`${base}.${i+1}=${values[i]}`);
|
params.push(`${base}.${i + 1}=${values[i]}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ export class AwsSes implements INodeType {
|
||||||
description: INodeTypeDescription = {
|
description: INodeTypeDescription = {
|
||||||
displayName: 'AWS SES',
|
displayName: 'AWS SES',
|
||||||
name: 'awsSes',
|
name: 'awsSes',
|
||||||
icon: 'file:ses.png',
|
icon: 'file:ses.svg',
|
||||||
group: ['output'],
|
group: ['output'],
|
||||||
version: 1,
|
version: 1,
|
||||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||||
|
@ -49,6 +49,10 @@ export class AwsSes implements INodeType {
|
||||||
name: 'resource',
|
name: 'resource',
|
||||||
type: 'options',
|
type: 'options',
|
||||||
options: [
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Custom Verification Email',
|
||||||
|
value: 'customVerificationEmail',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Email',
|
name: 'Email',
|
||||||
value: 'email',
|
value: 'email',
|
||||||
|
@ -61,6 +65,338 @@ export class AwsSes implements INodeType {
|
||||||
default: 'email',
|
default: 'email',
|
||||||
description: 'The operation to perform.',
|
description: 'The operation to perform.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'customVerificationEmail',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Create',
|
||||||
|
value: 'create',
|
||||||
|
description: 'Create a new custom verification email template',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Delete',
|
||||||
|
value: 'delete',
|
||||||
|
description: 'Delete an existing custom verification email template',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Get',
|
||||||
|
value: 'get',
|
||||||
|
description: 'Get the custom email verification template',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Get All',
|
||||||
|
value: 'getAll',
|
||||||
|
description: 'Get all the existing custom verification email templates for your account',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Send',
|
||||||
|
value: 'send',
|
||||||
|
description: 'Add an email address to the list of identities',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Update',
|
||||||
|
value: 'update',
|
||||||
|
description: 'Update an existing custom verification email template.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'create',
|
||||||
|
description: 'The operation to perform.',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
displayName: 'From Email',
|
||||||
|
name: 'fromEmailAddress',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'customVerificationEmail',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'create',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
description: 'The email address that the custom verification email is sent from.',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Template Name',
|
||||||
|
name: 'templateName',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'customVerificationEmail',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'create',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'The name of the custom verification email template.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Template Content',
|
||||||
|
name: 'templateContent',
|
||||||
|
type: 'string',
|
||||||
|
typeOptions: {
|
||||||
|
alwaysOpenEditWindow: true,
|
||||||
|
},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'customVerificationEmail',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'create',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: `The content of the custom verification email. The total size of the email must be less than 10 MB. The message body may contain HTML`,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Template Subject',
|
||||||
|
name: 'templateSubject',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'customVerificationEmail',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'create',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
description: 'The subject line of the custom verification email.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Success Redirection URL',
|
||||||
|
name: 'successRedirectionURL',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'customVerificationEmail',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'create',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
description: 'The URL that the recipient of the verification email is sent to if his or her address is successfully verified.',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Failure Redirection URL',
|
||||||
|
name: 'failureRedirectionURL',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'customVerificationEmail',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'create',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
description: 'The URL that the recipient of the verification email is sent to if his or her address is not successfully verified.',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
displayName: 'Email',
|
||||||
|
name: 'email',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'customVerificationEmail',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'send',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
description: 'The email address to verify.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Template Name',
|
||||||
|
name: 'templateName',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'customVerificationEmail',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'send',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
description: 'The name of the custom verification email template to use when sending the verification email.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Additional Fields',
|
||||||
|
name: 'additionalFields',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Field',
|
||||||
|
default: {},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'customVerificationEmail',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'send',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Configuration Set Name',
|
||||||
|
name: 'configurationSetName',
|
||||||
|
type: 'string',
|
||||||
|
description: 'Name of a configuration set to use when sending the verification email.',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
displayName: 'Template Name',
|
||||||
|
name: 'templateName',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'customVerificationEmail',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'update',
|
||||||
|
'delete',
|
||||||
|
'get',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'The name of the custom verification email template.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Update Fields',
|
||||||
|
name: 'updateFields',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Field',
|
||||||
|
default: {},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'customVerificationEmail',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'update',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Failure Redirection URL',
|
||||||
|
name: 'failureRedirectionURL',
|
||||||
|
type: 'string',
|
||||||
|
description: 'The URL that the recipient of the verification email is sent to if his or her address is not successfully verified.',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'From Email',
|
||||||
|
name: 'fromEmailAddress',
|
||||||
|
type: 'string',
|
||||||
|
description: 'The email address that the custom verification email is sent from.',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Success Redirection URL',
|
||||||
|
name: 'successRedirectionURL',
|
||||||
|
type: 'string',
|
||||||
|
description: 'The URL that the recipient of the verification email is sent to if his or her address is successfully verified.',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Template Content',
|
||||||
|
name: 'templateContent',
|
||||||
|
type: 'string',
|
||||||
|
typeOptions: {
|
||||||
|
alwaysOpenEditWindow: true,
|
||||||
|
},
|
||||||
|
description: `The content of the custom verification email. The total size of the email must be less than 10 MB. The message body may contain HTML`,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Template Subject',
|
||||||
|
name: 'templateSubject',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'The subject line of the custom verification email.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Return All',
|
||||||
|
name: 'returnAll',
|
||||||
|
type: 'boolean',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'customVerificationEmail',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'getAll',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: false,
|
||||||
|
description: 'If all results should be returned or only up to a given limit.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Limit',
|
||||||
|
name: 'limit',
|
||||||
|
type: 'number',
|
||||||
|
default: 20,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'customVerificationEmail',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'getAll',
|
||||||
|
],
|
||||||
|
returnAll: [
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Operation',
|
displayName: 'Operation',
|
||||||
name: 'operation',
|
name: 'operation',
|
||||||
|
@ -598,6 +934,140 @@ export class AwsSes implements INodeType {
|
||||||
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
|
||||||
|
if (resource === 'customVerificationEmail') {
|
||||||
|
|
||||||
|
if (operation === 'create') {
|
||||||
|
|
||||||
|
const failureRedirectionURL = this.getNodeParameter('failureRedirectionURL', i) as string;
|
||||||
|
|
||||||
|
const email = this.getNodeParameter('fromEmailAddress', i) as string;
|
||||||
|
|
||||||
|
const successRedirectionURL = this.getNodeParameter('successRedirectionURL', i) as string;
|
||||||
|
|
||||||
|
const templateContent = this.getNodeParameter('templateContent', i) as string;
|
||||||
|
|
||||||
|
const templateName = this.getNodeParameter('templateName', i) as string;
|
||||||
|
|
||||||
|
const templateSubject = this.getNodeParameter('templateSubject', i) as string;
|
||||||
|
|
||||||
|
const params = [
|
||||||
|
`Action=CreateCustomVerificationEmailTemplate`,
|
||||||
|
`FailureRedirectionURL=${failureRedirectionURL}`,
|
||||||
|
`FromEmailAddress=${email}`,
|
||||||
|
`SuccessRedirectionURL=${successRedirectionURL}`,
|
||||||
|
`TemplateContent=${templateContent}`,
|
||||||
|
`TemplateName=${templateName}`,
|
||||||
|
`TemplateSubject=${templateSubject}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
responseData = await awsApiRequestSOAP.call(this, 'email', 'POST', '', params.join('&'));
|
||||||
|
|
||||||
|
responseData = responseData.CreateCustomVerificationEmailTemplateResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operation === 'delete') {
|
||||||
|
|
||||||
|
const templateName = this.getNodeParameter('templateName', i) as string;
|
||||||
|
|
||||||
|
const params = [
|
||||||
|
`Action=DeleteCustomVerificationEmailTemplate`,
|
||||||
|
`TemplateName=${templateName}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
responseData = await awsApiRequestSOAP.call(this, 'email', 'POST', '', params.join('&'));
|
||||||
|
|
||||||
|
responseData = responseData.DeleteCustomVerificationEmailTemplateResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operation === 'get') {
|
||||||
|
|
||||||
|
const templateName = this.getNodeParameter('templateName', i) as string;
|
||||||
|
|
||||||
|
const params = [
|
||||||
|
`TemplateName=${templateName}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
responseData = await awsApiRequestSOAP.call(this, 'email', 'POST', '/?Action=GetCustomVerificationEmailTemplate&' + params.join('&'));
|
||||||
|
|
||||||
|
responseData = responseData.GetCustomVerificationEmailTemplateResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operation === 'getAll') {
|
||||||
|
|
||||||
|
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
||||||
|
|
||||||
|
if (returnAll === true) {
|
||||||
|
responseData = await awsApiRequestSOAPAllItems.call(this, 'ListCustomVerificationEmailTemplatesResponse.ListCustomVerificationEmailTemplatesResult.CustomVerificationEmailTemplates.member', 'email', 'POST', '/?Action=ListCustomVerificationEmailTemplates');
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const limit = this.getNodeParameter('limit', i) as number;
|
||||||
|
|
||||||
|
responseData = await awsApiRequestSOAP.call(this, 'email', 'GET', `/?Action=ListCustomVerificationEmailTemplates&MaxResults=${limit}`);
|
||||||
|
|
||||||
|
responseData = responseData.ListCustomVerificationEmailTemplatesResponse.ListCustomVerificationEmailTemplatesResult.CustomVerificationEmailTemplates.member;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operation === 'send') {
|
||||||
|
|
||||||
|
const email = this.getNodeParameter('email', i) as string[];
|
||||||
|
|
||||||
|
const templateName = this.getNodeParameter('templateName', i) as string;
|
||||||
|
|
||||||
|
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
|
||||||
|
|
||||||
|
const params = [
|
||||||
|
`Action=SendCustomVerificationEmail`,
|
||||||
|
`TemplateName=${templateName}`,
|
||||||
|
`EmailAddress=${email}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (additionalFields.configurationSetName) {
|
||||||
|
params.push(`ConfigurationSetName=${additionalFields.configurationSetName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData = await awsApiRequestSOAP.call(this, 'email', 'POST', '', params.join('&'));
|
||||||
|
|
||||||
|
responseData = responseData.SendCustomVerificationEmailResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operation === 'update') {
|
||||||
|
|
||||||
|
const templateName = this.getNodeParameter('templateName', i) as string;
|
||||||
|
|
||||||
|
const updateFields = this.getNodeParameter('updateFields', i) as IDataObject;
|
||||||
|
|
||||||
|
const params = [
|
||||||
|
`Action=UpdateCustomVerificationEmailTemplate`,
|
||||||
|
`TemplateName=${templateName}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (updateFields.FailureRedirectionURL) {
|
||||||
|
params.push(`FailureRedirectionURL=${updateFields.FailureRedirectionURL}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateFields.email) {
|
||||||
|
params.push(`FromEmailAddress=${updateFields.email}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateFields.successRedirectionURL) {
|
||||||
|
params.push(`SuccessRedirectionURL=${updateFields.successRedirectionURL}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateFields.templateContent) {
|
||||||
|
params.push(`TemplateContent=${updateFields.templateContent}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateFields.templateSubject) {
|
||||||
|
params.push(`TemplateSubject=${updateFields.templateSubject}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData = await awsApiRequestSOAP.call(this, 'email', 'POST', '', params.join('&'));
|
||||||
|
|
||||||
|
responseData = responseData.UpdateCustomVerificationEmailTemplateResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (resource === 'email') {
|
if (resource === 'email') {
|
||||||
|
|
||||||
if (operation === 'send') {
|
if (operation === 'send') {
|
||||||
|
@ -837,11 +1307,13 @@ export class AwsSes implements INodeType {
|
||||||
responseData = responseData.UpdateTemplateResponse;
|
responseData = responseData.UpdateTemplateResponse;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(responseData)) {
|
if (Array.isArray(responseData)) {
|
||||||
returnData.push.apply(returnData, responseData as IDataObject[]);
|
returnData.push.apply(returnData, responseData as IDataObject[]);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
returnData.push(responseData as IDataObject);
|
if (responseData !== undefined) {
|
||||||
|
returnData.push(responseData as IDataObject);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I
|
||||||
headers: signOpts.headers,
|
headers: signOpts.headers,
|
||||||
method,
|
method,
|
||||||
uri: endpoint.href,
|
uri: endpoint.href,
|
||||||
body: signOpts.body,
|
body: signOpts.body as string,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 1,022 B |
1
packages/nodes-base/nodes/Aws/SES/ses.svg
Normal file
1
packages/nodes-base/nodes/Aws/SES/ses.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 74.375 85" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round"><use xlink:href="#A" x="2.188" y="2.5"/><symbol id="A" overflow="visible"><g stroke="none"><path d="M16.558 12.75L0 38.591l16.558 25.841 13.227-3.324.654-44.869-13.881-3.489z" fill="#876929"/><path d="M35.049 59.786l-18.491 4.645V12.75l18.491 4.645v42.391z" fill="#d9a741"/><g fill="#876929"><path d="M32.849 21.614L35.05 80 70 62.867l-.01-43.615-8.914 1.448-28.228.913z"/><path d="M46.184 33.149l10.906-.632 10.778-19.164L40.612 0 30.439 4.364l15.745 28.785z"/></g><path d="M40.612 0l27.256 13.353-10.778 19.164L40.612 0z" fill="#d9a741"/><path d="M35.049 5.539L57.09 44.742l3.788 22.595L35.049 80l-10.46-5.131V9.64l10.46-4.101z" fill="#876929"/><path d="M69.991 19.251L70 62.867 35.05 80V5.539l22.041 39.203 12.899-25.491z" fill="#d9a741"/></g></symbol></svg>
|
After Width: | Height: | Size: 961 B |
Loading…
Reference in a new issue