diff --git a/locales/en-US.yml b/locales/en-US.yml
index 3cf165e3e..8b9994c70 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -675,6 +675,10 @@ smtpHost: "Host"
smtpPort: "Port"
smtpUser: "Username"
smtpPass: "Password"
+customEmailTemplate: {
+ title: "Custom Email Template (Advanced)",
+ description: "This allows you to modify the default email template that is used for any automated sent emails."
+}
emptyToDisableSmtpAuth: "Leave username and password empty to disable SMTP authentication"
smtpSecure: "Use implicit SSL/TLS for SMTP connections"
smtpSecureInfo: "Turn this off when using STARTTLS"
diff --git a/locales/index.d.ts b/locales/index.d.ts
index da14073e8..76f440461 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -2720,6 +2720,16 @@ export interface Locale extends ILocale {
* パスワード
*/
"smtpPass": string;
+ "customEmailTemplate": {
+ /**
+ * カスタムメールテンプレート(上級者向け)
+ */
+ "title": string;
+ /**
+ * これにより、自動送信メールに使用されるデフォルトのメールテンプレートを変更することができます。
+ */
+ "description": string;
+ };
/**
* ユーザー名とパスワードを空欄にすることで、SMTP認証を無効化出来ます
*/
@@ -6843,7 +6853,7 @@ export interface Locale extends ILocale {
};
"_tutorialCompleted": {
/**
- * Misskey初心者講座 修了証
+ * Forkey初心者講座 修了証
*/
"title": string;
/**
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 0e4770453..811ebf7be 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -676,6 +676,10 @@ smtpHost: "ホスト"
smtpPort: "ポート"
smtpUser: "ユーザー名"
smtpPass: "パスワード"
+customEmailTemplate: {
+ title: "カスタムメールテンプレート(上級者向け)",
+ description: "これにより、自動送信メールに使用されるデフォルトのメールテンプレートを変更することができます。"
+}
emptyToDisableSmtpAuth: "ユーザー名とパスワードを空欄にすることで、SMTP認証を無効化出来ます"
smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する"
smtpSecureInfo: "STARTTLS使用時はオフにします。"
diff --git a/packages/backend/migration/1737293151180-custom-email-templates.js b/packages/backend/migration/1737293151180-custom-email-templates.js
new file mode 100644
index 000000000..8765049ae
--- /dev/null
+++ b/packages/backend/migration/1737293151180-custom-email-templates.js
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: leah and other forkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class CustomEmailTemplates1737293151180 {
+ name = 'CustomEmailTemplates1737293151180'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "meta" ADD "emailTemplate" text`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "emailTemplate"`);
+ }
+}
diff --git a/packages/backend/package.json b/packages/backend/package.json
index bf08f631f..24844a956 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -118,6 +118,7 @@
"fluent-ffmpeg": "2.1.3",
"form-data": "4.0.1",
"got": "14.4.5",
+ "handlebars": "^4.7.8",
"hpagent": "1.2.0",
"htmlescape": "1.1.1",
"http-link-header": "1.1.3",
diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts
index 37c9c736e..6fd99ec6b 100644
--- a/packages/backend/src/core/EmailService.ts
+++ b/packages/backend/src/core/EmailService.ts
@@ -18,9 +18,106 @@ import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { RedisKVCache } from '@/misc/cache.js';
+import handlebars from 'handlebars';
@Injectable()
export class EmailService {
+ public static defaultEmailTemplate: string = `
+
+
+
+ {{ subject }}
+
+
+
+
+
+
+
+
+ {{ subject }}
+ {{ html }}
+
+
+
+
+
+
+`;
+
private logger: Logger;
private verifymailResponseCache: RedisKVCache<{
@@ -67,6 +164,19 @@ export class EmailService {
const enableAuth = meta.smtpUser != null && meta.smtpUser !== '';
+ const emailTemplateSource = meta.emailTemplate ?? EmailService.defaultEmailTemplate;
+ // console.log(handlebar.)
+ const template = handlebars.compile(emailTemplateSource)
+
+ const htmlToSend = template({
+ subject,
+ html,
+ emailSettingUrl,
+ logo: meta.logoImageUrl ?? meta.iconUrl ?? iconUrl,
+ url: this.config.url,
+ host: this.config.host,
+ })
+
const transporter = nodemailer.createTransport({
host: meta.smtpHost,
port: meta.smtpPort,
@@ -86,86 +196,7 @@ export class EmailService {
to: to,
subject: subject,
text: text,
- html: `
-
-
-
- ${ subject }
-
-
-
-
-
-
-
-
- ${ subject }
- ${ html }
-
-
-
-
-
-`,
+ html: htmlToSend,
});
this.logger.info(`Message sent: ${info.messageId}`);
diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts
index dbc5a4c1e..dbbacd683 100644
--- a/packages/backend/src/models/Meta.ts
+++ b/packages/backend/src/models/Meta.ts
@@ -349,6 +349,11 @@ export class MiMeta {
})
public smtpPass: string | null;
+ @Column('text', {
+ nullable: true,
+ })
+ public emailTemplate: string | null;
+
@Column('boolean', {
default: false,
})
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index f00f39720..87125f860 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -9,6 +9,7 @@ import { MetaService } from '@/core/MetaService.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
+import {EmailService} from "@/core/EmailService.js";
export const meta = {
tags: ['meta'],
@@ -269,6 +270,10 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
+ emailTemplate: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
swPrivateKey: {
type: 'string',
optional: false, nullable: true,
@@ -559,7 +564,7 @@ export default class extends Endpoint { // eslint-
name: instance.name,
shortName: instance.shortName,
uri: this.config.url,
- description: instance.description,
+ description: instance.description ?? EmailService.defaultEmailTemplate,
langs: instance.langs,
tosUrl: instance.termsOfServiceUrl,
repositoryUrl: instance.repositoryUrl,
@@ -624,6 +629,7 @@ export default class extends Endpoint { // eslint-
smtpPort: instance.smtpPort,
smtpUser: instance.smtpUser,
smtpPass: instance.smtpPass,
+ emailTemplate: instance.emailTemplate ?? EmailService.defaultEmailTemplate,
swPrivateKey: instance.swPrivateKey,
useObjectStorage: instance.useObjectStorage,
objectStorageBaseUrl: instance.objectStorageBaseUrl,
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 2bb1b7443..a6b6907f6 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -459,6 +459,10 @@ export default class extends Endpoint { // eslint-
set.smtpPass = ps.smtpPass;
}
+ if (ps.emailTemplate !== undefined) {
+ set.emailTemplate = ps.emailTemplate;
+ }
+
if (ps.enableServiceWorker !== undefined) {
set.enableServiceWorker = ps.enableServiceWorker;
}
diff --git a/packages/frontend/src/pages/admin/email-settings.vue b/packages/frontend/src/pages/admin/email-settings.vue
index 4a858887f..a8fe52fc2 100644
--- a/packages/frontend/src/pages/admin/email-settings.vue
+++ b/packages/frontend/src/pages/admin/email-settings.vue
@@ -44,6 +44,15 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.smtpSecure }}
{{ i18n.ts.smtpSecureInfo }}
+
+
+ {{ i18n.ts.customEmailTemplate.title }}
+
+
+ {{ i18n.ts.customEmailTemplate.description }}
+
+
+
@@ -78,6 +87,8 @@ import { fetchInstance, instance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkButton from '@/components/MkButton.vue';
+import MkFolder from "@/components/MkFolder.vue";
+import MkCodeEditor from "@/components/MkCodeEditor.vue";
const enableEmail = ref(false);
const email = ref(null);
@@ -86,6 +97,7 @@ const smtpHost = ref('');
const smtpPort = ref(0);
const smtpUser = ref('');
const smtpPass = ref('');
+const emailTemplate = ref('');
async function init() {
const meta = await misskeyApi('admin/meta');
@@ -96,6 +108,7 @@ async function init() {
smtpPort.value = meta.smtpPort;
smtpUser.value = meta.smtpUser;
smtpPass.value = meta.smtpPass;
+ emailTemplate.value = meta.emailTemplate;
}
async function testEmail() {
@@ -123,6 +136,7 @@ function save() {
smtpPort: smtpPort.value,
smtpUser: smtpUser.value,
smtpPass: smtpPass.value,
+ emailTemplate: emailTemplate.value,
}).then(() => {
fetchInstance(true);
});
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 6c563ea2c..9b6eeb240 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -4608,7 +4608,7 @@ export type components = {
blockee: components['schemas']['UserDetailedNotMe'];
};
Hashtag: {
- /** @example misskey */
+ /** @example forkey */
tag: string;
mentionedUsersCount: number;
mentionedLocalUsersCount: number;
@@ -4791,7 +4791,7 @@ export type components = {
isNotResponding: boolean;
isSuspended: boolean;
isBlocked: boolean;
- /** @example misskey */
+ /** @example forkey */
softwareName: string | null;
softwareVersion: string | null;
/** @example true */
@@ -5142,6 +5142,8 @@ export type components = {
enableTurnstile: boolean;
turnstileSiteKey: string | null;
googleAnalyticsId: string | null;
+ enableFC: boolean;
+ fcSiteKey: string | null;
swPublickey: string | null;
/** @default /assets/ai.png */
mascotImageUrl: string;
@@ -5291,6 +5293,8 @@ export type operations = {
enableTurnstile: boolean;
turnstileSiteKey: string | null;
googleAnalyticsId: string | null;
+ enableFC: boolean;
+ fcSiteKey: string | null;
swPublickey: string | null;
/** @default /assets/ai.png */
mascotImageUrl: string | null;
@@ -5317,6 +5321,7 @@ export type operations = {
mcaptchaSecretKey: string | null;
recaptchaSecretKey: string | null;
turnstileSecretKey: string | null;
+ fcSecretKey: string | null;
sensitiveMediaDetection: string;
sensitiveMediaDetectionSensitivity: string;
setSensitiveFlagAutomatically: boolean;
@@ -5329,6 +5334,7 @@ export type operations = {
smtpPort: number | null;
smtpUser: string | null;
smtpPass: string | null;
+ emailTemplate: string | null;
swPrivateKey: string | null;
useObjectStorage: boolean;
objectStorageBaseUrl: string | null;
@@ -10222,6 +10228,9 @@ export type operations = {
enableTurnstile?: boolean;
turnstileSiteKey?: string | null;
turnstileSecretKey?: string | null;
+ enableFC?: boolean;
+ fcSiteKey?: string | null;
+ fcSecretKey?: string | null;
googleAnalyticsId?: string | null;
/** @enum {string} */
sensitiveMediaDetection?: 'none' | 'all' | 'local' | 'remote';
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index edcf33cc0..c2e69342f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -240,6 +240,9 @@ importers:
got:
specifier: 14.4.5
version: 14.4.5
+ handlebars:
+ specifier: ^4.7.8
+ version: 4.7.8
hpagent:
specifier: 1.2.0
version: 1.2.0
@@ -5942,6 +5945,11 @@ packages:
resolution: {integrity: sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==}
engines: {node: '>=0.8.0'}
+ handlebars@4.7.8:
+ resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==}
+ engines: {node: '>=0.4.7'}
+ hasBin: true
+
happy-dom@16.0.1:
resolution: {integrity: sha512-cqbqvutE6XAIMe4nM93TkbW5SDFtLkU/6nsQfJBJ2KSlaT+My2kmnYsCFXrvEzvmP7s1xGJ6Xt4D9LNJIJHMbA==}
engines: {node: '>=18.0.0'}
@@ -7319,6 +7327,9 @@ packages:
resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==}
engines: {node: '>= 0.6'}
+ neo-async@2.6.2:
+ resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
+
nested-property@4.0.0:
resolution: {integrity: sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA==}
@@ -9364,6 +9375,11 @@ packages:
resolution: {integrity: sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==}
engines: {node: '>=12.17'}
+ uglify-js@3.19.3:
+ resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==}
+ engines: {node: '>=0.8.0'}
+ hasBin: true
+
uid2@0.0.4:
resolution: {integrity: sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==}
@@ -9828,6 +9844,9 @@ packages:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
+ wordwrap@1.0.0:
+ resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
+
wordwrapjs@5.1.0:
resolution: {integrity: sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg==}
engines: {node: '>=12.17'}
@@ -15916,6 +15935,15 @@ snapshots:
hammerjs@2.0.8: {}
+ handlebars@4.7.8:
+ dependencies:
+ minimist: 1.2.8
+ neo-async: 2.6.2
+ source-map: 0.6.1
+ wordwrap: 1.0.0
+ optionalDependencies:
+ uglify-js: 3.19.3
+
happy-dom@16.0.1:
dependencies:
webidl-conversions: 7.0.0
@@ -17639,6 +17667,8 @@ snapshots:
negotiator@0.6.4: {}
+ neo-async@2.6.2: {}
+
nested-property@4.0.0: {}
netmask@2.0.2: {}
@@ -19767,6 +19797,9 @@ snapshots:
typical@7.3.0: {}
+ uglify-js@3.19.3:
+ optional: true
+
uid2@0.0.4: {}
uid@2.0.2:
@@ -20245,6 +20278,8 @@ snapshots:
word-wrap@1.2.5: {}
+ wordwrap@1.0.0: {}
+
wordwrapjs@5.1.0: {}
wrap-ansi@6.2.0: