From 0a73973a7c6e6e95a5206bfc5388ff7f7a9ba8ed Mon Sep 17 00:00:00 2001
From: Nafu Satsuki <satsuki@nafusoft.dev>
Date: Sat, 18 Nov 2023 20:39:48 +0900
Subject: [PATCH] =?UTF-8?q?=E3=83=A1=E3=83=BC=E3=83=AB=E3=82=A2=E3=83=89?=
 =?UTF-8?q?=E3=83=AC=E3=82=B9=E3=81=AE=E8=AA=8D=E8=A8=BC=E3=81=ABverifymai?=
 =?UTF-8?q?l.io=E3=82=92=E4=BD=BF=E3=81=88=E3=82=8B=E3=82=88=E3=81=86?=
 =?UTF-8?q?=E3=81=AB=E3=81=99=E3=82=8B=E3=80=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../1700303245007-supportVerifyMailApi.js     | 18 ++++
 packages/backend/src/core/EmailService.ts     | 92 +++++++++++++++++--
 packages/backend/src/models/Meta.ts           | 11 +++
 .../server/api/endpoints/admin/update-meta.ts | 14 +++
 .../frontend/src/pages/admin/security.vue     | 13 +++
 5 files changed, 140 insertions(+), 8 deletions(-)
 create mode 100644 packages/backend/migration/1700303245007-supportVerifyMailApi.js

