feat: Add email verification endpoint at /v1/verify
This commit is contained in:
@@ -22,6 +22,7 @@
|
|||||||
"@plunk/shared": "*",
|
"@plunk/shared": "*",
|
||||||
"@plunk/types": "*",
|
"@plunk/types": "*",
|
||||||
"@react-email/render": "^2.0.0",
|
"@react-email/render": "^2.0.0",
|
||||||
|
"@zootools/email-spell-checker": "^1.12.0",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"body-parser": "^2.2.0",
|
"body-parser": "^2.2.0",
|
||||||
"bullmq": "^5.63.2",
|
"bullmq": "^5.63.2",
|
||||||
@@ -29,11 +30,13 @@
|
|||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"csv-parse": "^6.1.0",
|
"csv-parse": "^6.1.0",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
|
"disposable-email": "^0.2.3",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"helmet": "^8.1.0",
|
"helmet": "^8.1.0",
|
||||||
"ioredis": "^5.8.2",
|
"ioredis": "^5.8.2",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"mailchecker": "^6.0.19",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"signale": "^1.4.0",
|
"signale": "^1.4.0",
|
||||||
@@ -43,6 +46,7 @@
|
|||||||
"@types/bcrypt": "^6.0.0",
|
"@types/bcrypt": "^6.0.0",
|
||||||
"@types/cookie-parser": "^1.4.7",
|
"@types/cookie-parser": "^1.4.7",
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/disposable-email": "^0",
|
||||||
"@types/express": "^5.0.5",
|
"@types/express": "^5.0.5",
|
||||||
"@types/helmet": "^4.0.0",
|
"@types/helmet": "^4.0.0",
|
||||||
"@types/jsonwebtoken": "^9.0.6",
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {prisma} from '../database/prisma.js';
|
|||||||
import {ContactService} from '../services/ContactService.js';
|
import {ContactService} from '../services/ContactService.js';
|
||||||
import {DomainService} from '../services/DomainService.js';
|
import {DomainService} from '../services/DomainService.js';
|
||||||
import {EmailService} from '../services/EmailService.js';
|
import {EmailService} from '../services/EmailService.js';
|
||||||
|
import {EmailVerificationService} from '../services/EmailVerificationService.js';
|
||||||
import {EventService} from '../services/EventService.js';
|
import {EventService} from '../services/EventService.js';
|
||||||
import {NotFound, ValidationError} from '../exceptions/index.js';
|
import {NotFound, ValidationError} from '../exceptions/index.js';
|
||||||
import {CatchAsync} from '../utils/asyncHandler.js';
|
import {CatchAsync} from '../utils/asyncHandler.js';
|
||||||
@@ -15,7 +16,7 @@ import {DASHBOARD_URI} from '../app/constants.js';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Public API Actions Controller
|
* Public API Actions Controller
|
||||||
* Handles track event and transactional email endpoints
|
* Handles track event, transactional email, and email verification endpoints
|
||||||
*/
|
*/
|
||||||
@Controller('v1')
|
@Controller('v1')
|
||||||
export class Actions {
|
export class Actions {
|
||||||
@@ -341,4 +342,58 @@ export class Actions {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /v1/verify
|
||||||
|
* Verify an email address
|
||||||
|
*
|
||||||
|
* Request body:
|
||||||
|
* - email: string (required) - Email address to verify
|
||||||
|
*
|
||||||
|
* Response:
|
||||||
|
* - success: boolean
|
||||||
|
* - data: object with verification results
|
||||||
|
* - email: string - Email address that was verified
|
||||||
|
* - valid: boolean - Whether the email appears to be valid
|
||||||
|
* - isDisposable: boolean - Whether the email is from a disposable domain
|
||||||
|
* - hasMxRecords: boolean - Whether the domain has MX records configured
|
||||||
|
* - suggestedEmail?: string - Suggested correction if typo detected
|
||||||
|
* - reasons: string[] - Array of reasons describing the verification results
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* {
|
||||||
|
* email: "[email protected]"
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Response:
|
||||||
|
* {
|
||||||
|
* success: true,
|
||||||
|
* data: {
|
||||||
|
* email: "[email protected]",
|
||||||
|
* valid: false,
|
||||||
|
* isDisposable: false,
|
||||||
|
* hasMxRecords: false,
|
||||||
|
* suggestedEmail: "[email protected]",
|
||||||
|
* reasons: [
|
||||||
|
* "Possible typo detected, did you mean [email protected]?",
|
||||||
|
* "Domain does not exist or has no MX records"
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
@Post('verify')
|
||||||
|
@Middleware([requireSecretKey])
|
||||||
|
@CatchAsync
|
||||||
|
public async verify(req: Request, res: Response, _next: NextFunction) {
|
||||||
|
// Zod validation - errors automatically handled by global error handler
|
||||||
|
const {email} = ActionSchemas.verify.parse(req.body);
|
||||||
|
|
||||||
|
// Verify the email address
|
||||||
|
const verificationResult = await EmailVerificationService.verifyEmail(email);
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: verificationResult,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import {promises as dns} from 'dns';
|
||||||
|
import {run} from '@zootools/email-spell-checker';
|
||||||
|
import disposable from 'disposable-email';
|
||||||
|
|
||||||
|
export interface EmailVerificationResult {
|
||||||
|
email: string;
|
||||||
|
valid: boolean;
|
||||||
|
isDisposable: boolean;
|
||||||
|
isTypo: boolean;
|
||||||
|
domainExists: boolean;
|
||||||
|
hasMxRecords: boolean;
|
||||||
|
suggestedEmail?: string;
|
||||||
|
reasons: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EmailVerificationService {
|
||||||
|
/**
|
||||||
|
* Verify an email address
|
||||||
|
* - Checks if domain exists (DNS A/AAAA records)
|
||||||
|
* - Checks for MX records
|
||||||
|
* - Detects disposable email addresses
|
||||||
|
* - Suggests corrections for common typos
|
||||||
|
*/
|
||||||
|
static async verifyEmail(email: string): Promise<EmailVerificationResult> {
|
||||||
|
const result: EmailVerificationResult = {
|
||||||
|
email,
|
||||||
|
valid: true,
|
||||||
|
isDisposable: false,
|
||||||
|
isTypo: false,
|
||||||
|
domainExists: false,
|
||||||
|
hasMxRecords: false,
|
||||||
|
reasons: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract domain from email
|
||||||
|
const emailParts = email.split('@');
|
||||||
|
if (emailParts.length !== 2) {
|
||||||
|
result.valid = false;
|
||||||
|
result.reasons.push('Invalid email format');
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const domain = emailParts[1]!; // Safe to assert, we already validated length
|
||||||
|
|
||||||
|
// Check if email is from a disposable domain
|
||||||
|
// MailChecker.isValid returns false for disposable emails
|
||||||
|
result.isDisposable = disposable.validate(domain);
|
||||||
|
|
||||||
|
// Check for common typos and suggest corrections
|
||||||
|
const typoCheck = run({email});
|
||||||
|
if (typoCheck && typoCheck.address && typoCheck.address !== email) {
|
||||||
|
result.suggestedEmail = typoCheck.full;
|
||||||
|
result.reasons.push(`Possible typo detected, did you mean ${typoCheck.domain}?`);
|
||||||
|
result.isTypo = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if domain exists (has any DNS records)
|
||||||
|
try {
|
||||||
|
await dns.resolve(domain, 'A');
|
||||||
|
result.domainExists = true;
|
||||||
|
} catch {
|
||||||
|
// Try AAAA records if A records fail
|
||||||
|
try {
|
||||||
|
await dns.resolve(domain, 'AAAA');
|
||||||
|
result.domainExists = true;
|
||||||
|
} catch {
|
||||||
|
result.domainExists = false;
|
||||||
|
result.valid = false;
|
||||||
|
result.reasons.push('Domain does not exist');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check MX records (only if domain exists)
|
||||||
|
if (result.domainExists) {
|
||||||
|
try {
|
||||||
|
const mxRecords = await dns.resolveMx(domain);
|
||||||
|
result.hasMxRecords = mxRecords && mxRecords.length > 0;
|
||||||
|
if (!result.hasMxRecords) {
|
||||||
|
result.valid = false;
|
||||||
|
result.reasons.push('No MX records found for domain');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
result.hasMxRecords = false;
|
||||||
|
result.valid = false;
|
||||||
|
result.reasons.push('No MX records found for domain');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no issues were found, add a success reason
|
||||||
|
if (result.valid && result.reasons.length === 0) {
|
||||||
|
result.reasons.push('Email appears to be valid');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
"---Public API---",
|
"---Public API---",
|
||||||
"public-api/sendEmail",
|
"public-api/sendEmail",
|
||||||
"public-api/trackEvent",
|
"public-api/trackEvent",
|
||||||
|
"public-api/verifyEmail",
|
||||||
"---Resources---",
|
"---Resources---",
|
||||||
"contacts",
|
"contacts",
|
||||||
"templates",
|
"templates",
|
||||||
|
|||||||
@@ -592,6 +592,189 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/v1/verify": {
|
||||||
|
"post": {
|
||||||
|
"tags": ["Public API"],
|
||||||
|
"summary": "Verify email address",
|
||||||
|
"description": "Verify an email address for validity, check if it's from a disposable domain, verify MX records, and detect potential typos with suggestions.",
|
||||||
|
"operationId": "verifyEmail",
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["email"],
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "email",
|
||||||
|
"description": "Email address to verify"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": {
|
||||||
|
"validEmail": {
|
||||||
|
"summary": "Valid email address",
|
||||||
|
"value": {
|
||||||
|
"email": "[email protected]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"typoEmail": {
|
||||||
|
"summary": "Email with potential typo",
|
||||||
|
"value": {
|
||||||
|
"email": "[email protected]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"disposableEmail": {
|
||||||
|
"summary": "Disposable email address",
|
||||||
|
"value": {
|
||||||
|
"email": "[email protected]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Email verification completed successfully",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Always true for successful requests"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "email",
|
||||||
|
"description": "Email address that was verified"
|
||||||
|
},
|
||||||
|
"valid": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether the email appears to be valid overall"
|
||||||
|
},
|
||||||
|
"isDisposable": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether the email is from a disposable/temporary email domain"
|
||||||
|
},
|
||||||
|
"isTypo": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether a potential typo was detected in the email address"
|
||||||
|
},
|
||||||
|
"domainExists": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether the domain exists (has DNS A or AAAA records)"
|
||||||
|
},
|
||||||
|
"hasMxRecords": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether the domain has MX records configured for email delivery"
|
||||||
|
},
|
||||||
|
"suggestedEmail": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "email",
|
||||||
|
"description": "Suggested correction if a typo was detected (optional)",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"reasons": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Array of human-readable reasons describing the verification results"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["email", "valid", "isDisposable", "isTypo", "domainExists", "hasMxRecords", "reasons"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": {
|
||||||
|
"validEmail": {
|
||||||
|
"summary": "Valid email",
|
||||||
|
"value": {
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"email": "[email protected]",
|
||||||
|
"valid": true,
|
||||||
|
"isDisposable": false,
|
||||||
|
"isTypo": false,
|
||||||
|
"domainExists": true,
|
||||||
|
"hasMxRecords": true,
|
||||||
|
"reasons": [
|
||||||
|
"Email appears to be valid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"typoDetected": {
|
||||||
|
"summary": "Email with typo detected",
|
||||||
|
"value": {
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"email": "[email protected]",
|
||||||
|
"valid": false,
|
||||||
|
"isDisposable": false,
|
||||||
|
"isTypo": true,
|
||||||
|
"domainExists": false,
|
||||||
|
"hasMxRecords": false,
|
||||||
|
"suggestedEmail": "[email protected]",
|
||||||
|
"reasons": [
|
||||||
|
"Possible typo detected, did you mean gmail.com?",
|
||||||
|
"Domain does not exist"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"disposableEmail": {
|
||||||
|
"summary": "Disposable email detected",
|
||||||
|
"value": {
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"email": "[email protected]",
|
||||||
|
"valid": true,
|
||||||
|
"isDisposable": true,
|
||||||
|
"isTypo": false,
|
||||||
|
"domainExists": true,
|
||||||
|
"hasMxRecords": true,
|
||||||
|
"reasons": [
|
||||||
|
"Email appears to be valid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad request - invalid email format",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized - invalid or missing API key",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/contacts": {
|
"/contacts": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": ["Contacts"],
|
"tags": ["Contacts"],
|
||||||
|
|||||||
@@ -390,6 +390,9 @@ export const ActionSchemas = {
|
|||||||
message: 'Total attachment size must not exceed 10MB',
|
message: 'Total attachment size must not exceed 10MB',
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
verify: z.object({
|
||||||
|
email,
|
||||||
|
}),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const BillingLimitSchemas = {
|
export const BillingLimitSchemas = {
|
||||||
|
|||||||
@@ -6552,6 +6552,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/disposable-email@npm:^0":
|
||||||
|
version: 0.2.2
|
||||||
|
resolution: "@types/disposable-email@npm:0.2.2"
|
||||||
|
checksum: 10c0/8e96fe0e5d9677629d848cae2245e88d182a712ee53179d83a7ce371da836585fb2a4fed29ead263c2aec74d100daeff5af9dc2a54dfbac3f20ffc3b875fb087
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/eslint-scope@npm:^3.7.7":
|
"@types/eslint-scope@npm:^3.7.7":
|
||||||
version: 3.7.7
|
version: 3.7.7
|
||||||
resolution: "@types/eslint-scope@npm:3.7.7"
|
resolution: "@types/eslint-scope@npm:3.7.7"
|
||||||
@@ -7606,6 +7613,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@zootools/email-spell-checker@npm:^1.12.0":
|
||||||
|
version: 1.12.0
|
||||||
|
resolution: "@zootools/email-spell-checker@npm:1.12.0"
|
||||||
|
checksum: 10c0/0b2fa9d0aaf1e6d846fe9e261276fecbd5c48db2689814b702bcec6b63729d097b245f59cdfb3dfca65d66b68de24b67e3b317494bcfbfdd166c244cd298a0cc
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"abbrev@npm:^4.0.0":
|
"abbrev@npm:^4.0.0":
|
||||||
version: 4.0.0
|
version: 4.0.0
|
||||||
resolution: "abbrev@npm:4.0.0"
|
resolution: "abbrev@npm:4.0.0"
|
||||||
@@ -7829,12 +7843,14 @@ __metadata:
|
|||||||
"@types/bcrypt": "npm:^6.0.0"
|
"@types/bcrypt": "npm:^6.0.0"
|
||||||
"@types/cookie-parser": "npm:^1.4.7"
|
"@types/cookie-parser": "npm:^1.4.7"
|
||||||
"@types/cors": "npm:^2.8.17"
|
"@types/cors": "npm:^2.8.17"
|
||||||
|
"@types/disposable-email": "npm:^0"
|
||||||
"@types/express": "npm:^5.0.5"
|
"@types/express": "npm:^5.0.5"
|
||||||
"@types/helmet": "npm:^4.0.0"
|
"@types/helmet": "npm:^4.0.0"
|
||||||
"@types/jsonwebtoken": "npm:^9.0.6"
|
"@types/jsonwebtoken": "npm:^9.0.6"
|
||||||
"@types/morgan": "npm:^1.9.9"
|
"@types/morgan": "npm:^1.9.9"
|
||||||
"@types/multer": "npm:^2.0.0"
|
"@types/multer": "npm:^2.0.0"
|
||||||
"@types/signale": "npm:^1.4.7"
|
"@types/signale": "npm:^1.4.7"
|
||||||
|
"@zootools/email-spell-checker": "npm:^1.12.0"
|
||||||
bcrypt: "npm:^6.0.0"
|
bcrypt: "npm:^6.0.0"
|
||||||
body-parser: "npm:^2.2.0"
|
body-parser: "npm:^2.2.0"
|
||||||
bullmq: "npm:^5.63.2"
|
bullmq: "npm:^5.63.2"
|
||||||
@@ -7843,11 +7859,13 @@ __metadata:
|
|||||||
cors: "npm:^2.8.5"
|
cors: "npm:^2.8.5"
|
||||||
csv-parse: "npm:^6.1.0"
|
csv-parse: "npm:^6.1.0"
|
||||||
dayjs: "npm:^1.11.19"
|
dayjs: "npm:^1.11.19"
|
||||||
|
disposable-email: "npm:^0.2.3"
|
||||||
dotenv: "npm:^17.2.3"
|
dotenv: "npm:^17.2.3"
|
||||||
express: "npm:^5.1.0"
|
express: "npm:^5.1.0"
|
||||||
helmet: "npm:^8.1.0"
|
helmet: "npm:^8.1.0"
|
||||||
ioredis: "npm:^5.8.2"
|
ioredis: "npm:^5.8.2"
|
||||||
jsonwebtoken: "npm:^9.0.2"
|
jsonwebtoken: "npm:^9.0.2"
|
||||||
|
mailchecker: "npm:^6.0.19"
|
||||||
morgan: "npm:^1.10.0"
|
morgan: "npm:^1.10.0"
|
||||||
multer: "npm:^2.0.2"
|
multer: "npm:^2.0.2"
|
||||||
signale: "npm:^1.4.0"
|
signale: "npm:^1.4.0"
|
||||||
@@ -9490,6 +9508,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"disposable-email@npm:^0.2.3":
|
||||||
|
version: 0.2.3
|
||||||
|
resolution: "disposable-email@npm:0.2.3"
|
||||||
|
checksum: 10c0/d2da52e225bb784e640f9aff6c3e319c596e82c778b5483e36ff67eba5c5b9df6f59ed8c26be401b0813c921783e0b271fb97d49b33a73cb46dd010dc7081931
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"dlv@npm:^1.1.3":
|
"dlv@npm:^1.1.3":
|
||||||
version: 1.1.3
|
version: 1.1.3
|
||||||
resolution: "dlv@npm:1.1.3"
|
resolution: "dlv@npm:1.1.3"
|
||||||
@@ -13296,6 +13321,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"mailchecker@npm:^6.0.19":
|
||||||
|
version: 6.0.19
|
||||||
|
resolution: "mailchecker@npm:6.0.19"
|
||||||
|
checksum: 10c0/96269fc53d5103f1950926554ac92cb38ff6c8511c7628d13a36e74572f88484a47d8789d35902cb69589c1eeb7c8d96d46105f92d285c4dfabbb00b88e8b8a3
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"mailparser@npm:^3.7.1":
|
"mailparser@npm:^3.7.1":
|
||||||
version: 3.9.1
|
version: 3.9.1
|
||||||
resolution: "mailparser@npm:3.9.1"
|
resolution: "mailparser@npm:3.9.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user