@easy-cms/core 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -245,6 +245,25 @@ interface EmailAdapter {
245
245
  send(message: EmailMessage & {
246
246
  readonly from: string;
247
247
  }): Promise<void>;
248
+ /**
249
+ * The settings in use, shown to admins on Settings → Email. Never return secrets: say whether
250
+ * a password is set, not what it is.
251
+ */
252
+ describe?(): readonly EmailSetting[];
253
+ /** Checks the connection without sending (Settings → Email); throws with the reason. */
254
+ verify?(): Promise<void>;
255
+ }
256
+ /** One setting an email adapter shows to admins (`describe()`). */
257
+ interface EmailSetting {
258
+ /**
259
+ * `host`, `port`, `secure`, `user`, `password`, `from` and `delivery` (`log`: printed, not sent)
260
+ * are translated in the admin; others are shown as they are.
261
+ */
262
+ readonly key: string;
263
+ /** `null`: not set. */
264
+ readonly value: string | null;
265
+ /** Where it comes from, e.g. `SMTP_HOST` or `smtp({ host })`. */
266
+ readonly source?: string;
248
267
  }
249
268
  /** Prints emails to the log instead of sending them: for development and tests. */
250
269
  declare function consoleEmail(options?: {
@@ -2080,6 +2099,26 @@ interface AdminDeliveries {
2080
2099
  counts: Record<DeliveryState, number>;
2081
2100
  }
2082
2101
 
2102
+ /** Settings → Email in the admin (`GET <api>/admin/email`). */
2103
+ interface AdminEmail {
2104
+ /** An `email` adapter is set in the config. */
2105
+ configured: boolean;
2106
+ /** The adapter's name, e.g. `smtp`; `custom` when it has none. */
2107
+ name: string | null;
2108
+ /** The default sender. */
2109
+ from: string | null;
2110
+ /** What the adapter tells about itself (`describe()`), never secrets. */
2111
+ settings: EmailSetting[];
2112
+ /** The adapter can check its connection without sending (`verify()`). */
2113
+ canVerify: boolean;
2114
+ }
2115
+ type EmailCheck = {
2116
+ ok: true;
2117
+ } | {
2118
+ ok: false;
2119
+ error: string;
2120
+ };
2121
+
2083
2122
  /**
2084
2123
  * Finds the file of an admin module (`admin.modules`): a package export or a path relative to
2085
2124
  * the project root. Looks from the project root first, then from the server's entry file
@@ -2320,4 +2359,4 @@ declare function validateConfig(config: Config): ConfigIssue[];
2320
2359
  /** The version of `@easy-cms/core`, set when the package is built. */
2321
2360
  declare const VERSION: string;
2322
2361
 
2323
- export { ADMIN_ICONS, API_KEYS, API_KEY_GLOBAL_OPERATIONS, API_KEY_OPERATIONS, type Access, type AccessArgs, type AccessOptions, type AdminAttention, type AdminBrand, type AdminCollection, type AdminComponent, type AdminComponentRef, type AdminConfig, type AdminDeliveries, type AdminDelivery, type AdminEmailDelivery, type AdminField, type AdminGlobal, type AdminIcon, type AdminLocale, type AdminPage, type AdminPageRef, type AdminSchema, type AdminStatus, type AdminViewAccess, type AdminWebhookDelivery, type AdminWidgetRef, type AfterChangeHook, type AfterDeleteHook, type AfterReadHook, type ApiKeyContext, type ApiKeyGlobalOperation, type ApiKeyOperation, type ApiKeyPermissions, type ArrayField, Auth, type AuthConfig, type AuthUser, BUILTIN_COLLECTIONS, type BeforeChangeHook, type BeforeDeleteHook, type BeforeValidateHook, type Block, type BlocksField, type BooleanField, type BuiltinApiKeysCollection, type BuiltinMediaCollection, type BuiltinUsersCollection, type BundleHints, CONFIG_FILE_NAMES, CSRF_COOKIE, CSRF_HEADER, type CliCommand, type CollectionAccess, type CollectionAdmin, type CollectionConfig, type CollectionDocument, type CollectionHooks, type CollectionListAdmin, type CollectionSlug, type Config, ConfigError, type ConfigIssue, type ContainerAdmin, type CopyProgress, type CopyResult, type CreateEasyCMSOptions, type CreateInput, type CreateMigrationResult, type CustomField, type CustomFieldTypes, DEFAULT_ADMIN_PATH, DEFAULT_API_PATH, DEFAULT_DEPTH, DEFAULT_MAX_FILE_SIZE, DEFAULT_MAX_VERSIONS, DEFAULT_PASSWORD_EMAILS, DEFAULT_TOKEN_EXPIRATION, type DashboardWidget, type Database, type DatabaseAdapter, type DatabaseInitArgs, type DatabaseTransfer, type DateField, type DeliveryKind, type DeliveryState, type DepthOptions, type DocumentOf, EXTENSIONS, EasyCMS, EasyCMSError, type EmailAdapter, type EmailField, type EmailMessage, type Endpoint, type EndpointRequest, FIELD_TYPES, FIELD_TYPE_BASES, type Field, type FieldAccess, type FieldAccessFn, type FieldAdmin, type FieldError, type FieldType, type FieldTypeBase, type FieldTypeDefinition, type FieldTypeValidateContext, type FieldValidate, type FieldValidateContext, type FieldValue, type FieldsInput, type FieldsValue, type FilterOptions, type FilterOptionsArgs, type FindArgs, type FindOptions, ForbiddenError, type GlobalAccess, type GlobalConfig, type GlobalDocument, type GlobalHooks, type GlobalInput, type GlobalSlug, type GroupField, type ID, INTERNAL_COLLECTIONS, type ImageSize, type InferCollection, type InferGlobal, type JsonField, type Label, type LivePreview, type LoadConfigOptions, type LocalStorageOptions, type LocalizationConfig, type Logger, type LoginArgs, MAX_DEPTH, MEDIA, MIN_PASSWORD_LENGTH, MIN_SECRET_LENGTH, type MediaDocument, type MediaSize, type MigrationInfo, NotFoundError, type NumberField, type Operation, type PaginatedDocs, type PasswordEmail, type PasswordEmailArgs, type PasswordEmailFn, type PasswordLinkOptions, PayloadTooLargeError, type Plugin, type PluginInfo, type PluginTypes, type PreviewTarget, type PreviewURL, QueryError, type RawDocument, type ReadOptions, type RelationshipField, type ResolvedConfig, type RestHandler, type RestHandlerOptions, type RichTextDocument, type RichTextField, type Role, type RoutesConfig, SESSION_COOKIE, type ScheduledJob, SchemaError, type SchemaMode, type SelectField, type SelectOption, type Session, type SlugField, type SlugOf, type StorageAdapter, type StoredFile, type TextField, type TextareaField, TooManyRequestsError, type TypedPlugin, UnauthorizedError, type UpdateInput, type UpdateOptions, type UploadConfig, type UploadField, type UploadFromURLConfig, VERSION, ValidationError, type Version, type VersionSummary, type VersionsConfig, WEBHOOK_EVENTS, type WebhookConfig, type WebhookEvent, type WebhookPayload, type Where, type WhereOperators, anyone, applyDefaults, applyPlugins, configSignature, consoleEmail, consoleLogger, copyDatabase, createEasyCMS, createRestHandler, createRootEndpointHandler, defineConfig, defineFieldType, definePlugin, fillMissing, findConfigFile, generateSlugs, generateTypes, hasRows, hashPassword, imageDimensions, importConfig, isAdmin, isHasMany, isLoggedIn, keyAllows, loadConfig, localStorage, mergeForUpdate, mimeAllowed, mimeAllowedBy, parseId, populate, readCookie, resolveAdminModule, resolveConfig, rowFields, silentLogger, singularize, slugify, sniffMimeType, validateConfig, validateFields, verifyPassword };
2362
+ export { ADMIN_ICONS, API_KEYS, API_KEY_GLOBAL_OPERATIONS, API_KEY_OPERATIONS, type Access, type AccessArgs, type AccessOptions, type AdminAttention, type AdminBrand, type AdminCollection, type AdminComponent, type AdminComponentRef, type AdminConfig, type AdminDeliveries, type AdminDelivery, type AdminEmail, type AdminEmailDelivery, type AdminField, type AdminGlobal, type AdminIcon, type AdminLocale, type AdminPage, type AdminPageRef, type AdminSchema, type AdminStatus, type AdminViewAccess, type AdminWebhookDelivery, type AdminWidgetRef, type AfterChangeHook, type AfterDeleteHook, type AfterReadHook, type ApiKeyContext, type ApiKeyGlobalOperation, type ApiKeyOperation, type ApiKeyPermissions, type ArrayField, Auth, type AuthConfig, type AuthUser, BUILTIN_COLLECTIONS, type BeforeChangeHook, type BeforeDeleteHook, type BeforeValidateHook, type Block, type BlocksField, type BooleanField, type BuiltinApiKeysCollection, type BuiltinMediaCollection, type BuiltinUsersCollection, type BundleHints, CONFIG_FILE_NAMES, CSRF_COOKIE, CSRF_HEADER, type CliCommand, type CollectionAccess, type CollectionAdmin, type CollectionConfig, type CollectionDocument, type CollectionHooks, type CollectionListAdmin, type CollectionSlug, type Config, ConfigError, type ConfigIssue, type ContainerAdmin, type CopyProgress, type CopyResult, type CreateEasyCMSOptions, type CreateInput, type CreateMigrationResult, type CustomField, type CustomFieldTypes, DEFAULT_ADMIN_PATH, DEFAULT_API_PATH, DEFAULT_DEPTH, DEFAULT_MAX_FILE_SIZE, DEFAULT_MAX_VERSIONS, DEFAULT_PASSWORD_EMAILS, DEFAULT_TOKEN_EXPIRATION, type DashboardWidget, type Database, type DatabaseAdapter, type DatabaseInitArgs, type DatabaseTransfer, type DateField, type DeliveryKind, type DeliveryState, type DepthOptions, type DocumentOf, EXTENSIONS, EasyCMS, EasyCMSError, type EmailAdapter, type EmailCheck, type EmailField, type EmailMessage, type EmailSetting, type Endpoint, type EndpointRequest, FIELD_TYPES, FIELD_TYPE_BASES, type Field, type FieldAccess, type FieldAccessFn, type FieldAdmin, type FieldError, type FieldType, type FieldTypeBase, type FieldTypeDefinition, type FieldTypeValidateContext, type FieldValidate, type FieldValidateContext, type FieldValue, type FieldsInput, type FieldsValue, type FilterOptions, type FilterOptionsArgs, type FindArgs, type FindOptions, ForbiddenError, type GlobalAccess, type GlobalConfig, type GlobalDocument, type GlobalHooks, type GlobalInput, type GlobalSlug, type GroupField, type ID, INTERNAL_COLLECTIONS, type ImageSize, type InferCollection, type InferGlobal, type JsonField, type Label, type LivePreview, type LoadConfigOptions, type LocalStorageOptions, type LocalizationConfig, type Logger, type LoginArgs, MAX_DEPTH, MEDIA, MIN_PASSWORD_LENGTH, MIN_SECRET_LENGTH, type MediaDocument, type MediaSize, type MigrationInfo, NotFoundError, type NumberField, type Operation, type PaginatedDocs, type PasswordEmail, type PasswordEmailArgs, type PasswordEmailFn, type PasswordLinkOptions, PayloadTooLargeError, type Plugin, type PluginInfo, type PluginTypes, type PreviewTarget, type PreviewURL, QueryError, type RawDocument, type ReadOptions, type RelationshipField, type ResolvedConfig, type RestHandler, type RestHandlerOptions, type RichTextDocument, type RichTextField, type Role, type RoutesConfig, SESSION_COOKIE, type ScheduledJob, SchemaError, type SchemaMode, type SelectField, type SelectOption, type Session, type SlugField, type SlugOf, type StorageAdapter, type StoredFile, type TextField, type TextareaField, TooManyRequestsError, type TypedPlugin, UnauthorizedError, type UpdateInput, type UpdateOptions, type UploadConfig, type UploadField, type UploadFromURLConfig, VERSION, ValidationError, type Version, type VersionSummary, type VersionsConfig, WEBHOOK_EVENTS, type WebhookConfig, type WebhookEvent, type WebhookPayload, type Where, type WhereOperators, anyone, applyDefaults, applyPlugins, configSignature, consoleEmail, consoleLogger, copyDatabase, createEasyCMS, createRestHandler, createRootEndpointHandler, defineConfig, defineFieldType, definePlugin, fillMissing, findConfigFile, generateSlugs, generateTypes, hasRows, hashPassword, imageDimensions, importConfig, isAdmin, isHasMany, isLoggedIn, keyAllows, loadConfig, localStorage, mergeForUpdate, mimeAllowed, mimeAllowedBy, parseId, populate, readCookie, resolveAdminModule, resolveConfig, rowFields, silentLogger, singularize, slugify, sniffMimeType, validateConfig, validateFields, verifyPassword };
package/dist/index.js CHANGED
@@ -150,8 +150,8 @@ ${lines.join("\n")}`;
150
150
  };
151
151
  var EasyCMSError = class extends Error {
152
152
  status;
153
- constructor(message, status) {
154
- super(message);
153
+ constructor(message2, status) {
154
+ super(message2);
155
155
  this.name = "EasyCMSError";
156
156
  this.status = status;
157
157
  }
@@ -172,38 +172,38 @@ var NotFoundError = class extends EasyCMSError {
172
172
  }
173
173
  };
174
174
  var QueryError = class extends EasyCMSError {
175
- constructor(message) {
176
- super(message, 400);
175
+ constructor(message2) {
176
+ super(message2, 400);
177
177
  this.name = "QueryError";
178
178
  }
179
179
  };
180
180
  var SchemaError = class extends EasyCMSError {
181
- constructor(message) {
182
- super(message, 500);
181
+ constructor(message2) {
182
+ super(message2, 500);
183
183
  this.name = "SchemaError";
184
184
  }
185
185
  };
186
186
  var UnauthorizedError = class extends EasyCMSError {
187
- constructor(message = "You must be logged in") {
188
- super(message, 401);
187
+ constructor(message2 = "You must be logged in") {
188
+ super(message2, 401);
189
189
  this.name = "UnauthorizedError";
190
190
  }
191
191
  };
192
192
  var ForbiddenError = class extends EasyCMSError {
193
- constructor(message = "You are not allowed to do this") {
194
- super(message, 403);
193
+ constructor(message2 = "You are not allowed to do this") {
194
+ super(message2, 403);
195
195
  this.name = "ForbiddenError";
196
196
  }
197
197
  };
198
198
  var TooManyRequestsError = class extends EasyCMSError {
199
- constructor(message) {
200
- super(message, 429);
199
+ constructor(message2) {
200
+ super(message2, 429);
201
201
  this.name = "TooManyRequestsError";
202
202
  }
203
203
  };
204
204
  var PayloadTooLargeError = class extends EasyCMSError {
205
- constructor(message) {
206
- super(message, 413);
205
+ constructor(message2) {
206
+ super(message2, 413);
207
207
  this.name = "PayloadTooLargeError";
208
208
  }
209
209
  };
@@ -1331,7 +1331,7 @@ async function validateFields(fields, input, options, prefix = "") {
1331
1331
  for (const field of fields) {
1332
1332
  const path = prefix + field.name;
1333
1333
  const raw = input[field.name];
1334
- const fail = (message) => errors.push({ field: path, message });
1334
+ const fail = (message2) => errors.push({ field: path, message: message2 });
1335
1335
  if (field.type === "group") {
1336
1336
  if (!isEmpty(raw) && !isPlainObject(raw)) {
1337
1337
  fail("must be an object");
@@ -1360,7 +1360,7 @@ async function validateFields(fields, input, options, prefix = "") {
1360
1360
  for (const locale of localization.locales) {
1361
1361
  const own = locale === written;
1362
1362
  const localePath = own ? path : `${path}.${locale}`;
1363
- const failLocale = (message) => errors.push({ field: localePath, message });
1363
+ const failLocale = (message2) => errors.push({ field: localePath, message: message2 });
1364
1364
  const value2 = map[locale];
1365
1365
  if (isEmpty(value2) || Array.isArray(value2) && value2.length === 0) {
1366
1366
  if (own && field.required && !options.skipRequired) failLocale("is required");
@@ -1543,17 +1543,19 @@ function consoleEmail(options = {}) {
1543
1543
  const sent = [];
1544
1544
  const adapter = {
1545
1545
  name: "console",
1546
+ // `log`: the admin says it prints to the server log instead of sending.
1547
+ describe: () => [{ key: "delivery", value: "log" }],
1546
1548
  from: options.from ?? "Easy CMS <no-reply@localhost>",
1547
1549
  sent,
1548
- async send(message) {
1549
- sent.push(message);
1550
+ async send(message2) {
1551
+ sent.push(message2);
1550
1552
  const list = (value) => value === void 0 ? void 0 : typeof value === "string" ? value : value.join(", ");
1551
1553
  const lines = [
1552
- `Email to ${list(message.to)}${message.cc ? `, cc ${list(message.cc)}` : ""}`,
1553
- `From: ${message.from}${message.replyTo ? ` (reply to ${message.replyTo})` : ""}`,
1554
- `Subject: ${message.subject}`,
1554
+ `Email to ${list(message2.to)}${message2.cc ? `, cc ${list(message2.cc)}` : ""}`,
1555
+ `From: ${message2.from}${message2.replyTo ? ` (reply to ${message2.replyTo})` : ""}`,
1556
+ `Subject: ${message2.subject}`,
1555
1557
  "",
1556
- message.text ?? message.html ?? ""
1558
+ message2.text ?? message2.html ?? ""
1557
1559
  ];
1558
1560
  (options.log ?? console.log)(lines.join("\n"));
1559
1561
  }
@@ -1581,14 +1583,14 @@ var Mailer = class {
1581
1583
  * for the sending too. Without an `email` adapter in the config, the email is skipped with a
1582
1584
  * warning.
1583
1585
  */
1584
- async send(message) {
1586
+ async send(message2) {
1585
1587
  if (!this.adapter) {
1586
1588
  this.logger.warn(
1587
- `Email "${message.subject}" not sent: set \`email\` in the config (e.g. consoleEmail() in development)`
1589
+ `Email "${message2.subject}" not sent: set \`email\` in the config (e.g. consoleEmail() in development)`
1588
1590
  );
1589
1591
  return;
1590
1592
  }
