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 }} + + + +
+
+ The instances logo +
+
+

{{ 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 + + + +
+ + + +
+
@@ -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: