diff --git a/locales/en-US.yml b/locales/en-US.yml
index 5f3c74754e..a786e178cb 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -1080,6 +1080,7 @@ noteId: "Post ID"
signupsDisabled: "Signups on this server are currently disabled, but you can always sign up at another server! If you have an invitation code for this server, please enter it below."
findOtherInstance: "Find another server"
apps: "Apps"
+sendModMail: "Send Moderation Notice"
_sensitiveMediaDetection:
description: "Reduces the effort of server moderation through automatically recognizing\
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index e6f8f7ee6a..6a98fdfb28 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -53,6 +53,7 @@ import * as ep___admin_resetPassword from "./endpoints/admin/reset-password.js";
import * as ep___admin_resolveAbuseUserReport from "./endpoints/admin/resolve-abuse-user-report.js";
import * as ep___admin_search_indexAll from "./endpoints/admin/search/index-all.js";
import * as ep___admin_sendEmail from "./endpoints/admin/send-email.js";
+import * as ep___admin_sendModMail from "./endpoints/admin/send-mod-mail.js";
import * as ep___admin_serverInfo from "./endpoints/admin/server-info.js";
import * as ep___admin_showModerationLogs from "./endpoints/admin/show-moderation-logs.js";
import * as ep___admin_showUser from "./endpoints/admin/show-user.js";
@@ -403,6 +404,7 @@ const eps = [
["admin/resolve-abuse-user-report", ep___admin_resolveAbuseUserReport],
["admin/search/index-all", ep___admin_search_indexAll],
["admin/send-email", ep___admin_sendEmail],
+ ["admin/send-mod-mail", ep___admin_sendModMail],
["admin/server-info", ep___admin_serverInfo],
["admin/show-moderation-logs", ep___admin_showModerationLogs],
["admin/show-user", ep___admin_showUser],
diff --git a/packages/backend/src/server/api/endpoints/admin/send-mod-mail.ts b/packages/backend/src/server/api/endpoints/admin/send-mod-mail.ts
new file mode 100644
index 0000000000..b6f3111ce2
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/send-mod-mail.ts
@@ -0,0 +1,68 @@
+import * as sanitizeHtml from "sanitize-html";
+import define from "../../define.js";
+import { Users, UserProfiles } from "@/models/index.js";
+import { ApiError } from "../../error.js";
+import { sendEmail } from "@/services/send-email.js";
+import { createNotification } from "@/services/create-notification.js";
+
+export const meta = {
+ tags: ["users"],
+
+ requireCredential: true,
+ requireModerator: true,
+
+ description: "Send a mod mail.",
+
+ errors: {
+ noSuchUser: {
+ message: "No such user.",
+ code: "NO_SUCH_USER",
+ id: "1acefcb5-0959-43fd-9685-b48305736cb5",
+ },
+ noEmail: {
+ message: "No email for user.",
+ code: "NO_EMAIL",
+ id: "ac9d2d22-ef73-11ed-a05b-0242ac120003",
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: "object",
+ properties: {
+ userId: { type: "string", format: "misskey:id" },
+ comment: { type: "string", minLength: 1, maxLength: 2048 },
+ },
+ required: ["userId", "comment"],
+} as const;
+
+export default define(meta, paramDef, async (ps) => {
+ const [user, profile] = await Promise.all([
+ Users.findOneBy({ id: ps.userId }),
+ UserProfiles.findOneBy({ userId: ps.userId }),
+ ]);
+
+ if (user == null || profile == null) {
+ throw new ApiError(meta.errors.noSuchUser);
+ }
+
+ createNotification(user.id, "app", {
+ customBody: ps.comment,
+ customHeader: "Moderation Notice",
+ customIcon: "/static-assets/badges/info.png",
+ });
+
+ setImmediate(async () => {
+ const email = profile.email;
+ if (email == null) {
+ throw new ApiError(meta.errors.noEmail);
+ }
+
+ sendEmail(
+ email,
+ "Moderation notice",
+ sanitizeHtml(ps.comment),
+ sanitizeHtml(ps.comment),
+ );
+ });
+});
diff --git a/packages/client/src/pages/user-info.vue b/packages/client/src/pages/user-info.vue
index 12321c2ba2..f66069a35d 100644
--- a/packages/client/src/pages/user-info.vue
+++ b/packages/client/src/pages/user-info.vue
@@ -218,6 +218,12 @@
>{{ i18n.ts.deleteAccount }}
+ {{ i18n.ts.sendModMail }}