1591
- const body = JSON.stringify(message);
1593
+ const body = JSON.stringify(message2);
1592
1594
  let id;
1593
1595
  try {
1594
1596
  id = await this.queue?.add({
@@ -1601,7 +1603,7 @@ var Mailer = class {
1601
1603
  } catch (error) {
1602
1604
  this.logger.error(`Could not save email: ${error.message}`);
1603
1605
  }
1604
- this.track(this.deliver(message, body, id));
1606
+ this.track(this.deliver(message2, body, id));
1605
1607
  }
1606
1608
  /** Waits for emails being sent, e.g. before a serverless function returns. */
1607
1609
  async flush() {
@@ -1637,25 +1639,25 @@ var Mailer = class {
1637
1639
  return { sent, failed };
1638
1640
  }
1639
1641
  /** One attempt now for a saved email (the admin's Retry): `undefined` when sent, else the error. */
1640
- async sendNow(message) {
1642
+ async sendNow(message2) {
1641
1643
  if (!this.adapter) return "email is not set up (`email` in the config)";
1642
- return this.attempt(JSON.parse(message));
1644
+ return this.attempt(JSON.parse(message2));
1643
1645
  }
1644
1646
  track(delivery) {
1645
1647
  const tracked = delivery.catch((error) => this.logger.error(`Email queue failed: ${error.message}`)).finally(() => this.pending.delete(tracked));
1646
1648
  this.pending.add(tracked);
1647
1649
  }
1648
- async deliver(message, body, id) {
1649
- const error = await this.attempt(message);
1650
+ async deliver(message2, body, id) {
1651
+ const error = await this.attempt(message2);
1650
1652
  if (!error) {
1651
1653
  if (id !== void 0) await this.queue?.remove(id);
1652
1654
  return;
1653
1655
  }
1654
1656
  if (id === void 0 || !this.queue) {
1655
- this.logger.error(`Email "${message.subject}" failed: ${error}`);
1657
+ this.logger.error(`Email "${message2.subject}" failed: ${error}`);
1656
1658
  return;
1657
1659
  }
1658
- this.logger.warn(`Email "${message.subject}" failed, retrying later: ${error}`);
1660
+ this.logger.warn(`Email "${message2.subject}" failed, retrying later: ${error}`);
1659
1661
  await this.queue.update(id, {
1660
1662
  message: body,
1661
1663
  attempts: 1,
@@ -1665,12 +1667,12 @@ var Mailer = class {
1665
1667
  });
1666
1668
  }
1667
1669
  /** `undefined` when sent, else the error. */
1668
- async attempt(message) {
1670
+ async attempt(message2) {
1669
1671
  const adapter = this.adapter;
1670
- const from = message.from ?? adapter.from;
1672
+ const from = message2.from ?? adapter.from;
1671
1673
  if (!from) return "no sender: set `from` on the message or the email adapter";
1672
1674
  try {
1673
- await adapter.send({ ...message, from });
1675
+ await adapter.send({ ...message2, from });
1674
1676
  return void 0;
1675
1677
  } catch (error) {
1676
1678
  return error.message || String(error);
@@ -2002,7 +2004,7 @@ var SLUG_PATTERN = /^[a-z][a-z0-9_-]*$/;
2002
2004
  var FIELD_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
2003
2005
  function validateConfig(config) {
2004
2006
  const issues = [];
2005
- const add = (path, message, hint) => issues.push(hint ? { path, message, hint } : { path, message });
2007
+ const add = (path, message2, hint) => issues.push(hint ? { path, message: message2, hint } : { path, message: message2 });
2006
2008
  if (typeof config !== "object" || config === null) {
2007
2009
  add("config", "must be an object", "export default defineConfig({ ... })");
2008
2010
  return issues;
@@ -3067,9 +3069,9 @@ function localizeSort(fields, sort, locale, locales = []) {
3067
3069
 
3068
3070
  // src/logger.ts
3069
3071
  var consoleLogger = {
3070
- info: (message) => console.info(`[easy-cms] ${message}`),
3071
- warn: (message) => console.warn(`[easy-cms] ${message}`),
3072
- error: (message) => console.error(`[easy-cms] ${message}`)
3072
+ info: (message2) => console.info(`[easy-cms] ${message2}`),
3073
+ warn: (message2) => console.warn(`[easy-cms] ${message2}`),
3074
+ error: (message2) => console.error(`[easy-cms] ${message2}`)
3073
3075
  };
3074
3076
  var silentLogger = { info() {
3075
3077
  }, warn() {
@@ -3248,8 +3250,8 @@ import { BlockList, isIP } from "net";
3248
3250
  var RemoteFileError = class extends Error {
3249
3251
  /** `413` when the file is too large, otherwise `400`. */
3250
3252
  status;
3251
- constructor(message, status = 400) {
3252
- super(message);
3253
+ constructor(message2, status = 400) {
3254
+ super(message2);
3253
3255
  this.name = "RemoteFileError";
3254
3256
  this.status = status;
3255
3257
  }
@@ -4370,8 +4372,8 @@ var EasyCMS = class {
4370
4372
  * the email is queued; it is sent in the background and retried from the queue if sending
4371
4373
  * fails. Without `email` in the config, the email is skipped with a warning.
4372
4374
  */
4373
- sendEmail(message) {
4374
- return this.mailer.send(message);
4375
+ sendEmail(message2) {
4376
+ return this.mailer.send(message2);
4375
4377
  }
4376
4378
  /** Waits for emails being sent, e.g. before a serverless function returns. */
4377
4379
  flushEmails() {
@@ -4530,12 +4532,12 @@ var EasyCMS = class {
4530
4532
  });
4531
4533
  } catch (error) {
4532
4534
  failed++;
4533
- const message = error.message;
4534
- this.logger.error(`Scheduled ${action} of ${parent} ${row.doc} failed: ${message}`);
4535
+ const message2 = error.message;
4536
+ this.logger.error(`Scheduled ${action} of ${parent} ${row.doc} failed: ${message2}`);
4535
4537
  await this.db.update({
4536
4538
  collection: SCHEDULED_JOBS,
4537
4539
  id: row.id,
4538
- data: { ...row, state: "failed", error: message.slice(0, 500) }
4540
+ data: { ...row, state: "failed", error: message2.slice(0, 500) }
4539
4541
  });
4540
4542
  }
4541
4543
  }
@@ -5300,13 +5302,13 @@ function toAdmin(kind, row) {
5300
5302
  body: String(row.body ?? "")
5301
5303
  };
5302
5304
  }
5303
- const message = parse(row.message);
5304
- const to = message.to;
5305
+ const message2 = parse(row.message);
5306
+ const to = message2.to;
5305
5307
  return {
5306
5308
  ...base,
5307
5309
  kind,
5308
5310
  to: Array.isArray(to) ? to.map(String) : typeof to === "string" ? [to] : [],
5309
- subject: String(message.subject ?? "")
5311
+ subject: String(message2.subject ?? "")
5310
5312
  };
5311
5313
  }
5312
5314
  async function listDeliveries(cms, kind, state, page) {
@@ -5381,6 +5383,82 @@ async function deleteFailedDeliveries(cms, kind) {
5381
5383
  return { deleted };
5382
5384
  }
5383
5385
 
5386
+ // src/rest/admin-email.ts
5387
+ var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
5388
+ var LIMIT = 5;
5389
+ var WINDOW = 10 * 6e4;
5390
+ var sentAt = /* @__PURE__ */ new WeakMap();
5391
+ var message = (error) => error instanceof Error ? error.message || error.name : String(error);
5392
+ function adminEmail(cms) {
5393
+ const adapter = cms.config.email;
5394
+ if (!adapter) return { configured: false, name: null, from: null, settings: [], canVerify: false };
5395
+ let settings = [];
5396
+ try {
5397
+ settings = [...adapter.describe?.() ?? []].map((s) => ({
5398
+ key: String(s.key),
5399
+ value: s.value === null || s.value === void 0 ? null : String(s.value),
5400
+ ...s.source ? { source: String(s.source) } : {}
5401
+ }));
5402
+ } catch {
5403
+ }
5404
+ return {
5405
+ configured: true,
5406
+ name: adapter.name ?? "custom",
5407
+ from: adapter.from ?? null,
5408
+ settings,
5409
+ canVerify: typeof adapter.verify === "function"
5410
+ };
5411
+ }
5412
+ async function verifyEmail(cms) {
5413
+ const adapter = cms.config.email;
5414
+ if (!adapter?.verify)
5415
+ return { ok: false, error: "This email adapter cannot check its connection" };
5416
+ try {
5417
+ await adapter.verify();
5418
+ return { ok: true };
5419
+ } catch (error) {
5420
+ return { ok: false, error: message(error) };
5421
+ }
5422
+ }
5423
+ async function sendTestEmail(cms, user, to, locale) {
5424
+ const adapter = cms.config.email;
5425
+ if (!adapter) return { ok: false, error: "Email is not set up (`email` in the config)" };
5426
+ const address = typeof to === "string" && to.trim() ? to.trim() : user.email;
5427
+ if (!EMAIL.test(address))
5428
+ throw new ValidationError("email", [{ field: "to", message: "must be an email address" }]);
5429
+ const times = sentAt.get(cms) ?? /* @__PURE__ */ new Map();
5430
+ sentAt.set(cms, times);
5431
+ const key2 = String(user.id);
5432
+ const now = Date.now();
5433
+ const recent = (times.get(key2) ?? []).filter((t) => now - t < WINDOW);
5434
+ if (recent.length >= LIMIT)
5435
+ throw new TooManyRequestsError("Too many test emails: try again in a few minutes");
5436
+ times.set(key2, [...recent, now]);
5437
+ const from = adapter.from;
5438
+ if (!from) return { ok: false, error: "No sender: set `from` on the email adapter" };
5439
+ const site = cms.config.admin.brand.name || "Easy CMS";
5440
+ const th = locale === "th" || locale !== "en" && cms.config.admin.locale === "th";
5441
+ const time = (/* @__PURE__ */ new Date()).toISOString();
5442
+ const subject = th ? `\u0E2D\u0E35\u0E40\u0E21\u0E25\u0E17\u0E14\u0E2A\u0E2D\u0E1A\u0E08\u0E32\u0E01 ${site}` : `Test email from ${site}`;
5443
+ const text = th ? `\u0E2D\u0E35\u0E40\u0E21\u0E25\u0E19\u0E35\u0E49\u0E2A\u0E48\u0E07\u0E08\u0E32\u0E01\u0E2B\u0E19\u0E49\u0E32 \u0E15\u0E31\u0E49\u0E07\u0E04\u0E48\u0E32 \u2192 \u0E2D\u0E35\u0E40\u0E21\u0E25 \u0E02\u0E2D\u0E07 ${site} \u0E40\u0E1E\u0E37\u0E48\u0E2D\u0E17\u0E14\u0E2A\u0E2D\u0E1A\u0E01\u0E32\u0E23\u0E15\u0E31\u0E49\u0E07\u0E04\u0E48\u0E32
5444
+
5445
+ \u0E2A\u0E48\u0E07\u0E42\u0E14\u0E22: ${user.email}
5446
+ Adapter: ${adapter.name ?? "custom"}
5447
+ \u0E1C\u0E39\u0E49\u0E2A\u0E48\u0E07: ${from}
5448
+ \u0E40\u0E27\u0E25\u0E32: ${time}` : `This email was sent from Settings \u2192 Email in ${site} to test the settings.
5449
+
5450
+ Sent by: ${user.email}
5451
+ Adapter: ${adapter.name ?? "custom"}
5452
+ From: ${from}
5453
+ Time: ${time}`;
5454
+ try {
5455
+ await adapter.send({ to: address, subject, text, from });
5456
+ return { ok: true };
5457
+ } catch (error) {
5458
+ return { ok: false, error: message(error) };
5459
+ }
5460
+ }
5461
+
5384
5462
  // src/rest/admin-schema.ts
5385
5463
  var SETTINGS = /* @__PURE__ */ new Set([USERS, API_KEYS]);
5386
5464
  function componentRef(component) {
@@ -5574,7 +5652,7 @@ async function widgets(cms, user) {
5574
5652
  }
5575
5653
 
5576
5654
  // src/version.ts
5577
- var VERSION = true ? "0.30.0" : "0.0.0-dev";
5655
+ var VERSION = true ? "0.31.0" : "0.0.0-dev";
5578
5656
 
5579
5657
  // src/rest/admin-status.ts
5580
5658
  var DAY = 864e5;
@@ -6014,6 +6092,23 @@ async function route(cms, ctx, method, segments, options) {
6014
6092
  }
6015
6093
  return { body: await cms.runJobs() };
6016
6094
  }
6095
+ if (first === "admin" && second === "email") {
6096
+ if (!ctx.user) throw new UnauthorizedError();
6097
+ if (ctx.user.role !== "admin" || ctx.user.apiKey) throw new ForbiddenError();
6098
+ const [, , action, extra] = segments;
6099
+ if (extra !== void 0) throw new HttpError("Not found", 404);
6100
+ if (action === void 0) {
6101
+ if (method !== "GET") throw methodNotAllowed(ctx, "GET");
6102
+ return { body: adminEmail(cms) };
6103
+ }
6104
+ if (method !== "POST") throw methodNotAllowed(ctx, "POST");
6105
+ if (action === "verify") return { body: await verifyEmail(cms) };
6106
+ if (action === "test") {
6107
+ const body = await readJson(ctx.request);
6108
+ return { body: await sendTestEmail(cms, ctx.user, body.to, body.locale) };
6109
+ }
6110
+ throw new HttpError("Not found", 404);
6111
+ }
6017
6112
  if (first === "admin" && second === "deliveries") {
6018
6113
  if (!ctx.user) throw new UnauthorizedError();
6019
6114
  if (ctx.user.role !== "admin" || ctx.user.apiKey) throw new ForbiddenError();
@@ -6471,11 +6566,11 @@ function errorResponse(cms, error, headers, production) {
6471
6566
  if (error instanceof UnauthorizedError) headers.set("www-authenticate", "Bearer");
6472
6567
  return new Response(JSON.stringify({ errors }), { status: error.status, headers });
6473
6568
  }
6474
- const message = error instanceof Error ? error.message : String(error);
6569
+ const message2 = error instanceof Error ? error.message : String(error);
6475
6570
  cms.logger.error(
6476
- `REST request failed: ${error instanceof Error ? error.stack ?? message : message}`
6571
+ `REST request failed: ${error instanceof Error ? error.stack ?? message2 : message2}`
6477
6572
  );
6478
- const body = { errors: [{ message: production ? "Internal Server Error" : message }] };
6573
+ const body = { errors: [{ message: production ? "Internal Server Error" : message2 }] };
6479
6574
  return new Response(JSON.stringify(body), { status: 500, headers });
6480
6575
  }
6481
6576
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easy-cms/core",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "description": "Embedded, code-first headless CMS for Nuxt and Next.js",
5
5
  "license": "MIT",
6
6
  "type": "module",