From b58a264064d61b623095c06e11142d12d4262fbe Mon Sep 17 00:00:00 2001
From: titanism <101466223+titanism@users.noreply.github.com>
Date: Fri, 30 Aug 2024 16:34:27 -0500
Subject: [PATCH] feat: added ability to set alias-specific and domain-wide
storage quota limitations, drop pretty-bytes in favor of bytes
---
.../web/my-account/ensure-domain-admin.js | 15 ++-
.../web/my-account/update-domain.js | 40 +++++++
.../web/my-account/validate-alias.js | 39 ++++++-
app/models/aliases.js | 25 ++++-
app/models/domains.js | 105 +++++++++++++++---
app/views/api/index.md | 4 +
app/views/my-account/domains/_table.pug | 6 +-
.../my-account/domains/advanced-settings.pug | 38 +++++++
.../my-account/domains/aliases/_form.pug | 16 +++
.../my-account/domains/aliases/_table.pug | 63 +++++++++--
config/index.js | 3 +-
config/phrases.js | 6 +
config/utilities.js | 6 +-
helpers/imap/on-append.js | 5 +-
helpers/monitor-server.js | 5 +-
helpers/parse-payload.js | 41 ++++---
helpers/sync-temporary-mailbox.js | 4 +-
helpers/worker.js | 8 +-
jobs/cleanup-sqlite.js | 8 +-
jobs/cleanup-tmp.js | 3 +-
locales/ar.json | 9 +-
locales/cs.json | 9 +-
locales/da.json | 10 +-
locales/de.json | 9 +-
locales/en.json | 7 +-
locales/es.json | 9 +-
locales/fi.json | 9 +-
locales/fr.json | 10 +-
locales/he.json | 10 +-
locales/hu.json | 9 +-
locales/id.json | 9 +-
locales/it.json | 9 +-
locales/ja.json | 9 +-
locales/ko.json | 9 +-
locales/nl.json | 9 +-
locales/no.json | 9 +-
locales/pl.json | 9 +-
locales/pt.json | 9 +-
locales/ru.json | 9 +-
locales/sv.json | 9 +-
locales/th.json | 9 +-
locales/tr.json | 9 +-
locales/uk.json | 9 +-
locales/vi.json | 10 +-
locales/zh.json | 9 +-
package.json | 1 -
46 files changed, 569 insertions(+), 99 deletions(-)
diff --git a/app/controllers/web/my-account/ensure-domain-admin.js b/app/controllers/web/my-account/ensure-domain-admin.js
index 08668b9138..70aa3c4c93 100644
--- a/app/controllers/web/my-account/ensure-domain-admin.js
+++ b/app/controllers/web/my-account/ensure-domain-admin.js
@@ -6,7 +6,11 @@
const Boom = require('@hapi/boom');
function ensureDomainAdmin(ctx, next) {
- if (ctx.state.domain.group === 'admin') return next();
+ if (ctx.state.domain.group === 'admin') {
+ if (typeof next !== 'function') return;
+ return next();
+ }
+
// if no `ctx.state.domain.group` property exists, then we can try to find it
if (
ctx.state.domain &&
@@ -20,10 +24,15 @@ function ensureDomainAdmin(ctx, next) {
return m.user.toString() === ctx.state.user.id;
return false;
});
- if (member && member.group === 'admin') return next();
+ if (member && member.group === 'admin') {
+ if (typeof next !== 'function') return;
+ return next();
+ }
}
- ctx.throw(Boom.badRequest(ctx.translateError('IS_NOT_ADMIN')));
+ const err = Boom.badRequest(ctx.translateError('IS_NOT_ADMIN'));
+ if (typeof next !== 'function') throw err;
+ ctx.throw(err);
}
module.exports = ensureDomainAdmin;
diff --git a/app/controllers/web/my-account/update-domain.js b/app/controllers/web/my-account/update-domain.js
index e50ee7931f..ca64f28811 100644
--- a/app/controllers/web/my-account/update-domain.js
+++ b/app/controllers/web/my-account/update-domain.js
@@ -5,14 +5,22 @@
const punycode = require('node:punycode');
+const RE2 = require('re2');
const Boom = require('@hapi/boom');
const _ = require('lodash');
+const bytes = require('bytes');
const isSANB = require('is-string-and-not-blank');
const { boolean } = require('boolean');
const { isPort } = require('validator');
const { Domains } = require('#models');
+//
+// NOTE: this regex is not safe according to `safe-regex2` so we use `re2` to wrap it
+// https://github.com/visionmedia/bytes.js/blob/9ddc13b6c66e0cb293616fba246e05db4b6cef4d/index.js#L37C5-L37C16
+//
+const REGEX_BYTES = new RE2(/^((-|\+)?(\d+(?:\.\d+)?)) *(kb|mb|gb|tb|pb)$/i);
+
// eslint-disable-next-line complexity
async function updateDomain(ctx, next) {
ctx.state.domain = await Domains.findById(ctx.state.domain._id);
@@ -53,6 +61,25 @@ async function updateDomain(ctx, next) {
ctx.state.domain.bounce_webhook = undefined;
}
+ //
+ // max_quota_per_alias
+ // (string -> bytes)
+ // (number -> already bytes)
+ //
+ if (
+ typeof ctx.request.body.max_quota_per_alias !== 'undefined' &&
+ typeof ctx.request.body.max_quota_per_alias !== 'string'
+ )
+ throw Boom.badRequest(ctx.translateError('INVALID_BYTES'));
+ else if (isSANB(ctx.request.body.max_quota_per_alias)) {
+ // NOTE: this validation should be moved to `validate-domain.js`
+ if (!REGEX_BYTES.test(ctx.request.body.max_quota_per_alias))
+ throw Boom.badRequest(ctx.translateError('INVALID_BYTES'));
+ ctx.state.domain.max_quota_per_alias = bytes(
+ ctx.request.body.max_quota_per_alias
+ );
+ }
+
// require paid plan (note that the API middleware already does this)
for (const bool of [
'has_adult_content_protection',
@@ -75,6 +102,19 @@ async function updateDomain(ctx, next) {
break;
}
+ case 'max_quota_per_alias': {
+ // NOTE: this validation should be moved to `validate-domain.js`
+ if (
+ !isSANB(ctx.request.body.max_quota_per_alias) ||
+ !REGEX_BYTES.test(ctx.request.body.max_quota_per_alias)
+ )
+ throw Boom.badRequest(ctx.translateError('INVALID_BYTES'));
+ ctx.state.domain.max_quota_per_alias = bytes(
+ ctx.request.body.max_quota_per_alias
+ );
+ break;
+ }
+
case 'spam_scanner_settings': {
// require paid plan
if (ctx.state.domain.plan === 'free')
diff --git a/app/controllers/web/my-account/validate-alias.js b/app/controllers/web/my-account/validate-alias.js
index 0c88d8523f..3b56c2ebd7 100644
--- a/app/controllers/web/my-account/validate-alias.js
+++ b/app/controllers/web/my-account/validate-alias.js
@@ -4,7 +4,9 @@
*/
const Boom = require('@hapi/boom');
+const RE2 = require('re2');
const _ = require('lodash');
+const bytes = require('bytes');
const isSANB = require('is-string-and-not-blank');
const slug = require('speakingurl');
const splitLines = require('split-lines');
@@ -12,8 +14,16 @@ const striptags = require('striptags');
const { boolean } = require('boolean');
// const { isEmail } = require('validator');
+const ensureDomainAdmin = require('./ensure-domain-admin');
+
const config = require('#config');
+//
+// NOTE: this regex is not safe according to `safe-regex2` so we use `re2` to wrap it
+// https://github.com/visionmedia/bytes.js/blob/9ddc13b6c66e0cb293616fba246e05db4b6cef4d/index.js#L37C5-L37C16
+//
+const REGEX_BYTES = new RE2(/^((-|\+)?(\d+(?:\.\d+)?)) *(kb|mb|gb|tb|pb)$/i);
+
// eslint-disable-next-line complexity
function validateAlias(ctx, next) {
const body = _.pick(ctx.request.body, [
@@ -21,9 +31,36 @@ function validateAlias(ctx, next) {
'description',
'labels',
'recipients',
- 'error_code_if_disabled'
+ 'error_code_if_disabled',
+ 'max_quota'
]);
+ //
+ // NOTE: if body includes `max_quota` and user was not an admin of the domain
+ // then throw a permission/forbidden error (either through API or web form manipulation)
+ //
+ if (typeof body.max_quota !== 'undefined') ensureDomainAdmin(ctx); // this will throw an error
+
+ // validate `body.max_quota` if a value was passed
+ if (
+ typeof body.max_quota !== 'undefined' &&
+ typeof body.max_quota !== 'string'
+ )
+ throw Boom.badRequest(ctx.translateError('INVALID_BYTES'));
+
+ // indicates reset of the value
+ if (body.max_quota === '') {
+ body.max_quota = Number.isFinite(ctx.state.domain.max_quota_per_alias)
+ ? ctx.state.domain.max_quota_per_alias
+ : config.maxQuotaPerAlias;
+ } else if (typeof body.max_quota === 'string') {
+ // test against bytes regex
+ if (!REGEX_BYTES.test(body.max_quota))
+ throw Boom.badRequest(ctx.translateError('INVALID_BYTES'));
+ // otherwise convert the value
+ body.max_quota = bytes(body.max_quota);
+ }
+
if (!isSANB(body.name)) delete body.name;
body.description = isSANB(body.description)
diff --git a/app/models/aliases.js b/app/models/aliases.js
index f6ed0ffba0..852381f14e 100644
--- a/app/models/aliases.js
+++ b/app/models/aliases.js
@@ -13,7 +13,6 @@ const isSANB = require('is-string-and-not-blank');
const mongoose = require('mongoose');
const mongooseCommonPlugin = require('mongoose-common-plugin');
const ms = require('ms');
-const prettyBytes = require('pretty-bytes');
const reservedAdminList = require('reserved-email-addresses-list/admin-list.json');
const reservedEmailAddressesList = require('reserved-email-addresses-list');
const slug = require('speakingurl');
@@ -538,6 +537,24 @@ Aliases.pre('save', async function (next) {
)
throw Boom.badRequest(i18n.translateError('INVALID_EMAIL', alias.locale));
+ //
+ // NOTE: we ensure that `alias.max_quota` cannot exceed `domain.max_quota_per_alias`
+ //
+ if (
+ Number.isFinite(domain.max_quota_per_alias) &&
+ Number.isFinite(alias.max_quota) &&
+ alias.max_quota > domain.max_quota_per_alias
+ )
+ throw Boom.badRequest(
+ i18n.translateError(
+ 'ALIAS_QUOTA_EXCEEDS_DOMAIN',
+ alias.locale,
+ `${alias.name}@${domain.name}`,
+ bytes(alias.max_quota),
+ bytes(domain.max_quota_per_alias)
+ )
+ );
+
// determine the domain membership for the user
let member = domain.members.find((member) =>
user
@@ -851,9 +868,9 @@ Aliases.statics.isOverQuota = async function (
if (isOverQuota)
logger.fatal(
new TypeError(
- `Alias ${alias.id} is over quota (${prettyBytes(
- storageUsed + size
- )}/${prettyBytes(maxQuotaPerAlias)})`
+ `Alias ${alias.id} is over quota (${bytes(storageUsed + size)}/${bytes(
+ maxQuotaPerAlias
+ )})`
)
);
diff --git a/app/models/domains.js b/app/models/domains.js
index e4189a2992..b5fa7151b4 100644
--- a/app/models/domains.js
+++ b/app/models/domains.js
@@ -757,9 +757,53 @@ Domains.pre('validate', async function (next) {
_id: { $in: domain.members.map((m) => m.user) }
})
.lean()
- .select('plan email')
+ .select(`id plan email ${config.userFields.maxQuotaPerAlias}`)
.exec();
+ //
+ // NOTE: we ensure that max quota per alias cannot exceed global
+ // max or value higher than any current admin's max quota
+ //
+ const adminUserIds = new Set(
+ domain.members
+ .filter((m) => m.group === 'admin' && m.user)
+ .map((m) =>
+ m?.user?._id
+ ? m.user._id.toString()
+ : m?.user?.id || m.user.toString()
+ )
+ );
+ const adminUsers = users.filter((user) => adminUserIds.has(user.id));
+
+ const adminMaxQuota =
+ adminUsers.length === 0
+ ? config.maxQuotaPerAlias
+ : _.max(
+ adminUsers.map((user) =>
+ typeof user[config.userFields.maxQuotaPerAlias] === 'number'
+ ? user[config.userFields.maxQuotaPerAlias]
+ : config.maxQuotaPerAlias
+ )
+ );
+
+ //
+ // NOTE: hard-coded max of 100 GB (safeguard)
+ //
+ const maxQuota = _.clamp(adminMaxQuota, 0, bytes('100GB'));
+ if (
+ Number.isFinite(domain.max_quota_per_alias) &&
+ domain.max_quota_per_alias > maxQuota
+ )
+ throw Boom.badRequest(
+ i18n.translateError(
+ 'DOMAIN_MAX_QUOTA_EXCEEDS_USER',
+ domain.locale,
+ domain.name,
+ bytes(domain.max_quota_per_alias),
+ bytes(maxQuota)
+ )
+ );
+
const hasValidPlan = users.some((user) => {
if (domain.plan === 'team' && user.plan === 'team') {
return true;
@@ -2217,14 +2261,33 @@ async function ensureUserHasValidPlan(user, locale) {
Domains.statics.ensureUserHasValidPlan = ensureUserHasValidPlan;
-async function getMaxQuota(_id, locale = i18n.config.defaultLocale) {
- const domain = await this.findById(_id)
- .populate(
- 'members.user',
- `_id id plan ${config.userFields.isBanned} ${config.userFields.maxQuotaPerAlias}`
- )
- .lean()
- .exec();
+//
+// NOTE: in order to save db lookups, you can pass an alias object with `max_quota` property
+// instead of a string for `aliasId` value, so it will re-use an existing alias object
+//
+async function getMaxQuota(_id, aliasId, locale = i18n.config.defaultLocale) {
+ if (typeof conn?.models?.Aliases?.findOne !== 'function')
+ throw new TypeError('Aliases model is not ready');
+
+ const [domain, alias] = await Promise.all([
+ this.findById(_id)
+ .populate(
+ 'members.user',
+ `_id id plan ${config.userFields.isBanned} ${config.userFields.maxQuotaPerAlias}`
+ )
+ .lean()
+ .exec(),
+ typeof aliasId === 'string'
+ ? conn.models.Aliases.findOne({ id: aliasId, domain: _id })
+ .select('max_quota')
+ .lean()
+ .exec()
+ : Promise.resolve(
+ typeof aliasId === 'object' && typeof aliasId.max_quota === 'number'
+ ? aliasId
+ : null
+ )
+ ]);
if (!domain)
throw Boom.badRequest(
@@ -2258,6 +2321,9 @@ async function getMaxQuota(_id, locale = i18n.config.defaultLocale) {
);
}
+ if (aliasId && !alias)
+ throw Boom.badRequest(i18n.translateError('ALIAS_DOES_NOT_EXIST', locale));
+
// Filter out a domain's members without actual users
const adminMembers = domain.members.filter(
(member) =>
@@ -2280,12 +2346,8 @@ async function getMaxQuota(_id, locale = i18n.config.defaultLocale) {
);
}
- // TODO: add in form and API endpoint and API docs
- // TODO: if the alias had a limit set on `alias.max_quota` by an admin
- // TODO: if the domain had an alias-wide limit set on `domain.max_quota_per_alias`
-
// go through all admins and get the max value
- const max = _.max(
+ let max = _.max(
adminMembers.map((member) =>
typeof member.user[config.userFields.maxQuotaPerAlias] === 'number'
? member.user[config.userFields.maxQuotaPerAlias]
@@ -2293,6 +2355,21 @@ async function getMaxQuota(_id, locale = i18n.config.defaultLocale) {
)
);
+ // if domain had a max value set, and it was less than `max`, then set new max
+ if (
+ Number.isFinite(domain.max_quota_per_alias) &&
+ domain.max_quota_per_alias < max
+ )
+ max = domain.max_quota_per_alias;
+
+ // if alias passed, and had a max value set, and it was less than `max`, then set new max
+ if (
+ alias &&
+ Number.isFinite(alias.max_quota) &&
+ alias.max_quota_per_alias < max
+ )
+ max = alias.max_quota_per_alias;
+
//
// NOTE: hard-coded max of 100 GB (safeguard)
//
diff --git a/app/views/api/index.md b/app/views/api/index.md
index 9b33fe2cc1..30949e615a 100644
--- a/app/views/api/index.md
+++ b/app/views/api/index.md
@@ -609,6 +609,7 @@ curl BASE_URI/v1/domains \
| `ignore_mx_check` | No | Boolean | Whether to ignore the MX record check on the domain for verification. This is mainly for users that have advanced MX exchange configuration rules and need to keep their existing MX exchange and forward to ours. |
| `retention_days` | No | Number | Integer between `0` and `30` that corresponds to the number of retention days to store outbound SMTP emails once successfully delivered or permanently errored. Defaults to `0`, which means that outbound SMTP emails are purged and redacted immediately for your security. |
| `bounce_webhook` | No | String (URL) or Boolean (false) | The `http://` or `https://` webhook URL of your choice to send bounce webhooks to. We will submit a `POST` request to this URL with information on outbound SMTP failures (e.g. soft or hard failures – so you can manage your subscribers and programmatically manage your outbound email). |
+| `max_quota_per_alias` | No | String | Storage maximum quota for aliases on this domain name. Enter a value such as "1 GB" that will be parsed by [bytes](https://github.com/visionmedia/bytes.js). |
> Example Request:
@@ -656,6 +657,7 @@ curl BASE_URI/v1/domains/DOMAIN_NAME/verify-records \
| `ignore_mx_check` | No | Boolean | Whether to ignore the MX record check on the domain for verification. This is mainly for users that have advanced MX exchange configuration rules and need to keep their existing MX exchange and forward to ours. |
| `retention_days` | No | Number | Integer between `0` and `30` that corresponds to the number of retention days to store outbound SMTP emails once successfully delivered or permanently errored. Defaults to `0`, which means that outbound SMTP emails are purged and redacted immediately for your security. |
| `bounce_webhook` | No | String (URL) or Boolean (false) | The `http://` or `https://` webhook URL of your choice to send bounce webhooks to. We will submit a `POST` request to this URL with information on outbound SMTP failures (e.g. soft or hard failures – so you can manage your subscribers and programmatically manage your outbound email). |
+| `max_quota_per_alias` | No | String | Storage maximum quota for aliases on this domain name. Enter a value such as "1 GB" that will be parsed by [bytes](https://github.com/visionmedia/bytes.js). |
> Example Request:
@@ -812,6 +814,7 @@ curl BASE_URI/v1/domains/DOMAIN_NAME/aliases?pagination=true \
| `has_imap` | No | Boolean | Whether to enable or disable IMAP storage for this alias (if disabled, then inbound emails received will not get stored to [IMAP storage](/blog/docs/best-quantum-safe-encrypted-email-service). If a value is passed, it is converted to a boolean using [boolean](https://github.com/thenativeweb/boolean#quick-start)) |
| `has_pgp` | No | Boolean | Whether to enable or disable [OpenPGP encryption](/faq#do-you-support-openpgpmime-end-to-end-encryption-e2ee-and-web-key-directory-wkd) for [IMAP/POP3/CalDAV encrypted email storage](/blog/docs/best-quantum-safe-encrypted-email-service) using the alias' `public_key`. |
| `public_key` | No | String | OpenPGP public key in ASCII Armor format ([click here to view an example](/.well-known/openpgpkey/hu/mxqp8ogw4jfq83a58pn1wy1ccc1cx3f5.txt); e.g. GPG key for `support@forwardemail.net`). This only applies if you have `has_pgp` set to `true`. [Learn more about end-to-end encryption in our FAQ](/faq#do-you-support-openpgpmime-end-to-end-encryption-e2ee-and-web-key-directory-wkd). |
+| `max_quota` | No | String | Storage maximum quota for this alias. Leave blank to reset to domain current maximum quota or enter a value such as "1 GB" that will be parsed by [bytes](https://github.com/visionmedia/bytes.js). This value can only be adjusted by domain admins. |
> Example Request:
@@ -858,6 +861,7 @@ curl BASE_URI/v1/domains/:domain_name/aliases/:alias_name \
| `has_imap` | No | Boolean | Whether to enable or disable IMAP storage for this alias (if disabled, then inbound emails received will not get stored to [IMAP storage](/blog/docs/best-quantum-safe-encrypted-email-service). If a value is passed, it is converted to a boolean using [boolean](https://github.com/thenativeweb/boolean#quick-start)) |
| `has_pgp` | No | Boolean | Whether to enable or disable [OpenPGP encryption](/faq#do-you-support-openpgpmime-end-to-end-encryption-e2ee-and-web-key-directory-wkd) for [IMAP/POP3/CalDAV encrypted email storage](/blog/docs/best-quantum-safe-encrypted-email-service) using the alias' `public_key`. |
| `public_key` | No | String | OpenPGP public key in ASCII Armor format ([click here to view an example](/.well-known/openpgpkey/hu/mxqp8ogw4jfq83a58pn1wy1ccc1cx3f5.txt); e.g. GPG key for `support@forwardemail.net`). This only applies if you have `has_pgp` set to `true`. [Learn more about end-to-end encryption in our FAQ](/faq#do-you-support-openpgpmime-end-to-end-encryption-e2ee-and-web-key-directory-wkd). |
+| `max_quota` | No | String | Storage maximum quota for this alias. Leave blank to reset to domain current maximum quota or enter a value such as "1 GB" that will be parsed by [bytes](https://github.com/visionmedia/bytes.js). This value can only be adjusted by domain admins. |
> Example Request:
diff --git a/app/views/my-account/domains/_table.pug b/app/views/my-account/domains/_table.pug
index aeae5b5298..feceb69e48 100644
--- a/app/views/my-account/domains/_table.pug
+++ b/app/views/my-account/domains/_table.pug
@@ -88,17 +88,17 @@ mixin renderProgressBar(domain, isMobile = false)
ul.list-unstyled.small.mt-1.mb-0
if storageUsedByAliases > 0
li.text-primary
- = prettyBytes(domain.storage_used_by_aliases)
+ = bytes(domain.storage_used_by_aliases)
= " "
= t("domain")
if pooledStorageUsed > 0
li(class=textClass)
- = prettyBytes(domain.storage_used)
+ = bytes(domain.storage_used)
= " "
= t("pooled")
if availableStorage > 0
li.text-success
- = prettyBytes(domain.storage_quota - domain.storage_used)
+ = bytes(domain.storage_quota - domain.storage_used)
= " "
= t("available")
else
diff --git a/app/views/my-account/domains/advanced-settings.pug b/app/views/my-account/domains/advanced-settings.pug
index c26a252e06..ca3aaf0fa9 100644
--- a/app/views/my-account/domains/advanced-settings.pug
+++ b/app/views/my-account/domains/advanced-settings.pug
@@ -398,6 +398,44 @@ block body
href=l(`/private-business-email?domain=${domain.name}`)
)= t("See all plan features")
+ //- Max Quota Per Alias
+ if domain.group === 'admin'
+ form.ajax-form.confirm-prompt.card.border-themed.mb-3(
+ action=ctx.path,
+ method="POST",
+ disabled=isDisabled
+ )
+ input(type="hidden", name="_method", value="PUT")
+ input(
+ type="hidden",
+ name="_section",
+ value="max_quota_per_alias"
+ )
+ h2.h6.card-header= t("Storage Max Quota Per Alias")
+ .card-body
+ +planRequired(false)
+ .form-group.mb-0
+ input#input-max-quota-per-alias.form-control(
+ type="text",
+ disabled=isDisabled,
+ name="max_quota_per_alias",
+ value=bytes(Number.isFinite(domain.max_quota_per_alias) ? domain.max_quota_per_alias : config.maxQuotaPerAlias)
+ )
+ p.form-text.small.text-black.text-themed-50.mb-0
+ != t('Domain admins can update the maximum storage quota across all aliases. Enter a human-friendly string such as "1GB" – note that we use bytes to parse the value to a Number. Domain admins can set maximum storage quota limits on a per alias basis by editing the alias directly.')
+ .card-footer.text-right
+ ul.list-inline.mb-0
+ li.list-inline-item
+ button.confirm-prompt.btn.btn-lg.btn-secondary(
+ type="reset",
+ disabled=isDisabled
+ )= t("Reset")
+ li.list-inline-item
+ button.btn.btn-lg.btn-primary(
+ type="submit",
+ disabled=isDisabled
+ )= t("Save")
+
//- Ignore MX Check
if domain.group === 'admin'
form.ajax-form.confirm-prompt.card.border-themed.mb-3(
diff --git a/app/views/my-account/domains/aliases/_form.pug b/app/views/my-account/domains/aliases/_form.pug
index 38636562c9..2bac12dad4 100644
--- a/app/views/my-account/domains/aliases/_form.pug
+++ b/app/views/my-account/domains/aliases/_form.pug
@@ -121,6 +121,22 @@ if !domain || !domain.is_global && (!isUbuntuAlias || domain.group === 'admin')
= t("You can have this and forwarding recipients enabled at the same time.")
= " "
!= t('If you would like to learn more about storage, please click here to read our deep dive on Encrypted Email.', l("/blog/docs/best-quantum-safe-encrypted-email-service"))
+
+//- if user was admin of the domain then allow them to update storage quota
+if domain && domain.group === 'admin'
+ .form-group
+ label(for="input-max-quota")
+ = t("Storage Max Quota")
+ = " "
+ span.text-muted= t("(optional)")
+ input#input-max-quota.form-control(
+ type="text",
+ name="max_quota",
+ value=bytes(alias && Number.isFinite(alias.max_quota) ? alias.max_quota : Number.isFinite(domain.max_quota_per_alias) ? domain.max_quota_per_alias : config.maxQuotaPerAlias)
+ )
+ .alert.alert-secondary.small.mt-3
+ != t('Domain admins can update the storage quota for this alias. Leave blank and hit save to reset it to the current domain\'s maximum storage quota of %s. Enter a human-friendly string such as "1GB" – note that we use bytes to parse the value to a Number. If you would like to update the max storage quota across all aliases for this domain, then go to the domain\'s Settings page.', bytes(Number.isFinite(domain.max_quota_per_alias) ? domain.max_quota_per_alias : config.maxQuotaPerAlias))
+
.form-group
label(for="textarea-alias-recipients")
= t("Forwarding Recipients")
diff --git a/app/views/my-account/domains/aliases/_table.pug b/app/views/my-account/domains/aliases/_table.pug
index 70e223f0e4..74575cf152 100644
--- a/app/views/my-account/domains/aliases/_table.pug
+++ b/app/views/my-account/domains/aliases/_table.pug
@@ -3,11 +3,60 @@ include ../../../_pagination
mixin renderProgressBar(domain, alias, isMobile = false)
if (typeof domain.storage_used === 'number' && typeof domain.storage_used_by_aliases === 'number' && typeof domain.storage_quota === 'number')
- - alias.storage_used = typeof alias.storage_used === "number" ? alias.storage_used : 0;
+ - alias.storage_used = typeof alias.storage_used === "number" ? alias.storage_used : 0;0
- const storageUsedBySpecificAlias = Math.round((alias.storage_used / domain.storage_quota) * 100);
- const storageUsedByAliases = Math.round(((domain.storage_used_by_aliases - alias.storage_used) / domain.storage_quota) * 100);
- const pooledStorageUsed = Math.round(((domain.storage_used - domain.storage_used_by_aliases - alias.storage_used) / domain.storage_quota) * 100);
- const availableStorage = Math.round(((domain.storage_quota - domain.storage_used) / domain.storage_quota) * 100);
+
+ //- if alias had a specific limit then render that as its own progress bar
+ if Number.isFinite(alias.max_quota) && alias.max_quota < domain.storage_quota
+ - let aliasPercentage = Math.round((alias.storage_used / alias.max_quota) * 100);
+ if alias.storage_used >= alias.max_quota
+ - aliasPercentage = 100;
+ - let aliasProgressClass = "bg-primary";
+ - let aliasTextClass = "text-muted";
+ if aliasPercentage >= 80
+ - aliasProgressClass = "bg-danger";
+ - aliasTextClass = "text-danger";
+ else if aliasPercentage >= 60
+ - aliasProgressClass = "bg-warning";
+ - aliasTextClass = "text-warning";
+ if aliasPercentage > 0
+ .progress.bg-light.border.border-dark
+ //- render alias portion
+ .progress-bar.min-width-25(
+ class=aliasProgressClass,
+ data-width=`${aliasPercentage}%`,
+ aria-valuenow=aliasPercentage,
+ aria-valuemin=0,
+ aria-valuemax=100,
+ data-toggle="tooltip",
+ data-title=t("Storage used by this alias"),
+ data-placement="bottom",
+ role="button"
+ )= `${aliasPercentage}%`
+ //- render remaining portion
+ if aliasPercentage !== 100
+ .progress-bar.bg-success.min-width-25(
+ data-width=`${100 - aliasPercentage}%`,
+ aria-valuenow=100 - aliasPercentage,
+ aria-valuemin=0,
+ aria-valuemax=100,
+ data-toggle="tooltip",
+ data-title=t("Available storage"),
+ data-placement="bottom",
+ role="button"
+ )= `${100 - aliasPercentage}%`
+ ul.list-unstyled.small.mt-1
+ li(class=aliasTextClass)
+ = bytes(alias.storage_used)
+ = " / "
+ = bytes(alias.max_quota)
+ = " "
+ = t("alias quota")
+
+ //- render normal progress bar
- let percentage = Math.round((domain.storage_used / domain.storage_quota) * 100);
if domain.storage_used >= domain.storage_quota
- percentage = 100;
@@ -47,7 +96,7 @@ mixin renderProgressBar(domain, alias, isMobile = false)
else
//- Render Storage Used by Specific Alias
if alias.storage_used > 0
- .progress-bar.progress-bar-striped.progress-bar-animated.bg-primary.min-width-25(
+ .progress-bar.progress-bar-striped.progress-bar-animated.bg-info.min-width-25(
data-width=`${storageUsedBySpecificAlias || 1}%`,
aria-valuenow=storageUsedBySpecificAlias || 1,
aria-valuemin=0,
@@ -99,23 +148,23 @@ mixin renderProgressBar(domain, alias, isMobile = false)
)= `${availableStorage}%`
ul.list-unstyled.small.mt-1.mb-0
if alias.storage_used > 0
- li.text-primary
- = prettyBytes(alias.storage_used)
+ li.text-info
+ = bytes(alias.storage_used)
= " "
= t("alias")
if storageUsedByAliases > 0
li.text-primary
- = prettyBytes(domain.storage_used_by_aliases - alias.storage_used)
+ = bytes(domain.storage_used_by_aliases - alias.storage_used)
= " "
= t("domain")
if pooledStorageUsed > 0
li(class=textClass)
- = prettyBytes(domain.storage_used)
+ = bytes(domain.storage_used)
= " "
= t("pooled")
if availableStorage > 0
li.text-success
- = prettyBytes(domain.storage_quota - domain.storage_used)
+ = bytes(domain.storage_quota - domain.storage_used)
= " "
= t("available")
else
diff --git a/config/index.js b/config/index.js
index 07f18135b1..673ab56a7e 100644
--- a/config/index.js
+++ b/config/index.js
@@ -1542,7 +1542,8 @@ config.views.locals.config = _.pick(config, [
'metaTitleAffix',
'modulusLength',
'openPGPKey',
- 'ubuntuTeamMapping'
+ 'ubuntuTeamMapping',
+ 'maxQuotaPerAlias'
]);
//
diff --git a/config/phrases.js b/config/phrases.js
index b9c23af40a..6768e51660 100644
--- a/config/phrases.js
+++ b/config/phrases.js
@@ -20,6 +20,12 @@ for (const key of Object.keys(statuses.message)) {
}
module.exports = {
+ INVALID_BYTES:
+ 'Bytes were invalid, must be a string such as "1 GB" or "100 MB".',
+ ALIAS_QUOTA_EXCEEDS_DOMAIN:
+ "The quota for %s of %s exceeds the domain's maximum quota of %s.",
+ DOMAIN_MAX_QUOTA_EXCEEDS_USER:
+ 'The quota for %s of %s exceeds the maximum quota of from admins of the domain.',
PAGINATION_CHECK_SUBJECT:
'Notice: API pagination required starting November 1st',
PAGINATION_CHECK_MESSAGE:
diff --git a/config/utilities.js b/config/utilities.js
index 7f71d2d28c..6ec6fd1187 100644
--- a/config/utilities.js
+++ b/config/utilities.js
@@ -12,6 +12,7 @@ const _ = require('lodash');
const ajc = require('array-join-conjunction');
const ansiHTML = require('ansi-html-community');
const arrayJoinConjunction = require('array-join-conjunction');
+const bytes = require('bytes');
const capitalize = require('capitalize');
const dashify = require('dashify');
const dayjs = require('dayjs-with-plugins');
@@ -25,7 +26,6 @@ const isSANB = require('is-string-and-not-blank');
const ms = require('ms');
const numeral = require('numeral');
const pluralize = require('pluralize');
-const prettyBytes = require('pretty-bytes');
const prettyMilliseconds = require('pretty-ms');
const shortID = require('mongodb-short-id');
const splitLines = require('split-lines');
@@ -1232,7 +1232,6 @@ module.exports = {
validator,
ms,
prettyMilliseconds,
- prettyBytes,
developerDocs,
platforms,
arrayJoinConjunction,
@@ -1242,5 +1241,6 @@ module.exports = {
randomstring,
useCases,
decrypt,
- punycode
+ punycode,
+ bytes
};
diff --git a/helpers/imap/on-append.js b/helpers/imap/on-append.js
index 3f68aa70ea..df38cbe8d6 100644
--- a/helpers/imap/on-append.js
+++ b/helpers/imap/on-append.js
@@ -371,7 +371,10 @@ async function onAppend(path, flags, date, raw, session, fn) {
// store reference for cleanup
mimeTreeData = mimeTree;
- const maxQuotaPerAlias = await Domains.getMaxQuota(session.user.domain_id);
+ const maxQuotaPerAlias = await Domains.getMaxQuota(
+ session.user.domain_id,
+ session.user.alias_id
+ );
const exceedsQuota = storageUsed + size > maxQuotaPerAlias;
if (exceedsQuota)
diff --git a/helpers/monitor-server.js b/helpers/monitor-server.js
index d5e7513de7..77e97f2717 100644
--- a/helpers/monitor-server.js
+++ b/helpers/monitor-server.js
@@ -26,7 +26,6 @@ const checkDiskSpace = require('check-disk-space').default;
const ip = require('ip');
const ms = require('ms');
const osu = require('node-os-utils');
-const prettyBytes = require('pretty-bytes');
const env = require('#config/env');
const logger = require('#helpers/logger');
@@ -97,7 +96,7 @@ async function check() {
}
const err = new TypeError(message);
- err.memoryUsed = prettyBytes(memoryInfo.heapTotal);
+ err.memoryUsed = bytes(memoryInfo.heapTotal);
err.memoryInfo = memoryInfo;
logger.fatal(err);
@@ -118,7 +117,7 @@ async function check() {
// .then(() => {
// // alert admins
// const err = new TypeError(
- // `New snapshot created on ${HOSTNAME} (${IP_ADDRESS} for ${prettyBytes(
+ // `New snapshot created on ${HOSTNAME} (${IP_ADDRESS} for ${bytes(
// memoryInfo.heapTotal
// )} heap size`
// );
diff --git a/helpers/parse-payload.js b/helpers/parse-payload.js
index d5a12e985c..313db6e973 100644
--- a/helpers/parse-payload.js
+++ b/helpers/parse-payload.js
@@ -26,7 +26,6 @@ const pEvent = require('p-event');
const pMap = require('p-map');
const parseErr = require('parse-err');
const pify = require('pify');
-const prettyBytes = require('pretty-bytes');
const safeStringify = require('fast-safe-stringify');
const { Iconv } = require('iconv');
const { isEmail } = require('validator');
@@ -654,7 +653,7 @@ async function parsePayload(data, ws) {
`id email ${config.userFields.isBanned} ${config.lastLocaleField}`
)
.select(
- 'id has_imap has_pgp public_key storage_location user is_enabled name domain'
+ 'id has_imap has_pgp public_key storage_location user is_enabled name domain max_quota'
)
.lean()
.exec();
@@ -718,9 +717,7 @@ async function parsePayload(data, ws) {
if (isOverQuota) {
const err = new TypeError(
- `${
- session.user.username
- } has exceeded quota with ${prettyBytes(
+ `${session.user.username} has exceeded quota with ${bytes(
storageUsed
)} storage used`
);
@@ -739,15 +736,14 @@ async function parsePayload(data, ws) {
}
const maxQuotaPerAlias = await Domains.getMaxQuota(
- alias.domain.id
+ alias.domain.id,
+ alias
);
const exceedsQuota = storageUsed + byteLength > maxQuotaPerAlias;
if (exceedsQuota) {
const err = new TypeError(
- `${
- session.user.username
- } has exceeded quota with ${prettyBytes(
+ `${session.user.username} has exceeded quota with ${bytes(
storageUsed
)} storage used`
);
@@ -798,7 +794,7 @@ async function parsePayload(data, ws) {
// 1) Senders that we consider to be "trusted" as a source of truth
if (size >= bytes('100GB')) {
const err = new SMTPError(
- `${sender} limited to 100 GB with current of ${prettyBytes(
+ `${sender} limited to 100 GB with current of ${bytes(
size
)} from ${count} messages`,
{ responseCode: 421 }
@@ -817,7 +813,7 @@ async function parsePayload(data, ws) {
// 2) Senders that are allowlisted are limited to sending 10 GB per day.
if (size >= bytes('10GB')) {
const err = new SMTPError(
- `${sender} limited to 10 GB with current of ${prettyBytes(
+ `${sender} limited to 10 GB with current of ${bytes(
size
)} from ${count} messages`,
{ responseCode: 421 }
@@ -829,7 +825,7 @@ async function parsePayload(data, ws) {
// 3) All other Senders are limited to sending 1 GB and/or 1000 messages per day.
} else if (size >= bytes('1GB') || count >= 1000) {
const err = new SMTPError(
- `#3 ${sender} limited with current of ${prettyBytes(
+ `#3 ${sender} limited with current of ${bytes(
size
)} from ${count} messages`,
{ responseCode: 421 }
@@ -842,7 +838,7 @@ async function parsePayload(data, ws) {
} else if (size >= bytes('1GB') || count >= 1000) {
// 3) All other Senders are limited to sending 1 GB and/or 1000 messages per day.
const err = new SMTPError(
- `#3 ${sender} limited with current of ${prettyBytes(
+ `#3 ${sender} limited with current of ${bytes(
size
)} from ${count} messages`,
{ responseCode: 421 }
@@ -866,7 +862,7 @@ async function parsePayload(data, ws) {
if (specific.size >= bytes('1GB') || specific.count >= 1000) {
const err = new SMTPError(
- `${sender} limited with current of ${prettyBytes(
+ `${sender} limited with current of ${bytes(
specific.size
)} from ${specific.count} messages to ${root}`,
{
@@ -888,7 +884,7 @@ async function parsePayload(data, ws) {
const diskSpace = await checkDiskSpace(storagePath);
if (diskSpace.free < spaceRequired)
throw new TypeError(
- `Needed ${prettyBytes(spaceRequired)} but only ${prettyBytes(
+ `Needed ${bytes(spaceRequired)} but only ${bytes(
diskSpace.free
)} was available`
);
@@ -1224,7 +1220,8 @@ async function parsePayload(data, ws) {
const diskSpace = await checkDiskSpace(storagePath);
const maxQuotaPerAlias = await Domains.getMaxQuota(
- payload.session.user.domain_id
+ payload.session.user.domain_id,
+ payload.session.user.alias_id
);
// slight 2x overhead for backups
@@ -1232,7 +1229,7 @@ async function parsePayload(data, ws) {
if (diskSpace.free < spaceRequired)
throw new TypeError(
- `Needed ${prettyBytes(spaceRequired)} but only ${prettyBytes(
+ `Needed ${bytes(spaceRequired)} but only ${bytes(
diskSpace.free
)} was available`
);
@@ -1473,7 +1470,8 @@ async function parsePayload(data, ws) {
});
const diskSpace = await checkDiskSpace(storagePath);
const maxQuotaPerAlias = await Domains.getMaxQuota(
- payload.session.user.domain_id
+ payload.session.user.domain_id,
+ payload.session.user.alias_id
);
let stats;
@@ -1497,7 +1495,7 @@ async function parsePayload(data, ws) {
if (diskSpace.free < spaceRequired)
throw new TypeError(
- `Needed ${prettyBytes(spaceRequired)} but only ${prettyBytes(
+ `Needed ${bytes(spaceRequired)} but only ${bytes(
diskSpace.free
)} was available`
);
@@ -1659,13 +1657,14 @@ async function parsePayload(data, ws) {
// slight 2x overhead for backups
const maxQuotaPerAlias = await Domains.getMaxQuota(
- payload.session.user.domain_id
+ payload.session.user.domain_id,
+ payload.session.user.alias_id
);
const spaceRequired = maxQuotaPerAlias * 2;
if (config.env !== 'development' && diskSpace.free < spaceRequired)
throw new TypeError(
- `Needed ${prettyBytes(spaceRequired)} but only ${prettyBytes(
+ `Needed ${bytes(spaceRequired)} but only ${bytes(
diskSpace.free
)} was available`
);
diff --git a/helpers/sync-temporary-mailbox.js b/helpers/sync-temporary-mailbox.js
index 3aac0acea5..2197eaa807 100644
--- a/helpers/sync-temporary-mailbox.js
+++ b/helpers/sync-temporary-mailbox.js
@@ -6,10 +6,10 @@
const { Buffer } = require('node:buffer');
const _ = require('lodash');
+const bytes = require('bytes');
const checkDiskSpace = require('check-disk-space').default;
const ms = require('ms');
const pify = require('pify');
-const prettyBytes = require('pretty-bytes');
const { Builder } = require('json-sql');
const getPathToDatabase = require('./get-path-to-database');
@@ -99,7 +99,7 @@ async function syncTemporaryMailbox(session) {
const diskSpace = await checkDiskSpace(storagePath);
if (diskSpace.free < spaceRequired)
throw new TypeError(
- `Needed ${prettyBytes(spaceRequired)} but only ${prettyBytes(
+ `Needed ${bytes(spaceRequired)} but only ${bytes(
diskSpace.free
)} was available`
);
diff --git a/helpers/worker.js b/helpers/worker.js
index bacf38510d..211994385f 100644
--- a/helpers/worker.js
+++ b/helpers/worker.js
@@ -20,6 +20,7 @@ const Redis = require('@ladjs/redis');
const _ = require('lodash');
const archiver = require('archiver');
const archiverZipEncrypted = require('archiver-zip-encrypted');
+const bytes = require('bytes');
const checkDiskSpace = require('check-disk-space').default;
const dashify = require('dashify');
const getStream = require('get-stream');
@@ -27,7 +28,6 @@ const hasha = require('hasha');
const mongoose = require('mongoose');
const ms = require('ms');
const pWaitFor = require('p-wait-for');
-const prettyBytes = require('pretty-bytes');
const sharedConfig = require('@ladjs/shared-config');
const splitLines = require('split-lines');
const {
@@ -159,7 +159,7 @@ async function rekey(payload) {
const diskSpace = await checkDiskSpace(storagePath);
if (diskSpace.free < spaceRequired)
throw new TypeError(
- `Needed ${prettyBytes(spaceRequired)} but only ${prettyBytes(
+ `Needed ${bytes(spaceRequired)} but only ${bytes(
diskSpace.free
)} was available`
);
@@ -411,7 +411,7 @@ async function backup(payload) {
if (diskSpace.free < spaceRequired)
throw new TypeError(
- `Needed ${prettyBytes(spaceRequired)} but only ${prettyBytes(
+ `Needed ${bytes(spaceRequired)} but only ${bytes(
diskSpace.free
)} was available`
);
@@ -809,7 +809,7 @@ async function backup(payload) {
if (diskSpace.free < spaceRequired)
throw new TypeError(
- `Needed ${prettyBytes(spaceRequired)} but only ${prettyBytes(
+ `Needed ${bytes(spaceRequired)} but only ${bytes(
diskSpace.free
)} was available`
);
diff --git a/jobs/cleanup-sqlite.js b/jobs/cleanup-sqlite.js
index 28cced0b43..1b4f9e0886 100644
--- a/jobs/cleanup-sqlite.js
+++ b/jobs/cleanup-sqlite.js
@@ -18,12 +18,12 @@ require('#config/mongoose');
const Graceful = require('@ladjs/graceful');
const Redis = require('@ladjs/redis');
const _ = require('lodash');
+const bytes = require('bytes');
const dayjs = require('dayjs-with-plugins');
const mongoose = require('mongoose');
const ms = require('ms');
const pMapSeries = require('p-map-series');
const parseErr = require('parse-err');
-const prettyBytes = require('pretty-bytes');
const sharedConfig = require('@ladjs/shared-config');
const Aliases = require('#models/aliases');
@@ -225,7 +225,7 @@ const mountDir = config.env === 'production' ? '/mnt' : tmpdir;
const [storageUsed, maxQuotaPerAlias] = await Promise.all([
Aliases.getStorageUsed(alias),
- Domains.getMaxQuota(alias.domain)
+ Domains.getMaxQuota(alias.domain, alias)
]);
const percentageUsed = Math.round(
@@ -277,8 +277,8 @@ const mountDir = config.env === 'production' ? '/mnt' : tmpdir;
'STORAGE_THRESHOLD_MESSAGE',
locale,
percentageUsed,
- prettyBytes(storageUsed),
- prettyBytes(maxQuotaPerAlias),
+ bytes(storageUsed),
+ bytes(maxQuotaPerAlias),
`${config.urls.web}/${locale}/my-account/billing`
);
diff --git a/jobs/cleanup-tmp.js b/jobs/cleanup-tmp.js
index ab33c389ef..882993ac16 100644
--- a/jobs/cleanup-tmp.js
+++ b/jobs/cleanup-tmp.js
@@ -21,7 +21,6 @@ const ip = require('ip');
const mongoose = require('mongoose');
const ms = require('ms');
const parseErr = require('parse-err');
-const prettyBytes = require('pretty-bytes');
const { getDirSize } = require('fast-dir-size');
const config = require('#config');
@@ -51,7 +50,7 @@ graceful.listen();
//
const size = await getDirSize(TMP_DIR);
if (size >= bytes('5GB')) {
- const subject = `${TMP_DIR} on ${os.hostname()} (${IP_ADDRESS}) is ${prettyBytes(
+ const subject = `${TMP_DIR} on ${os.hostname()} (${IP_ADDRESS}) is ${bytes(
size
)}`;
emailHelper({
diff --git a/locales/ar.json b/locales/ar.json
index 1ea6d5da8f..0f65c8a280 100644
--- a/locales/ar.json
+++ b/locales/ar.json
@@ -10310,5 +10310,12 @@
"Open-source Email Hosting Service": "خدمة استضافة البريد الإلكتروني مفتوحة المصدر",
"Custom Domain Email Forwarding": "إعادة توجيه البريد الإلكتروني إلى نطاق مخصص",
"Suggested": "مقترح",
- "Its description from its website is:": "وصفه من موقعه على الإنترنت هو:"
+ "Its description from its website is:": "وصفه من موقعه على الإنترنت هو:",
+ "Storage Max Quota Per Alias": "الحد الأقصى لحصة التخزين لكل اسم مستعار",
+ "Domain admins can update the maximum storage quota across all aliases. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. Domain admins can set maximum storage quota limits on a per alias basis by editing the alias directly.": "يمكن لمسؤولي المجال تحديث الحد الأقصى لحصة التخزين عبر جميع الأسماء المستعارة. أدخل سلسلة سهلة الفهم مثل "1 جيجابايت" - لاحظ أننا نستخدم bytes لتحليل القيمة إلى رقم. يمكن لمسؤولي المجال تعيين حدود الحد الأقصى لحصة التخزين على أساس كل اسم مستعار عن طريق تحرير الاسم المستعار مباشرة.",
+ "Storage Max Quota": "الحد الأقصى لحصة التخزين",
+ "Domain admins can update the storage quota for this alias. Leave blank and hit save to reset it to the current domain's maximum storage quota of %s. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. If you would like to update the max storage quota across all aliases for this domain, then go to the domain's Settings page.": "يمكن لمسؤولي المجال تحديث حصة التخزين لهذا الاسم المستعار. اتركه فارغًا واضغط على حفظ لإعادة تعيينه إلى الحد الأقصى لحصة التخزين للمجال الحالي بنسبة %s . أدخل سلسلة سهلة الفهم مثل "1 جيجابايت" - لاحظ أننا نستخدم bytes لتحليل القيمة إلى رقم. إذا كنت ترغب في تحديث الحد الأقصى لحصة التخزين عبر جميع الأسماء المستعارة لهذا المجال، فانتقل إلى صفحة إعدادات المجال.",
+ "The quota for %s of %s exceeds the domain's maximum quota of %s.": "حصة %s من %s تتجاوز الحد الأقصى لحصة النطاق وهي %s .",
+ "The quota for %s of %s exceeds the maximum quota of from admins of the domain.": "الحصة المخصصة لـ %s من %s تتجاوز الحصة القصوى المخصصة لـ من مسؤولي المجال.",
+ "Bytes were invalid, must be a string such as \"1 GB\" or \"100 MB\".": "كانت البايتات غير صالحة، يجب أن تكون سلسلة مثل \"1 جيجابايت\" أو \"100 ميجابايت\"."
}
\ No newline at end of file
diff --git a/locales/cs.json b/locales/cs.json
index bccac9b0cf..95c5c81856 100644
--- a/locales/cs.json
+++ b/locales/cs.json
@@ -10310,5 +10310,12 @@
"Open-source Email Hosting Service": "Open-source e-mailová hostingová služba",
"Custom Domain Email Forwarding": "Přeposílání e-mailů na vlastní doménu",
"Suggested": "Doporučeno",
- "Its description from its website is:": "Jeho popis z jeho webu je:"
+ "Its description from its website is:": "Jeho popis z jeho webu je:",
+ "Storage Max Quota Per Alias": "Maximální kvóta úložiště na alias",
+ "Domain admins can update the maximum storage quota across all aliases. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. Domain admins can set maximum storage quota limits on a per alias basis by editing the alias directly.": "Správci domény mohou aktualizovat maximální kvótu úložiště pro všechny aliasy. Zadejte řetězec vhodný pro člověka, například „1 GB“ – všimněte si, že k analýze hodnoty na číslo používáme bytes . Správci domény mohou nastavit maximální limity kvóty úložiště na základě aliasu přímou úpravou aliasu.",
+ "Storage Max Quota": "Maximální kvóta úložiště",
+ "Domain admins can update the storage quota for this alias. Leave blank and hit save to reset it to the current domain's maximum storage quota of %s. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. If you would like to update the max storage quota across all aliases for this domain, then go to the domain's Settings page.": "Správci domény mohou aktualizovat kvótu úložiště pro tento alias. Ponechte prázdné a stisknutím tlačítka Uložit jej resetujte na maximální kvótu úložiště aktuální domény %s . Zadejte řetězec vhodný pro člověka, například „1 GB“ – všimněte si, že k analýze hodnoty na číslo používáme bytes . Pokud byste chtěli aktualizovat maximální kvótu úložiště pro všechny aliasy pro tuto doménu, přejděte na stránku Nastavení domény.",
+ "The quota for %s of %s exceeds the domain's maximum quota of %s.": "Kvóta pro %s z %s překračuje maximální kvótu domény %s .",
+ "The quota for %s of %s exceeds the maximum quota of from admins of the domain.": "Kvóta pro %s z %s překračuje maximální kvótu od administrátorů domény.",
+ "Bytes were invalid, must be a string such as \"1 GB\" or \"100 MB\".": "Bajty byly neplatné, musí to být řetězec, například „1 GB“ nebo „100 MB“."
}
\ No newline at end of file
diff --git a/locales/da.json b/locales/da.json
index c24ec79145..f4f558a810 100644
--- a/locales/da.json
+++ b/locales/da.json
@@ -7297,5 +7297,13 @@
"Legacy Free Guide": "Legacy gratis guide",
"Suggested": "Foreslået",
"Its description from its website is:": "Beskrivelsen fra dens hjemmeside er:",
- "Compare %s with %d email services": "Sammenlign %s med %d e-mail-tjenester"
+ "Compare %s with %d email services": "Sammenlign %s med %d e-mail-tjenester",
+ "Storage Max Quota Per Alias": "Lager Max kvote pr. alias",
+ "Domain admins can update the maximum storage quota across all aliases. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. Domain admins can set maximum storage quota limits on a per alias basis by editing the alias directly.": "Domæneadministratorer kan opdatere den maksimale lagerkvote på tværs af alle aliasser. Indtast en menneskevenlig streng såsom "1GB" – bemærk, at vi bruger bytes til at parse værdien til et tal. Domæneadministratorer kan indstille maksimale lagerkvotegrænser på aliasbasis ved at redigere aliaset direkte.",
+ "Storage Max Quota": "Max lagringskvote",
+ "Domain admins can update the storage quota for this alias. Leave blank and hit save to reset it to the current domain's maximum storage quota of %s. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. If you would like to update the max storage quota across all aliases for this domain, then go to the domain's Settings page.": "Domæneadministratorer kan opdatere lagerkvoten for dette alias. Lad stå tomt, og tryk på Gem for at nulstille det til det aktuelle domænes maksimale lagerkvote på %s . Indtast en menneskevenlig streng såsom "1GB" – bemærk, at vi bruger bytes til at parse værdien til et tal. Hvis du gerne vil opdatere den maksimale lagerkvote på tværs af alle aliasser for dette domæne, skal du gå til domænets Indstillinger-side.",
+ "Verified": "Verificeret",
+ "The quota for %s of %s exceeds the domain's maximum quota of %s.": "Kvoten for %s af %s overstiger domænets maksimale kvote på %s .",
+ "The quota for %s of %s exceeds the maximum quota of from admins of the domain.": "Kvoten for %s af %s overstiger den maksimale kvote på fra domænets administratorer.",
+ "Bytes were invalid, must be a string such as \"1 GB\" or \"100 MB\".": "Bytes var ugyldige, skal være en streng såsom \"1 GB\" eller \"100 MB\"."
}
\ No newline at end of file
diff --git a/locales/de.json b/locales/de.json
index 1dbda2f46c..3307bf79e5 100644
--- a/locales/de.json
+++ b/locales/de.json
@@ -9349,5 +9349,12 @@
"Open-source Email Hosting Service": "Open-Source-E-Mail-Hosting-Dienst",
"Custom Domain Email Forwarding": "Benutzerdefinierte Domänen-E-Mail-Weiterleitung",
"Suggested": "Empfohlen",
- "Its description from its website is:": "Die Beschreibung auf der Website lautet:"
+ "Its description from its website is:": "Die Beschreibung auf der Website lautet:",
+ "Storage Max Quota Per Alias": "Maximales Speicherkontingent pro Alias",
+ "Domain admins can update the maximum storage quota across all aliases. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. Domain admins can set maximum storage quota limits on a per alias basis by editing the alias directly.": "Domänenadministratoren können das maximale Speicherkontingent für alle Aliase aktualisieren. Geben Sie eine benutzerfreundliche Zeichenfolge wie „1 GB“ ein. Beachten Sie, dass wir bytes verwenden, um den Wert in eine Zahl umzuwandeln. Domänenadministratoren können maximale Speicherkontingentgrenzen für jeden Alias festlegen, indem sie den Alias direkt bearbeiten.",
+ "Storage Max Quota": "Maximales Speicherkontingent",
+ "Domain admins can update the storage quota for this alias. Leave blank and hit save to reset it to the current domain's maximum storage quota of %s. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. If you would like to update the max storage quota across all aliases for this domain, then go to the domain's Settings page.": "Domänenadministratoren können das Speicherkontingent für diesen Alias aktualisieren. Lassen Sie das Feld leer und klicken Sie auf „Speichern“, um es auf das maximale Speicherkontingent der aktuellen Domäne von %s zurückzusetzen. Geben Sie eine benutzerfreundliche Zeichenfolge wie „1 GB“ ein. Beachten Sie, dass wir bytes verwenden, um den Wert in eine Zahl umzuwandeln. Wenn Sie das maximale Speicherkontingent für alle Aliase dieser Domäne aktualisieren möchten, wechseln Sie zur Seite „Einstellungen“ der Domäne.",
+ "The quota for %s of %s exceeds the domain's maximum quota of %s.": "Das Kontingent für %s von %s überschreitet das maximale Kontingent der Domäne von %s .",
+ "The quota for %s of %s exceeds the maximum quota of from admins of the domain.": "Das Kontingent für %s von %s überschreitet das maximale Kontingent von von Admins der Domäne.",
+ "Bytes were invalid, must be a string such as \"1 GB\" or \"100 MB\".": "Die Bytes waren ungültig. Es muss sich um eine Zeichenfolge wie „1 GB“ oder „100 MB“ handeln."
}
\ No newline at end of file
diff --git a/locales/en.json b/locales/en.json
index 397ed09427..01eb7d4d38 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -6693,5 +6693,10 @@
"If your ISP blocks outbound port 25, then you will have to find an alternate solution or contact them.": "If your ISP blocks outbound port 25, then you will have to find an alternate solution or contact them.",
"You can run": "You can run",
"from command line or terminal to see if your outbound port 25 connection is blocked.": "from command line or terminal to see if your outbound port 25 connection is blocked.",
- "Español": "Español"
+ "Español": "Español",
+ "Storage Max Quota Per Alias": "Storage Max Quota Per Alias",
+ "Domain admins can update the maximum storage quota across all aliases. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. Domain admins can set maximum storage quota limits on a per alias basis by editing the alias directly.": "Domain admins can update the maximum storage quota across all aliases. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. Domain admins can set maximum storage quota limits on a per alias basis by editing the alias directly.",
+ "Storage Max Quota": "Storage Max Quota",
+ "Domain admins can update the storage quota for this alias. Leave blank and hit save to reset it to the current domain's maximum storage quota of %s. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. If you would like to update the max storage quota across all aliases for this domain, then go to the domain's Settings page.": "Domain admins can update the storage quota for this alias. Leave blank and hit save to reset it to the current domain's maximum storage quota of %s. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. If you would like to update the max storage quota across all aliases for this domain, then go to the domain's Settings page.",
+ "Alias does not exist on the domain.": "Alias does not exist on the domain."
}
\ No newline at end of file
diff --git a/locales/es.json b/locales/es.json
index 1b36b53b61..e0c8d480d4 100644
--- a/locales/es.json
+++ b/locales/es.json
@@ -10308,5 +10308,12 @@
"Open-source Email Hosting Service": "Servicio de alojamiento de correo electrónico de código abierto",
"Custom Domain Email Forwarding": "Reenvío de correo electrónico de dominio personalizado",
"Suggested": "Sugerido",
- "Its description from its website is:": "Su descripción desde su sitio web es:"
+ "Its description from its website is:": "Su descripción desde su sitio web es:",
+ "Storage Max Quota Per Alias": "Cuota máxima de almacenamiento por alias",
+ "Domain admins can update the maximum storage quota across all aliases. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. Domain admins can set maximum storage quota limits on a per alias basis by editing the alias directly.": "Los administradores de dominio pueden actualizar la cuota máxima de almacenamiento en todos los alias. Ingrese una cadena de caracteres descriptiva como "1 GB" (tenga en cuenta que usamos bytes para analizar el valor en un número). Los administradores de dominio pueden establecer límites máximos de cuota de almacenamiento por alias editando el alias directamente.",
+ "Storage Max Quota": "Cuota máxima de almacenamiento",
+ "Domain admins can update the storage quota for this alias. Leave blank and hit save to reset it to the current domain's maximum storage quota of %s. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. If you would like to update the max storage quota across all aliases for this domain, then go to the domain's Settings page.": "Los administradores de dominio pueden actualizar la cuota de almacenamiento para este alias. Déjelo en blanco y presione Guardar para restablecerlo a la cuota de almacenamiento máxima del dominio actual de %s . Ingrese una cadena de caracteres descriptiva como "1 GB"; tenga en cuenta que usamos bytes para analizar el valor como un número. Si desea actualizar la cuota de almacenamiento máxima en todos los alias de este dominio, vaya a la página Configuración del dominio.",
+ "The quota for %s of %s exceeds the domain's maximum quota of %s.": "La cuota para %s de %s excede la cuota máxima del dominio de %s .",
+ "The quota for %s of %s exceeds the maximum quota of from admins of the domain.": "La cuota para %s de %s excede la cuota máxima de de los administradores del dominio.",
+ "Bytes were invalid, must be a string such as \"1 GB\" or \"100 MB\".": "Los bytes no son válidos, debe ser una cadena como \"1 GB\" o \"100 MB\"."
}
\ No newline at end of file
diff --git a/locales/fi.json b/locales/fi.json
index da206ed76d..8369ef2e23 100644
--- a/locales/fi.json
+++ b/locales/fi.json
@@ -10157,5 +10157,12 @@
"Open-source Email Hosting Service": "Avoimen lähdekoodin sähköpostipalvelu",
"Custom Domain Email Forwarding": "Mukautetun verkkotunnuksen sähköpostin edelleenlähetys",
"Suggested": "Ehdotettu",
- "Its description from its website is:": "Sen kuvaus sen verkkosivuilta on:"
+ "Its description from its website is:": "Sen kuvaus sen verkkosivuilta on:",
+ "Storage Max Quota Per Alias": "Suurin tallennuskiintiö aliasta kohti",
+ "Domain admins can update the maximum storage quota across all aliases. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. Domain admins can set maximum storage quota limits on a per alias basis by editing the alias directly.": "Verkkotunnuksen järjestelmänvalvojat voivat päivittää kaikkien aliasten enimmäistallennuskiintiön. Kirjoita ihmisystävällinen merkkijono, kuten "1 Gt" – huomaa, että käytämme bytes jäsentääksemme arvon numeroksi. Verkkotunnuksen järjestelmänvalvojat voivat asettaa enimmäistallennuskiintiön aliaskohtaisesti muokkaamalla aliasta suoraan.",
+ "Storage Max Quota": "Tallennustilan enimmäiskiintiö",
+ "Domain admins can update the storage quota for this alias. Leave blank and hit save to reset it to the current domain's maximum storage quota of %s. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. If you would like to update the max storage quota across all aliases for this domain, then go to the domain's Settings page.": "Verkkotunnuksen järjestelmänvalvojat voivat päivittää tämän aliaksen tallennuskiintiön. Jätä tyhjäksi ja paina Tallenna palauttaaksesi sen nykyisen verkkotunnuksen enimmäistallennuskiintiöön %s . Kirjoita ihmisystävällinen merkkijono, kuten "1 Gt" – huomaa, että käytämme bytes jäsentääksemme arvon numeroksi. Jos haluat päivittää tämän verkkotunnuksen kaikkien aliasten enimmäistallennuskiintiön, siirry verkkotunnuksen Asetukset-sivulle.",
+ "The quota for %s of %s exceeds the domain's maximum quota of %s.": "Kohteen %s kiintiö %s ylittää verkkotunnuksen enimmäiskiintiön %s .",
+ "The quota for %s of %s exceeds the maximum quota of from admins of the domain.": "%s n kiintiö %s ylittää enimmäiskiintiön verkkotunnuksen ylläpitäjiltä.",
+ "Bytes were invalid, must be a string such as \"1 GB\" or \"100 MB\".": "Tavut olivat virheellisiä, täytyy olla merkkijono, kuten \"1 Gt\" tai \"100 MB\"."
}
\ No newline at end of file
diff --git a/locales/fr.json b/locales/fr.json
index b1240584e1..da2ae63114 100644
--- a/locales/fr.json
+++ b/locales/fr.json
@@ -7834,5 +7834,13 @@
"Custom Domain Email Forwarding": "Transfert d'e-mails de domaine personnalisé",
"Legacy Free Guide": "Guide gratuit sur l'héritage",
"Suggested": "Suggéré",
- "Its description from its website is:": "Sa description sur son site Web est :"
+ "Its description from its website is:": "Sa description sur son site Web est :",
+ "Storage Max Quota Per Alias": "Quota de stockage maximal par alias",
+ "Domain admins can update the maximum storage quota across all aliases. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. Domain admins can set maximum storage quota limits on a per alias basis by editing the alias directly.": "Les administrateurs de domaine peuvent mettre à jour le quota de stockage maximal pour tous les alias. Saisissez une chaîne conviviale telle que « 1 Go » – notez que nous utilisons bytes pour analyser la valeur en un nombre. Les administrateurs de domaine peuvent définir des limites de quota de stockage maximales pour chaque alias en modifiant directement l'alias.",
+ "Storage Max Quota": "Quota de stockage maximum",
+ "Domain admins can update the storage quota for this alias. Leave blank and hit save to reset it to the current domain's maximum storage quota of %s. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. If you would like to update the max storage quota across all aliases for this domain, then go to the domain's Settings page.": "Les administrateurs de domaine peuvent mettre à jour le quota de stockage pour cet alias. Laissez ce champ vide et cliquez sur Enregistrer pour le réinitialiser au quota de stockage maximal du domaine actuel, soit %s . Saisissez une chaîne conviviale telle que « 1 Go » – notez que nous utilisons bytes pour analyser la valeur en un nombre. Si vous souhaitez mettre à jour le quota de stockage maximal pour tous les alias de ce domaine, accédez à la page Paramètres du domaine.",
+ "Verified": "Vérifié",
+ "The quota for %s of %s exceeds the domain's maximum quota of %s.": "Le quota pour %s de %s dépasse le quota maximum du domaine de %s .",
+ "The quota for %s of %s exceeds the maximum quota of from admins of the domain.": "Le quota pour %s de %s dépasse le quota maximum de des administrateurs du domaine.",
+ "Bytes were invalid, must be a string such as \"1 GB\" or \"100 MB\".": "Les octets n'étaient pas valides, il doit s'agir d'une chaîne telle que « 1 Go » ou « 100 Mo »."
}
\ No newline at end of file
diff --git a/locales/he.json b/locales/he.json
index 23b2dec43d..3598ea9601 100644
--- a/locales/he.json
+++ b/locales/he.json
@@ -8330,5 +8330,13 @@
"Custom Domain Email Forwarding": "העברת דואר אלקטרוני בדומיין מותאם אישית",
"Legacy Free Guide": "מדריך חינם מדור קודם",
"Suggested": "מוּצָע",
- "Its description from its website is:": "התיאור שלו מהאתר שלו הוא:"
+ "Its description from its website is:": "התיאור שלו מהאתר שלו הוא:",
+ "Storage Max Quota Per Alias": "מכסת מקסימום אחסון לכל כינוי",
+ "Domain admins can update the maximum storage quota across all aliases. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. Domain admins can set maximum storage quota limits on a per alias basis by editing the alias directly.": "מנהלי דומיין יכולים לעדכן את מכסת האחסון המקסימלית בכל הכינויים. הזן מחרוזת ידידותית לאדם כגון "1GB" - שים לב שאנו משתמשים bytes כדי לנתח את הערך למספר. מנהלי דומיין יכולים להגדיר מגבלות מכסת אחסון מקסימליות על בסיס כינוי על ידי עריכה ישירה של הכינוי.",
+ "Storage Max Quota": "מכסת מקסימום אחסון",
+ "Domain admins can update the storage quota for this alias. Leave blank and hit save to reset it to the current domain's maximum storage quota of %s. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. If you would like to update the max storage quota across all aliases for this domain, then go to the domain's Settings page.": "מנהלי דומיין יכולים לעדכן את מכסת האחסון עבור הכינוי הזה. השאר ריק והקש על שמור כדי לאפס אותו למכסת האחסון המקסימלית של הדומיין הנוכחי של %s . הזן מחרוזת ידידותית לאדם כגון "1GB" - שים לב שאנו משתמשים bytes כדי לנתח את הערך למספר. אם תרצה לעדכן את מכסת האחסון המקסימלית בכל הכינויים של הדומיין הזה, עבור אל דף ההגדרות של הדומיין.",
+ "Verified": "מְאוּמָת",
+ "The quota for %s of %s exceeds the domain's maximum quota of %s.": "המכסה עבור %s מתוך %s חורגת מהמכסה המקסימלית של הדומיין של %s .",
+ "The quota for %s of %s exceeds the maximum quota of from admins of the domain.": "המכסה עבור %s מתוך %s חורגת מהמכסה המקסימלית של ממנהלי הדומיין.",
+ "Bytes were invalid, must be a string such as \"1 GB\" or \"100 MB\".": "הבתים היו לא חוקיים, חייבים להיות מחרוזת כגון \"1 GB\" או \"100 MB\"."
}
\ No newline at end of file
diff --git a/locales/hu.json b/locales/hu.json
index 61ac22bd7e..bf69f5dbc7 100644
--- a/locales/hu.json
+++ b/locales/hu.json
@@ -10310,5 +10310,12 @@
"Open-source Email Hosting Service": "Nyílt forráskódú e-mail hosting szolgáltatás",
"Custom Domain Email Forwarding": "Egyéni domain e-mail továbbítása",
"Suggested": "Javasolt",
- "Its description from its website is:": "Leírása a honlapjáról a következő:"
+ "Its description from its website is:": "Leírása a honlapjáról a következő:",
+ "Storage Max Quota Per Alias": "Maximális tárhely kvóta aliasonként",
+ "Domain admins can update the maximum storage quota across all aliases. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. Domain admins can set maximum storage quota limits on a per alias basis by editing the alias directly.": "A domain rendszergazdái frissíthetik a maximális tárhelykvótát az összes álnévre vonatkozóan. Adjon meg egy emberbarát karakterláncot, például „1 GB” – vegye figyelembe, hogy bytes használunk az érték számmá értelmezéséhez. A tartományadminisztrátorok az alias közvetlen szerkesztésével beállíthatják a maximális tárhelykvóta-korlátokat aliasonként.",
+ "Storage Max Quota": "Maximális tárhely kvóta",
+ "Domain admins can update the storage quota for this alias. Leave blank and hit save to reset it to the current domain's maximum storage quota of %s. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. If you would like to update the max storage quota across all aliases for this domain, then go to the domain's Settings page.": "A domain rendszergazdái frissíthetik ennek az aliasnak a tárhelykvótáját. Hagyja üresen, és nyomja meg a Mentés gombot, hogy visszaállítsa az aktuális domain maximális tárhelykvótáját %s . Adjon meg egy emberbarát karakterláncot, például „1 GB” – vegye figyelembe, hogy bytes használunk az érték számmá értelmezéséhez. Ha frissíteni szeretné a maximális tárhelykvótát a domain összes álnevére vonatkozóan, lépjen a domain Beállítások oldalára.",
+ "The quota for %s of %s exceeds the domain's maximum quota of %s.": "A %s %s kvótája meghaladja a domain maximális %s kvótáját.",
+ "The quota for %s of %s exceeds the maximum quota of from admins of the domain.": "A %s %s kvótája meghaladja a maximális kvótát a domain rendszergazdáitól.",
+ "Bytes were invalid, must be a string such as \"1 GB\" or \"100 MB\".": "A bájtok érvénytelenek voltak, olyan karakterláncnak kell lennie, mint például „1 GB” vagy „100 MB”."
}
\ No newline at end of file
diff --git a/locales/id.json b/locales/id.json
index 75e154a2f1..3f3a4f15af 100644
--- a/locales/id.json
+++ b/locales/id.json
@@ -10310,5 +10310,12 @@
"Open-source Email Hosting Service": "Layanan Hosting Email Sumber Terbuka",
"Custom Domain Email Forwarding": "Penerusan Email Domain Kustom",
"Suggested": "Disarankan",
- "Its description from its website is:": "Uraian dari situs webnya adalah:"
+ "Its description from its website is:": "Uraian dari situs webnya adalah:",
+ "Storage Max Quota Per Alias": "Kuota Penyimpanan Maksimum Per Alias",
+ "Domain admins can update the maximum storage quota across all aliases. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. Domain admins can set maximum storage quota limits on a per alias basis by editing the alias directly.": "Admin domain dapat memperbarui kuota penyimpanan maksimum di semua alias. Masukkan string yang mudah dipahami manusia seperti "1GB" – perhatikan bahwa kami menggunakan bytes untuk mengurai nilai menjadi Angka. Admin domain dapat menetapkan batas kuota penyimpanan maksimum per alias dengan mengedit alias secara langsung.",
+ "Storage Max Quota": "Kuota Penyimpanan Maksimum",
+ "Domain admins can update the storage quota for this alias. Leave blank and hit save to reset it to the current domain's maximum storage quota of %s. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. If you would like to update the max storage quota across all aliases for this domain, then go to the domain's Settings page.": "Admin domain dapat memperbarui kuota penyimpanan untuk alias ini. Kosongkan dan tekan simpan untuk menyetel ulang ke kuota penyimpanan maksimum domain saat ini sebesar %s . Masukkan string yang mudah dipahami seperti "1GB" – perhatikan bahwa kami menggunakan bytes untuk mengurai nilai menjadi Angka. Jika Anda ingin memperbarui kuota penyimpanan maksimum di semua alias untuk domain ini, buka halaman Pengaturan domain.",
+ "The quota for %s of %s exceeds the domain's maximum quota of %s.": "Kuota untuk %s dari %s melebihi kuota maksimum domain sebesar %s .",
+ "The quota for %s of %s exceeds the maximum quota of from admins of the domain.": "Kuota untuk %s dari %s melebihi kuota maksimum dari admin domain.",
+ "Bytes were invalid, must be a string such as \"1 GB\" or \"100 MB\".": "Byte tidak valid, harus berupa string seperti \"1 GB\" atau \"100 MB\"."
}
\ No newline at end of file
diff --git a/locales/it.json b/locales/it.json
index bfdf7b42f1..9c479df00b 100644
--- a/locales/it.json
+++ b/locales/it.json
@@ -10310,5 +10310,12 @@
"Open-source Email Hosting Service": "Servizio di hosting di posta elettronica open source",
"Custom Domain Email Forwarding": "Inoltro e-mail di dominio personalizzato",
"Suggested": "Suggerito",
- "Its description from its website is:": "La descrizione dal suo sito web è:"
+ "Its description from its website is:": "La descrizione dal suo sito web è:",
+ "Storage Max Quota Per Alias": "Quota massima di archiviazione per alias",
+ "Domain admins can update the maximum storage quota across all aliases. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. Domain admins can set maximum storage quota limits on a per alias basis by editing the alias directly.": "Gli amministratori di dominio possono aggiornare la quota di archiviazione massima su tutti gli alias. Inserisci una stringa di facile comprensione come "1 GB" (nota che utilizziamo bytes per analizzare il valore in un numero). Gli amministratori di dominio possono impostare limiti massimi di quota di archiviazione su base per alias modificando direttamente l'alias.",
+ "Storage Max Quota": "Quota massima di archiviazione",
+ "Domain admins can update the storage quota for this alias. Leave blank and hit save to reset it to the current domain's maximum storage quota of %s. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. If you would like to update the max storage quota across all aliases for this domain, then go to the domain's Settings page.": "Gli amministratori di dominio possono aggiornare la quota di archiviazione per questo alias. Lascia vuoto e premi salva per reimpostarla sulla quota di archiviazione massima del dominio corrente di %s . Inserisci una stringa di facile comprensione come "1 GB" (nota che utilizziamo bytes per analizzare il valore in un numero). Se desideri aggiornare la quota di archiviazione massima su tutti gli alias per questo dominio, vai alla pagina Impostazioni del dominio.",
+ "The quota for %s of %s exceeds the domain's maximum quota of %s.": "La quota per %s di %s supera la quota massima del dominio di %s .",
+ "The quota for %s of %s exceeds the maximum quota of from admins of the domain.": "La quota per %s di %s supera la quota massima di dagli amministratori del dominio.",
+ "Bytes were invalid, must be a string such as \"1 GB\" or \"100 MB\".": "I byte non sono validi, deve essere una stringa come \"1 GB\" o \"100 MB\"."
}
\ No newline at end of file
diff --git a/locales/ja.json b/locales/ja.json
index d3fa7cfbfa..b47b04a7c4 100644
--- a/locales/ja.json
+++ b/locales/ja.json
@@ -10310,5 +10310,12 @@
"Open-source Email Hosting Service": "オープンソースのメールホスティングサービス",
"Custom Domain Email Forwarding": "カスタムドメインメール転送",
"Suggested": "提案",
- "Its description from its website is:": "ウェブサイトからの説明は次のとおりです。"
+ "Its description from its website is:": "ウェブサイトからの説明は次のとおりです。",
+ "Storage Max Quota Per Alias": "エイリアスごとのストレージ最大割り当て",
+ "Domain admins can update the maximum storage quota across all aliases. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. Domain admins can set maximum storage quota limits on a per alias basis by editing the alias directly.": "ドメイン管理者は、すべてのエイリアスの最大ストレージ クォータを更新できます。「1GB」などのわかりやすい文字列を入力します。値を数値に解析するためにbytesが使用されることに注意してください。ドメイン管理者は、エイリアスを直接編集することで、エイリアスごとに最大ストレージ クォータ制限を設定できます。",
+ "Storage Max Quota": "ストレージの最大割り当て",
+ "Domain admins can update the storage quota for this alias. Leave blank and hit save to reset it to the current domain's maximum storage quota of %s. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. If you would like to update the max storage quota across all aliases for this domain, then go to the domain's Settings page.": "ドメイン管理者は、このエイリアスのストレージ クォータを更新できます。空白のままにして [保存] をクリックすると、現在のドメインの最大ストレージ クォータである%sにリセットされます。「1GB」などのわかりやすい文字列を入力します。値を数値に解析するためにbytesが使用されることに注意してください。このドメインのすべてのエイリアスの最大ストレージ クォータを更新する場合は、ドメインの設定ページに移動してください。",
+ "The quota for %s of %s exceeds the domain's maximum quota of %s.": "% %s %sのクォータがドメインの最大クォータ%sを超えています。",
+ "The quota for %s of %s exceeds the maximum quota of from admins of the domain.": "% %s %sのクォータが最大クォータを超えていますドメインの管理者から。",
+ "Bytes were invalid, must be a string such as \"1 GB\" or \"100 MB\".": "バイトが無効です。「1 GB」や「100 MB」などの文字列である必要があります。"
}
\ No newline at end of file
diff --git a/locales/ko.json b/locales/ko.json
index cb840319e3..b43358e92b 100644
--- a/locales/ko.json
+++ b/locales/ko.json
@@ -10310,5 +10310,12 @@
"Open-source Email Hosting Service": "오픈소스 이메일 호스팅 서비스",
"Custom Domain Email Forwarding": "사용자 정의 도메인 이메일 전달",
"Suggested": "제안된",
- "Its description from its website is:": "해당 웹사이트의 설명은 다음과 같습니다."
+ "Its description from its website is:": "해당 웹사이트의 설명은 다음과 같습니다.",
+ "Storage Max Quota Per Alias": "별칭당 최대 저장 할당량",
+ "Domain admins can update the maximum storage quota across all aliases. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. Domain admins can set maximum storage quota limits on a per alias basis by editing the alias directly.": "도메인 관리자는 모든 별칭에서 최대 저장소 할당량을 업데이트할 수 있습니다. "1GB"와 같이 인간 친화적인 문자열을 입력합니다. 값을 숫자로 구문 분석하는 bytes 사용한다는 점에 유의하세요. 도메인 관리자는 별칭을 직접 편집하여 별칭별로 최대 저장소 할당량 한도를 설정할 수 있습니다.",
+ "Storage Max Quota": "저장 공간 최대 할당량",
+ "Domain admins can update the storage quota for this alias. Leave blank and hit save to reset it to the current domain's maximum storage quota of %s. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. If you would like to update the max storage quota across all aliases for this domain, then go to the domain's Settings page.": "도메인 관리자는 이 별칭에 대한 저장소 할당량을 업데이트할 수 있습니다. 비워두고 저장을 눌러 현재 도메인의 최대 저장소 할당량인 %s 로 재설정합니다. "1GB"와 같이 인간 친화적인 문자열을 입력합니다. 값을 숫자로 구문 분석하는 데 bytes 사용한다는 점에 유의하세요. 이 도메인의 모든 별칭에서 최대 저장소 할당량을 업데이트하려면 도메인의 설정 페이지로 이동합니다.",
+ "The quota for %s of %s exceeds the domain's maximum quota of %s.": "%s 의 %s s 할당량이 도메인의 최대 할당량인 %s 을 초과합니다.",
+ "The quota for %s of %s exceeds the maximum quota of from admins of the domain.": "%s의 %s %s 에 대한 할당량이 최대 할당량을 초과합니다. 도메인 관리자로부터.",
+ "Bytes were invalid, must be a string such as \"1 GB\" or \"100 MB\".": "바이트가 잘못되었습니다. \"1 GB\" 또는 \"100 MB\"와 같은 문자열이어야 합니다."
}
\ No newline at end of file
diff --git a/locales/nl.json b/locales/nl.json
index c75cbe1207..8aa49258d9 100644
--- a/locales/nl.json
+++ b/locales/nl.json
@@ -10310,5 +10310,12 @@
"Open-source Email Hosting Service": "Open-source e-mailhostingservice",
"Custom Domain Email Forwarding": "E-mail doorsturen naar aangepast domein",
"Suggested": "Voorgesteld",
- "Its description from its website is:": "De beschrijving op de website is:"
+ "Its description from its website is:": "De beschrijving op de website is:",
+ "Storage Max Quota Per Alias": "Maximale opslagquotum per alias",
+ "Domain admins can update the maximum storage quota across all aliases. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. Domain admins can set maximum storage quota limits on a per alias basis by editing the alias directly.": "Domeinbeheerders kunnen de maximale opslagquota voor alle aliassen bijwerken. Voer een mensvriendelijke string in, zoals "1GB" – let op dat we bytes gebruiken om de waarde te parseren naar een Number. Domeinbeheerders kunnen maximale opslagquotalimieten per alias instellen door de alias rechtstreeks te bewerken.",
+ "Storage Max Quota": "Maximale opslagquotum",
+ "Domain admins can update the storage quota for this alias. Leave blank and hit save to reset it to the current domain's maximum storage quota of %s. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. If you would like to update the max storage quota across all aliases for this domain, then go to the domain's Settings page.": "Domeinbeheerders kunnen de opslagquota voor deze alias bijwerken. Laat dit leeg en klik op opslaan om het opnieuw in te stellen op de maximale opslagquota van het huidige domein van %s . Voer een mensvriendelijke string in, zoals "1GB" – let op dat we bytes gebruiken om de waarde te parseren naar een getal. Als u de maximale opslagquota voor alle aliassen voor dit domein wilt bijwerken, ga dan naar de pagina Instellingen van het domein.",
+ "The quota for %s of %s exceeds the domain's maximum quota of %s.": "Het quotum voor %s van %s overschrijdt het maximale quotum van het domein van %s .",
+ "The quota for %s of %s exceeds the maximum quota of from admins of the domain.": "Het quotum voor %s van %s overschrijdt het maximumquotum van van beheerders van het domein.",
+ "Bytes were invalid, must be a string such as \"1 GB\" or \"100 MB\".": "Bytes zijn ongeldig, moet een tekenreeks zijn, bijvoorbeeld \"1 GB\" of \"100 MB\"."
}
\ No newline at end of file
diff --git a/locales/no.json b/locales/no.json
index cf7840d239..ed01c01fe9 100644
--- a/locales/no.json
+++ b/locales/no.json
@@ -10315,5 +10315,12 @@
"Open-source Email Hosting Service": "Åpen kildekode e-postvertstjeneste",
"Custom Domain Email Forwarding": "Egendefinert domene-e-postvideresending",
"Suggested": "Foreslått",
- "Its description from its website is:": "Beskrivelsen fra nettstedet er:"
+ "Its description from its website is:": "Beskrivelsen fra nettstedet er:",
+ "Storage Max Quota Per Alias": "Lagring Maks. kvote per alias",
+ "Domain admins can update the maximum storage quota across all aliases. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. Domain admins can set maximum storage quota limits on a per alias basis by editing the alias directly.": "Domeneadministratorer kan oppdatere den maksimale lagringskvoten på tvers av alle aliaser. Skriv inn en menneskevennlig streng som "1GB" - merk at vi bruker bytes for å analysere verdien til et tall. Domeneadministratorer kan angi maksimale lagringskvoter per alias ved å redigere aliaset direkte.",
+ "Storage Max Quota": "Maks lagringskvote",
+ "Domain admins can update the storage quota for this alias. Leave blank and hit save to reset it to the current domain's maximum storage quota of %s. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. If you would like to update the max storage quota across all aliases for this domain, then go to the domain's Settings page.": "Domeneadministratorer kan oppdatere lagringskvoten for dette aliaset. La stå tomt og trykk lagre for å tilbakestille det til gjeldende domenes maksimale lagringskvote på %s . Skriv inn en menneskevennlig streng som "1GB" - merk at vi bruker bytes for å analysere verdien til et tall. Hvis du vil oppdatere den maksimale lagringskvoten på tvers av alle aliaser for dette domenet, går du til domenets Innstillinger-side.",
+ "The quota for %s of %s exceeds the domain's maximum quota of %s.": "Kvoten for %s av %s overskrider domenets maksimale kvote på %s .",
+ "The quota for %s of %s exceeds the maximum quota of from admins of the domain.": "Kvoten for %s av %s overskrider maksimumskvoten på fra administratorer av domenet.",
+ "Bytes were invalid, must be a string such as \"1 GB\" or \"100 MB\".": "Byte var ugyldige, må være en streng som \"1 GB\" eller \"100 MB\"."
}
\ No newline at end of file
diff --git a/locales/pl.json b/locales/pl.json
index 5115971c2e..d4182cfcbe 100644
--- a/locales/pl.json
+++ b/locales/pl.json
@@ -10310,5 +10310,12 @@
"Open-source Email Hosting Service": "Usługa hostingu poczty e-mail typu open source",
"Custom Domain Email Forwarding": "Przekierowanie poczty e-mail na domenę niestandardową",
"Suggested": "Sugerowane",
- "Its description from its website is:": "Opis na stronie internetowej brzmi następująco:"
+ "Its description from its website is:": "Opis na stronie internetowej brzmi następująco:",
+ "Storage Max Quota Per Alias": "Maksymalna kwota pamięci masowej na alias",
+ "Domain admins can update the maximum storage quota across all aliases. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. Domain admins can set maximum storage quota limits on a per alias basis by editing the alias directly.": "Administratorzy domeny mogą aktualizować maksymalny limit pamięci masowej na wszystkich aliasach. Wprowadź przyjazny dla człowieka ciąg, taki jak „1 GB” – pamiętaj, że używamy bytes do parsowania wartości na liczbę. Administratorzy domeny mogą ustawiać maksymalne limity limitu pamięci masowej na podstawie aliasu, edytując alias bezpośrednio.",
+ "Storage Max Quota": "Maksymalna ilość miejsca na przechowywanie",
+ "Domain admins can update the storage quota for this alias. Leave blank and hit save to reset it to the current domain's maximum storage quota of %s. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. If you would like to update the max storage quota across all aliases for this domain, then go to the domain's Settings page.": "Administratorzy domeny mogą aktualizować limit pamięci dla tego aliasu. Pozostaw puste pole i naciśnij przycisk Zapisz, aby zresetować go do maksymalnego limitu pamięci dla bieżącej domeny wynoszącego %s . Wprowadź przyjazny dla człowieka ciąg znaków, taki jak „1 GB” – pamiętaj, że używamy bytes do parsowania wartości na liczbę. Jeśli chcesz zaktualizować maksymalny limit pamięci dla wszystkich aliasów dla tej domeny, przejdź do strony Ustawienia domeny.",
+ "The quota for %s of %s exceeds the domain's maximum quota of %s.": "Kwota %s dla %s przekracza maksymalną kwotę domeny wynoszącą %s .",
+ "The quota for %s of %s exceeds the maximum quota of from admins of the domain.": "Kwota dla %s %s przekracza maksymalną kwotę od administratorów domeny.",
+ "Bytes were invalid, must be a string such as \"1 GB\" or \"100 MB\".": "Bajty są nieprawidłowe, musi to być ciąg znaków, np. „1 GB” lub „100 MB”."
}
\ No newline at end of file
diff --git a/locales/pt.json b/locales/pt.json
index f670f04f89..74c1620db7 100644
--- a/locales/pt.json
+++ b/locales/pt.json
@@ -10310,5 +10310,12 @@
"Open-source Email Hosting Service": "Serviço de hospedagem de e-mail de código aberto",
"Custom Domain Email Forwarding": "Encaminhamento de e-mail de domínio personalizado",
"Suggested": "Sugerido",
- "Its description from its website is:": "A descrição no seu site é:"
+ "Its description from its website is:": "A descrição no seu site é:",
+ "Storage Max Quota Per Alias": "Cota máxima de armazenamento por alias",
+ "Domain admins can update the maximum storage quota across all aliases. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. Domain admins can set maximum storage quota limits on a per alias basis by editing the alias directly.": "Os administradores de domínio podem atualizar a cota máxima de armazenamento em todos os aliases. Insira uma string amigável, como "1 GB" – observe que usamos bytes para analisar o valor para um Número. Os administradores de domínio podem definir limites máximos de cota de armazenamento por alias editando o alias diretamente.",
+ "Storage Max Quota": "Cota máxima de armazenamento",
+ "Domain admins can update the storage quota for this alias. Leave blank and hit save to reset it to the current domain's maximum storage quota of %s. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. If you would like to update the max storage quota across all aliases for this domain, then go to the domain's Settings page.": "Os administradores de domínio podem atualizar a cota de armazenamento para este alias. Deixe em branco e clique em salvar para redefini-lo para a cota máxima de armazenamento do domínio atual de %s . Insira uma string amigável, como "1 GB" – observe que usamos bytes para analisar o valor para um número. Se você quiser atualizar a cota máxima de armazenamento em todos os aliases para este domínio, vá para a página Configurações do domínio.",
+ "The quota for %s of %s exceeds the domain's maximum quota of %s.": "A cota de %s de %s excede a cota máxima do domínio de %s .",
+ "The quota for %s of %s exceeds the maximum quota of from admins of the domain.": "A cota para %s de %s excede a cota máxima de dos administradores do domínio.",
+ "Bytes were invalid, must be a string such as \"1 GB\" or \"100 MB\".": "Bytes eram inválidos, deve ser uma string como \"1 GB\" ou \"100 MB\"."
}
\ No newline at end of file
diff --git a/locales/ru.json b/locales/ru.json
index ee602f7180..98610e4a9f 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -10310,5 +10310,12 @@
"Open-source Email Hosting Service": "Служба хостинга электронной почты с открытым исходным кодом",
"Custom Domain Email Forwarding": "Пересылка электронной почты на пользовательский домен",
"Suggested": "Предложенный",
- "Its description from its website is:": "Описание на сайте:"
+ "Its description from its website is:": "Описание на сайте:",
+ "Storage Max Quota Per Alias": "Максимальная квота хранения на псевдоним",
+ "Domain admins can update the maximum storage quota across all aliases. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. Domain admins can set maximum storage quota limits on a per alias basis by editing the alias directly.": "Администраторы домена могут обновить максимальную квоту хранения для всех псевдонимов. Введите понятную человеку строку, например "1 ГБ" – обратите внимание, что мы используем bytes для преобразования значения в число. Администраторы домена могут установить максимальные ограничения квоты хранения для каждого псевдонима, напрямую редактируя псевдоним.",
+ "Storage Max Quota": "Максимальная квота хранения",
+ "Domain admins can update the storage quota for this alias. Leave blank and hit save to reset it to the current domain's maximum storage quota of %s. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. If you would like to update the max storage quota across all aliases for this domain, then go to the domain's Settings page.": "Администраторы домена могут обновить квоту хранения для этого псевдонима. Оставьте поле пустым и нажмите «Сохранить», чтобы сбросить его до максимальной квоты хранения текущего домена %s . Введите понятную человеку строку, например «1 ГБ» — обратите внимание, что мы используем bytes для преобразования значения в число. Если вы хотите обновить максимальную квоту хранения для всех псевдонимов этого домена, перейдите на страницу настроек домена.",
+ "The quota for %s of %s exceeds the domain's maximum quota of %s.": "Квота для %s %s превышает максимальную квоту домена %s .",
+ "The quota for %s of %s exceeds the maximum quota of from admins of the domain.": "Квота для %s %s превышает максимальную квоту от администраторов домена.",
+ "Bytes were invalid, must be a string such as \"1 GB\" or \"100 MB\".": "Недопустимые байты, должна быть строка, например «1 ГБ» или «100 МБ»."
}
\ No newline at end of file
diff --git a/locales/sv.json b/locales/sv.json
index 51d42819b6..6408080a83 100644
--- a/locales/sv.json
+++ b/locales/sv.json
@@ -10310,5 +10310,12 @@
"Open-source Email Hosting Service": "E-postvärdtjänst med öppen källkod",
"Custom Domain Email Forwarding": "Anpassad vidarebefordran av e-post på domän",
"Suggested": "Föreslog",
- "Its description from its website is:": "Dess beskrivning från dess hemsida är:"
+ "Its description from its website is:": "Dess beskrivning från dess hemsida är:",
+ "Storage Max Quota Per Alias": "Lagring Max kvot per alias",
+ "Domain admins can update the maximum storage quota across all aliases. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. Domain admins can set maximum storage quota limits on a per alias basis by editing the alias directly.": "Domänadministratörer kan uppdatera den maximala lagringskvoten för alla alias. Ange en människovänlig sträng som "1GB" – observera att vi använder bytes för att analysera värdet till ett nummer. Domänadministratörer kan ställa in maximala lagringskvotsgränser per alias genom att redigera aliaset direkt.",
+ "Storage Max Quota": "Max lagringskvot",
+ "Domain admins can update the storage quota for this alias. Leave blank and hit save to reset it to the current domain's maximum storage quota of %s. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. If you would like to update the max storage quota across all aliases for this domain, then go to the domain's Settings page.": "Domänadministratörer kan uppdatera lagringskvoten för detta alias. Lämna tomt och tryck på spara för att återställa den till den aktuella domänens maximala lagringskvot på %s . Ange en människovänlig sträng som "1GB" – observera att vi använder bytes för att analysera värdet till ett nummer. Om du vill uppdatera den maximala lagringskvoten för alla alias för den här domänen, gå till domänens Inställningar-sida.",
+ "The quota for %s of %s exceeds the domain's maximum quota of %s.": "Kvoten för %s av %s överskrider domänens maximala kvot på %s .",
+ "The quota for %s of %s exceeds the maximum quota of from admins of the domain.": "Kvoten för %s av %s överskrider den maximala kvoten på från domänens administratörer.",
+ "Bytes were invalid, must be a string such as \"1 GB\" or \"100 MB\".": "Byte var ogiltiga, måste vara en sträng som \"1 GB\" eller \"100 MB\"."
}
\ No newline at end of file
diff --git a/locales/th.json b/locales/th.json
index 57839107cd..414aa3656d 100644
--- a/locales/th.json
+++ b/locales/th.json
@@ -10310,5 +10310,12 @@
"Open-source Email Hosting Service": "บริการโฮสติ้งอีเมล์โอเพ่นซอร์ส",
"Custom Domain Email Forwarding": "การส่งต่ออีเมลโดเมนที่กำหนดเอง",
"Suggested": "ข้อเสนอแนะ",
- "Its description from its website is:": "คำอธิบายจากเว็บไซต์มีดังนี้:"
+ "Its description from its website is:": "คำอธิบายจากเว็บไซต์มีดังนี้:",
+ "Storage Max Quota Per Alias": "โควตาสูงสุดของที่เก็บข้อมูลต่อนามแฝง",
+ "Domain admins can update the maximum storage quota across all aliases. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. Domain admins can set maximum storage quota limits on a per alias basis by editing the alias directly.": "ผู้ดูแลระบบโดเมนสามารถอัปเดตโควตาพื้นที่เก็บข้อมูลสูงสุดสำหรับนามแฝงทั้งหมดได้ ป้อนสตริงที่ผู้ใช้เข้าใจได้ เช่น "1GB" โปรดทราบว่าเราใช้ bytes เพื่อแยกค่าเป็นตัวเลข ผู้ดูแลระบบโดเมนสามารถกำหนดขีดจำกัดโควตาพื้นที่เก็บข้อมูลสูงสุดตามนามแฝงแต่ละนามแฝงได้โดยแก้ไขนามแฝงโดยตรง",
+ "Storage Max Quota": "โควตาการเก็บข้อมูลสูงสุด",
+ "Domain admins can update the storage quota for this alias. Leave blank and hit save to reset it to the current domain's maximum storage quota of %s. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. If you would like to update the max storage quota across all aliases for this domain, then go to the domain's Settings page.": "ผู้ดูแลโดเมนสามารถอัปเดตโควตาพื้นที่เก็บข้อมูลสำหรับนามแฝงนี้ได้ ปล่อยว่างไว้แล้วกดบันทึกเพื่อรีเซ็ตโควตาเป็นโควตาพื้นที่เก็บข้อมูลสูงสุดของโดเมนปัจจุบันที่ %s ป้อนสตริงที่มนุษย์สามารถเข้าใจได้ เช่น "1GB" โปรดทราบว่าเราใช้ bytes เพื่อแยกค่าเป็นตัวเลข หากคุณต้องการอัปเดตโควตาพื้นที่เก็บข้อมูลสูงสุดสำหรับนามแฝงทั้งหมดสำหรับโดเมนนี้ ให้ไปที่หน้าการตั้งค่าของโดเมน",
+ "The quota for %s of %s exceeds the domain's maximum quota of %s.": "โควตาสำหรับ %s ของ %s เกินโควตาสูงสุดของโดเมนที่ %s",
+ "The quota for %s of %s exceeds the maximum quota of from admins of the domain.": "โควตาสำหรับ %s ของ %s เกินโควตาสูงสุดของ จากผู้ดูแลระบบโดเมน",
+ "Bytes were invalid, must be a string such as \"1 GB\" or \"100 MB\".": "ไบต์ไม่ถูกต้อง ต้องเป็นสตริงเช่น \"1 GB\" หรือ \"100 MB\""
}
\ No newline at end of file
diff --git a/locales/tr.json b/locales/tr.json
index e1c1ff6876..c4140876bf 100644
--- a/locales/tr.json
+++ b/locales/tr.json
@@ -10310,5 +10310,12 @@
"Open-source Email Hosting Service": "Açık Kaynaklı E-posta Barındırma Hizmeti",
"Custom Domain Email Forwarding": "Özel Alan Adı E-posta Yönlendirme",
"Suggested": "Önerilen",
- "Its description from its website is:": "Web sitesindeki açıklaması şöyle:"
+ "Its description from its website is:": "Web sitesindeki açıklaması şöyle:",
+ "Storage Max Quota Per Alias": "Takma Ad Başına Depolama Maksimum Kotası",
+ "Domain admins can update the maximum storage quota across all aliases. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. Domain admins can set maximum storage quota limits on a per alias basis by editing the alias directly.": "Alan adı yöneticileri, tüm takma adlar genelinde maksimum depolama kotasını güncelleyebilir. "1 GB" gibi insan dostu bir dize girin; değeri bir Sayıya ayrıştırmak için bytes kullandığımızı unutmayın. Alan adı yöneticileri, takma adı doğrudan düzenleyerek, takma ad bazında maksimum depolama kotası sınırları belirleyebilir.",
+ "Storage Max Quota": "Depolama Maksimum Kotası",
+ "Domain admins can update the storage quota for this alias. Leave blank and hit save to reset it to the current domain's maximum storage quota of %s. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. If you would like to update the max storage quota across all aliases for this domain, then go to the domain's Settings page.": "Alan adı yöneticileri bu takma ad için depolama kotasını güncelleyebilir. Boş bırakın ve geçerli alanın maksimum depolama kotası olan %s değerine sıfırlamak için kaydet'e tıklayın. "1 GB" gibi insan dostu bir dize girin; değeri bir Sayıya ayrıştırmak için bytes kullandığımızı unutmayın. Bu alan adı için tüm takma adlardaki maksimum depolama kotasını güncellemek istiyorsanız, alan adının Ayarlar sayfasına gidin.",
+ "The quota for %s of %s exceeds the domain's maximum quota of %s.": "%s s'nin %s kotası, etki alanının %s olan maksimum kotasını aşıyor.",
+ "The quota for %s of %s exceeds the maximum quota of from admins of the domain.": "%s s'nin %s kotası, maksimum kotayı aşıyor Alan adının yöneticilerinden.",
+ "Bytes were invalid, must be a string such as \"1 GB\" or \"100 MB\".": "Baytlar geçersiz, \"1 GB\" veya \"100 MB\" gibi bir dize olmalı."
}
\ No newline at end of file
diff --git a/locales/uk.json b/locales/uk.json
index 4e5e6cb1d7..db6fd1a3eb 100644
--- a/locales/uk.json
+++ b/locales/uk.json
@@ -10310,5 +10310,12 @@
"Open-source Email Hosting Service": "Служба хостингу електронної пошти з відкритим кодом",
"Custom Domain Email Forwarding": "Переадресація електронної пошти власного домену",
"Suggested": "Запропоновано",
- "Its description from its website is:": "Його опис на веб-сайті:"
+ "Its description from its website is:": "Його опис на веб-сайті:",
+ "Storage Max Quota Per Alias": "Максимальна квота для зберігання на псевдонім",
+ "Domain admins can update the maximum storage quota across all aliases. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. Domain admins can set maximum storage quota limits on a per alias basis by editing the alias directly.": "Адміністратори домену можуть оновлювати максимальну квоту пам’яті для всіх псевдонімів. Введіть зрозумілий для людини рядок, як-от "1GB" – зверніть увагу, що ми використовуємо bytes для аналізу значення до числа. Адміністратори домену можуть установлювати максимальні ліміти квоти зберігання на основі кожного псевдоніма, безпосередньо редагуючи псевдонім.",
+ "Storage Max Quota": "Максимальна квота пам’яті",
+ "Domain admins can update the storage quota for this alias. Leave blank and hit save to reset it to the current domain's maximum storage quota of %s. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. If you would like to update the max storage quota across all aliases for this domain, then go to the domain's Settings page.": "Адміністратори домену можуть оновити квоту пам’яті для цього псевдоніма. Залиште поле порожнім і натисніть «Зберегти», щоб скинути його до максимальної квоти зберігання поточного домену %s . Введіть зрозумілий для людини рядок, як-от "1GB" – зверніть увагу, що ми використовуємо bytes для аналізу значення до числа. Якщо ви хочете оновити максимальну квоту пам’яті для всіх псевдонімів цього домену, перейдіть на сторінку налаштувань домену.",
+ "The quota for %s of %s exceeds the domain's maximum quota of %s.": "Квота для %s з %s перевищує максимальну квоту домену %s .",
+ "The quota for %s of %s exceeds the maximum quota of from admins of the domain.": "Квота для %s з %s перевищує максимальну квоту від адмінів домену.",
+ "Bytes were invalid, must be a string such as \"1 GB\" or \"100 MB\".": "Байти недійсні, це має бути рядок, наприклад \"1 ГБ\" або \"100 МБ\"."
}
\ No newline at end of file
diff --git a/locales/vi.json b/locales/vi.json
index d64b883ad9..387cad586c 100644
--- a/locales/vi.json
+++ b/locales/vi.json
@@ -7839,5 +7839,13 @@
"Custom Domain Email Forwarding": "Chuyển tiếp Email theo tên miền tùy chỉnh",
"Legacy Free Guide": "Hướng dẫn miễn phí Legacy",
"Suggested": "Đề xuất",
- "Its description from its website is:": "Mô tả từ trang web của nó như sau:"
+ "Its description from its website is:": "Mô tả từ trang web của nó như sau:",
+ "Storage Max Quota Per Alias": "Dung lượng lưu trữ tối đa cho mỗi bí danh",
+ "Domain admins can update the maximum storage quota across all aliases. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. Domain admins can set maximum storage quota limits on a per alias basis by editing the alias directly.": "Quản trị viên tên miền có thể cập nhật hạn ngạch lưu trữ tối đa trên tất cả các bí danh. Nhập một chuỗi dễ hiểu như "1GB" – lưu ý rằng chúng tôi sử dụng bytes để phân tích giá trị thành một số. Quản trị viên tên miền có thể đặt giới hạn hạn ngạch lưu trữ tối đa trên mỗi cơ sở bí danh bằng cách chỉnh sửa trực tiếp bí danh.",
+ "Storage Max Quota": "Hạn ngạch lưu trữ tối đa",
+ "Domain admins can update the storage quota for this alias. Leave blank and hit save to reset it to the current domain's maximum storage quota of %s. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. If you would like to update the max storage quota across all aliases for this domain, then go to the domain's Settings page.": "Quản trị viên tên miền có thể cập nhật hạn ngạch lưu trữ cho bí danh này. Để trống và nhấn lưu để đặt lại hạn ngạch lưu trữ tối đa của tên miền hiện tại là %s . Nhập một chuỗi dễ hiểu như "1GB" – lưu ý rằng chúng tôi sử dụng bytes để phân tích giá trị thành một Số. Nếu bạn muốn cập nhật hạn ngạch lưu trữ tối đa trên tất cả các bí danh cho tên miền này, hãy truy cập trang Cài đặt của tên miền.",
+ "Verified": "Đã xác minh",
+ "The quota for %s of %s exceeds the domain's maximum quota of %s.": "Hạn ngạch cho %s là %s vượt quá hạn ngạch tối đa của miền là %s .",
+ "The quota for %s of %s exceeds the maximum quota of from admins of the domain.": "Hạn ngạch cho %s của %s vượt quá hạn ngạch tối đa của từ người quản trị miền.",
+ "Bytes were invalid, must be a string such as \"1 GB\" or \"100 MB\".": "Byte không hợp lệ, phải là chuỗi như \"1 GB\" hoặc \"100 MB\"."
}
\ No newline at end of file
diff --git a/locales/zh.json b/locales/zh.json
index 44f4119983..aac229867f 100644
--- a/locales/zh.json
+++ b/locales/zh.json
@@ -10003,5 +10003,12 @@
"Open-source Email Hosting Service": "开源电子邮件托管服务",
"Custom Domain Email Forwarding": "自定义域名电子邮件转发",
"Suggested": "建议",
- "Its description from its website is:": "其网站上的描述是:"
+ "Its description from its website is:": "其网站上的描述是:",
+ "Storage Max Quota Per Alias": "每个别名的最大存储配额",
+ "Domain admins can update the maximum storage quota across all aliases. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. Domain admins can set maximum storage quota limits on a per alias basis by editing the alias directly.": "域管理员可以更新所有别名的最大存储配额。输入一个人性化的字符串,例如“1GB”——请注意,我们使用bytes将值解析为数字。域管理员可以通过直接编辑别名来为每个别名设置最大存储配额限制。",
+ "Storage Max Quota": "存储最大配额",
+ "Domain admins can update the storage quota for this alias. Leave blank and hit save to reset it to the current domain's maximum storage quota of %s. Enter a human-friendly string such as \"1GB\" – note that we use bytes to parse the value to a Number. If you would like to update the max storage quota across all aliases for this domain, then go to the domain's Settings page.": "域管理员可以更新此别名的存储配额。保留空白并点击保存以将其重置为当前域的最大存储配额%s 。输入一个人性化的字符串,例如“1GB”——请注意,我们使用bytes将值解析为数字。如果您想更新此域的所有别名的最大存储配额,请转到域的“设置”页面。",
+ "The quota for %s of %s exceeds the domain's maximum quota of %s.": "%s (共%s的配额超出了域的最大配额%s 。",
+ "The quota for %s of %s exceeds the maximum quota of from admins of the domain.": "%s的%s配额超出了来自域管理员。",
+ "Bytes were invalid, must be a string such as \"1 GB\" or \"100 MB\".": "字节无效,必须是诸如“1 GB”或“100 MB”之类的字符串。"
}
\ No newline at end of file
diff --git a/package.json b/package.json
index 34a6b22942..b31a783da2 100644
--- a/package.json
+++ b/package.json
@@ -238,7 +238,6 @@
"postcss-strip-font-face": "1.0.0",
"postcss-viewport-height-correction": "1.1.1",
"prepare-stack-trace": "0.0.4",
- "pretty-bytes": "5",
"pretty-ms": "7",
"preview-email": "3.1.0",
"private-ip": "3.0.2",