diff --git a/packages/backend/migration/1700303245007-supportVerifyMailApi.js b/packages/backend/migration/1700303245007-supportVerifyMailApi.js
new file mode 100644
index 0000000000..3ac59ec37a
--- /dev/null
+++ b/packages/backend/migration/1700303245007-supportVerifyMailApi.js
@@ -0,0 +1,18 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class SupportVerifyMailApi1700303245007 {
+    name = 'SupportVerifyMailApi1700303245007'
+
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" ADD "verifymailAuthKey" character varying(1024)`);
+        await queryRunner.query(`ALTER TABLE "meta" ADD "enableVerifymailApi" boolean NOT NULL DEFAULT false`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableVerifymailApi"`);
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "verifymailAuthKey"`);
+    }
+}
diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts
index c9da3f77c0..8f28197ebc 100644
--- a/packages/backend/src/core/EmailService.ts
+++ b/packages/backend/src/core/EmailService.ts
@@ -13,6 +13,9 @@ import type Logger from '@/logger.js';
 import type { UserProfilesRepository } from '@/models/_.js';
 import { LoggerService } from '@/core/LoggerService.js';
 import { bindThis } from '@/decorators.js';
+import {URLSearchParams} from "node:url";
+import { HttpRequestService } from '@/core/HttpRequestService.js';
+import {SubOutputFormat} from "deep-email-validator/dist/output/output.js";
 
 @Injectable()
 export class EmailService {
@@ -27,6 +30,7 @@ export class EmailService {
 
 		private metaService: MetaService,
 		private loggerService: LoggerService,
+		private httpRequestService: HttpRequestService,
 	) {
 		this.logger = this.loggerService.getLogger('email');
 	}
@@ -160,14 +164,25 @@ export class EmailService {
 			email: emailAddress,
 		});
 
-		const validated = meta.enableActiveEmailValidation ? await validateEmail({
-			email: emailAddress,
-			validateRegex: true,
-			validateMx: true,
-			validateTypo: false, // TLDを見ているみたいだけどclubとか弾かれるので
-			validateDisposable: true, // 捨てアドかどうかチェック
-			validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので
-		}) : { valid: true, reason: null };
+		const verifymailApi = meta.enableVerifymailApi && meta.verifymailAuthKey != null;
+		let validated;
+
+		if (meta.enableActiveEmailValidation) {
+			if (verifymailApi) {
+				validated = await this.verifyMail(emailAddress, meta.verifymailAuthKey);
+			} else {
+				validated = meta.enableActiveEmailValidation ? await validateEmail({
+					email: emailAddress,
+					validateRegex: true,
+					validateMx: true,
+					validateTypo: false, // TLDを見ているみたいだけどclubとか弾かれるので
+					validateDisposable: true, // 捨てアドかどうかチェック
+					validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので
+				}) : { valid: true, reason: null };
+			}
+		} else {
+			validated = { valid: true, reason: null };
+		}
 
 		const available = exist === 0 && validated.valid;
 
@@ -182,4 +197,65 @@ export class EmailService {
 			null,
 		};
 	}
+
+	private async verifyMail(emailAddress: string, verifymailAuthKey: string): Promise<{
+		valid: boolean;
+		reason: 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | null;
+	}> {
+		const endpoint = 'https://verifymail.io/api/' + emailAddress + '?key=' + verifymailAuthKey;
+		const res = await this.httpRequestService.send(endpoint, {
+			method: 'GET',
+			headers: {
+				'Content-Type': 'application/x-www-form-urlencoded',
+				Accept: 'application/json, */*',
+			},
+		});
+
+		const json = (await res.json()) as {
+			block: boolean;
+			catch_all: boolean;
+			deliverable_email: boolean;
+			disposable: boolean;
+			domain: string;
+			email_address: string;
+			email_provider: string;
+			mx: boolean;
+			mx_fallback: boolean;
+			mx_host: string[];
+			mx_ip: string[];
+			mx_priority: { [key: string]: number };
+			privacy: boolean;
+			related_domains: string[];
+		};
+
+		if (json.email_address === undefined) {
+			return {
+				valid: false,
+				reason: 'format',
+			};
+		}
+		if (json.deliverable_email !== undefined && !json.deliverable_email) {
+			return {
+				valid: false,
+				reason: 'smtp',
+			};
+		}
+		if (json.disposable) {
+			return {
+				valid: false,
+				reason: 'disposable',
+			};
+		}
+		if (json.mx !== undefined && !json.mx) {
+			return {
+				valid: false,
+				reason: 'mx',
+			};
+		}
+
+		return {
+			valid: true,
+			reason: null,
+		};
+	}
 }
diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts
index 14a72add1d..83e8962f5d 100644
--- a/packages/backend/src/models/Meta.ts
+++ b/packages/backend/src/models/Meta.ts
@@ -446,6 +446,17 @@ export class MiMeta {
 	})
 	public enableActiveEmailValidation: boolean;
 
+	@Column('boolean', {
+		default: false,
+	})
+	public enableVerifymailApi: boolean;
+
+	@Column('varchar', {
+		length: 1024,
+		nullable: true,
+	})
+	public verifymailAuthKey: string | null;
+
 	@Column('boolean', {
 		default: true,
 	})
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index da3e5dd9ac..d6f9b2cd94 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -113,6 +113,8 @@ export const paramDef = {
 		objectStorageS3ForcePathStyle: { type: 'boolean' },
 		enableIpLogging: { type: 'boolean' },
 		enableActiveEmailValidation: { type: 'boolean' },
+		enableVerifymailApi: { type: 'boolean' },
+		verifymailAuthKey: { type: 'string', nullable: true },
 		enableChartsForRemoteUser: { type: 'boolean' },
 		enableChartsForFederatedInstances: { type: 'boolean' },
 		enableServerMachineStats: { type: 'boolean' },
@@ -454,6 +456,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				set.enableActiveEmailValidation = ps.enableActiveEmailValidation;
 			}
 
+			if (ps.enableVerifymailApi !== undefined) {
+				set.enableVerifymailApi = ps.enableVerifymailApi;
+			}
+
+			if (ps.verifymailAuthKey !== undefined) {
+				if (ps.verifymailAuthKey === '') {
+					set.verifymailAuthKey = null;
+				} else {
+					set.verifymailAuthKey = ps.verifymailAuthKey;
+				}
+			}
+
 			if (ps.enableChartsForRemoteUser !== undefined) {
 				set.enableChartsForRemoteUser = ps.enableChartsForRemoteUser;
 			}
diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue
index a2594ee6c5..f7f76d910a 100644
--- a/packages/frontend/src/pages/admin/security.vue
+++ b/packages/frontend/src/pages/admin/security.vue
@@ -73,6 +73,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<MkSwitch v-model="enableActiveEmailValidation" @update:modelValue="save">
 							<template #label>Enable</template>
 						</MkSwitch>
+						<MkSwitch v-model="enableVerifymailApi" @update:modelValue="save">
+							<template #label>Use Verifymail API</template>
+						</MkSwitch>
+						<MkInput v-model="verifymailAuthKey" @update:modelValue="save">
+							<template #prefix><i class="ti ti-key"></i></template>
+							<template #label>Verifymail API Auth Key</template>
+						</MkInput>
 					</div>
 				</MkFolder>
 
@@ -132,6 +139,8 @@ let setSensitiveFlagAutomatically: boolean = $ref(false);
 let enableSensitiveMediaDetectionForVideos: boolean = $ref(false);
 let enableIpLogging: boolean = $ref(false);
 let enableActiveEmailValidation: boolean = $ref(false);
+let enableVerifymailApi: boolean = $ref(false);
+let verifymailAuthKey: string | null = $ref(null);
 
 async function init() {
 	const meta = await os.api('admin/meta');
@@ -150,6 +159,8 @@ async function init() {
 	enableSensitiveMediaDetectionForVideos = meta.enableSensitiveMediaDetectionForVideos;
 	enableIpLogging = meta.enableIpLogging;
 	enableActiveEmailValidation = meta.enableActiveEmailValidation;
+	enableVerifymailApi = meta.enableVerifymailApi;
+	verifymailAuthKey = meta.verifymailAuthKey;
 }
 
 function save() {
@@ -167,6 +178,8 @@ function save() {
 		enableSensitiveMediaDetectionForVideos,
 		enableIpLogging,
 		enableActiveEmailValidation,
+		enableVerifymailApi,
+		verifymailAuthKey,
 	}).then(() => {
 		fetchInstance();
 	});
-- 
GitLab