diff --git a/app/controllers/web/my-account/download-alias-backup.js b/app/controllers/web/my-account/download-alias-backup.js
index 026f1c2a44..47d5cae56d 100644
--- a/app/controllers/web/my-account/download-alias-backup.js
+++ b/app/controllers/web/my-account/download-alias-backup.js
@@ -117,6 +117,9 @@ async function downloadAliasBackup(ctx) {
// send backup request
if (isSANB(ctx.request.body.password)) {
+ // purge cache so we can run another backup
+ await ctx.client.del(`backup_check:${alias.id}`);
+
const wsp = createWebSocketAsPromised();
wsp
.request({
diff --git a/app/controllers/web/my-account/generate-alias-password.js b/app/controllers/web/my-account/generate-alias-password.js
index 2fdc5e8bdd..7d2d07d976 100644
--- a/app/controllers/web/my-account/generate-alias-password.js
+++ b/app/controllers/web/my-account/generate-alias-password.js
@@ -172,10 +172,12 @@ async function generateAliasPassword(ctx) {
await alias.save();
// close websocket
- try {
- wsp.close();
- } catch (err) {
- ctx.logger.fatal(err);
+ if (wsp.isOpened) {
+ try {
+ wsp.close();
+ } catch (err) {
+ ctx.logger.fatal(err);
+ }
}
} else if (boolean(ctx.request.body.is_override)) {
// reset existing mailbox and create new mailbox
@@ -204,10 +206,12 @@ async function generateAliasPassword(ctx) {
await alias.save();
// close websocket
- try {
- wsp.close();
- } catch (err) {
- ctx.logger.fatal(err);
+ if (wsp.isOpened) {
+ try {
+ wsp.close();
+ } catch (err) {
+ ctx.logger.fatal(err);
+ }
}
} else {
// create new mailbox
@@ -258,10 +262,12 @@ async function generateAliasPassword(ctx) {
await alias.save();
// close websocket
- try {
- wsp.close();
- } catch (err) {
- ctx.logger.fatal(err);
+ if (wsp.isOpened) {
+ try {
+ wsp.close();
+ } catch (err) {
+ ctx.logger.fatal(err);
+ }
}
}
diff --git a/app/views/docs/best-quantum-safe-encrypted-email-service/index.md b/app/views/docs/best-quantum-safe-encrypted-email-service/index.md
index a524692470..2f69551ba7 100644
--- a/app/views/docs/best-quantum-safe-encrypted-email-service/index.md
+++ b/app/views/docs/best-quantum-safe-encrypted-email-service/index.md
@@ -172,7 +172,7 @@ The Primary is running on the data servers with the mounted volumes containing t
We accomplish two-way communication with [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket):
* Primary servers use an instance of [ws](https://github.com/websockets/ws)'s `WebSocketServer` server.
-* Secondary servers use an instance of [ws](https://github.com/websockets/ws)'s `WebSocket` client that is wrapped with [websocket-as-promised](https://github.com/vitalets/websocket-as-promised) and [reconnecting-websocket](https://github.com/pladaria/reconnecting-websocket). These two wrappers ensure that the `WebSocket` reconnects and can send and receive data for specific database writes.
+* Secondary servers use an instance of [ws](https://github.com/websockets/ws)'s `WebSocket` client that is wrapped with [websocket-as-promised](https://github.com/vitalets/websocket-as-promised) and [reconnecting-websocket](https://github.com/opensumi/reconnecting-websocket). These two wrappers ensure that the `WebSocket` reconnects and can send and receive data for specific database writes.
### Backups
diff --git a/config/index.js b/config/index.js
index 0ef1e4c922..2901bc5601 100644
--- a/config/index.js
+++ b/config/index.js
@@ -291,7 +291,7 @@ const config = {
removedEmailDomain: env.REMOVED_EMAIL_DOMAIN,
// SQLite busy_timeout value (how long we should wait for locking too)
- busyTimeout: ms('15s'),
+ busyTimeout: ms('30s'),
// server
env: env.NODE_ENV.toLowerCase(),
diff --git a/config/phrases.js b/config/phrases.js
index ec1d179f9e..6bbecaffa0 100644
--- a/config/phrases.js
+++ b/config/phrases.js
@@ -258,6 +258,10 @@ module.exports = {
'We tried to create a new account with this email address, but it already exists. Please log in with this email address if it belongs to you and then try again.',
LOGOUT_REQUIRED: 'Please log out to view the page you requested.',
ALIAS_DOES_NOT_EXIST: 'Alias does not exist on the domain.',
+ IMAP_NOT_ENABLED_SUBJECT:
+ 'Enable IMAP to receive mail for %s',
+ IMAP_NOT_ENABLED_MESSAGE:
+ 'Please edit your alias and enable IMAP to receive mail for %s.',
IMAP_MAILBOX_MAX_EXCEEDED: 'Maximum number of mailboxes exceeded',
IMAP_MESSAGE_SIZE_EXCEEDED: 'Maximum message size exceeded',
IMAP_MAILBOX_INBOX_CANNOT_STORE_DRAFTS: 'Inbox cannot store draft messages',
diff --git a/helpers/create-websocket-as-promised.js b/helpers/create-websocket-as-promised.js
index ce1c93a48c..3d6f5ad1b6 100644
--- a/helpers/create-websocket-as-promised.js
+++ b/helpers/create-websocket-as-promised.js
@@ -6,9 +6,7 @@
const { randomUUID } = require('node:crypto');
//
-// const ReconnectingWebSocket = require('reconnecting-websocket');
-
-const ReconnectingWebSocket = require('reconnecting-websocket');
+const ReconnectingWebSocket = require('@opensumi/reconnecting-websocket');
const WebSocketAsPromised = require('websocket-as-promised');
const mongoose = require('mongoose');
const ms = require('ms');
@@ -37,6 +35,44 @@ ReconnectingWebSocket.prototype._debug = () => {
// logger.debug('reconnectingwebsocket', { args });
};
+class Event {
+ constructor(type, target) {
+ this.target = target;
+ this.type = type;
+ }
+}
+
+class CloseEvent extends Event {
+ constructor(code = 1000, reason = '', target) {
+ super('close', target);
+ this.code = code;
+ this.reason = reason;
+ }
+}
+
+//
+ReconnectingWebSocket.prototype._disconnect = function (code, reason) {
+ if (code === undefined) {
+ code = 1000;
+ }
+
+ this._clearTimeouts();
+ if (!this._ws) {
+ return;
+ }
+
+ this._removeListeners();
+ try {
+ if (this._ws.readyState !== ReconnectingWebSocket.CONNECTING) {
+ this._ws.close(code, reason);
+ }
+
+ this._handleClose(new CloseEvent(code, reason, this));
+ } catch (err) {
+ logger.fatal(err);
+ }
+};
+
//
function createWebSocketClass(options) {
return class extends WebSocket {
diff --git a/helpers/get-database.js b/helpers/get-database.js
index 9faeb10ae6..a440ed58c3 100644
--- a/helpers/get-database.js
+++ b/helpers/get-database.js
@@ -9,32 +9,40 @@ const path = require('node:path');
//
const Database = require('better-sqlite3-multiple-ciphers');
+const dayjs = require('dayjs-with-plugins');
const isSANB = require('is-string-and-not-blank');
const mongoose = require('mongoose');
const ms = require('ms');
const pRetry = require('p-retry');
+const pify = require('pify');
+const { Builder } = require('json-sql');
const { boolean } = require('boolean');
const parseErr = require('parse-err');
const Aliases = require('#models/aliases');
-const Calendars = require('#models/calendars');
-const CalendarEvents = require('#models/calendar-events');
const Attachments = require('#models/attachments');
+const CalendarEvents = require('#models/calendar-events');
+const Calendars = require('#models/calendars');
const Mailboxes = require('#models/mailboxes');
const Messages = require('#models/messages');
const Threads = require('#models/threads');
const config = require('#config');
-const env = require('#config/env');
const email = require('#helpers/email');
+const env = require('#config/env');
const getPathToDatabase = require('#helpers/get-path-to-database');
const isTimeoutError = require('#helpers/is-timeout-error');
const isValidPassword = require('#helpers/is-valid-password');
const logger = require('#helpers/logger');
const migrateSchema = require('#helpers/migrate-schema');
+const onExpunge = require('#helpers/imap/on-expunge');
const setupPragma = require('#helpers/setup-pragma');
const { acquireLock, releaseLock } = require('#helpers/lock');
const { decrypt } = require('#helpers/encrypt-decrypt');
+const onExpungePromise = pify(onExpunge, { multiArgs: true });
+
+const builder = new Builder();
+
const HOSTNAME = os.hostname();
const AFFIXES = ['-wal', '-shm'];
@@ -591,18 +599,18 @@ async function getDatabase(
let migrateCheck = false;
let folderCheck = false;
- // let trashCheck = false;
+ let trashCheck = false;
if (instance.client) {
try {
const results = await instance.client.mget([
`migrate_check:${session.user.alias_id}`,
- `folder_check:${session.user.alias_id}`
- // `trash_check:${session.user.alias_id}`
+ `folder_check:${session.user.alias_id}`,
+ `trash_check:${session.user.alias_id}`
]);
migrateCheck = boolean(results[0]);
folderCheck = boolean(results[1]);
- // trashCheck = boolean(results[2]);
+ trashCheck = boolean(results[2]);
} catch (err) {
logger.fatal(err);
}
@@ -739,49 +747,76 @@ async function getDatabase(
logger.fatal(err, { session });
}
- // TODO: redo this so it sets `undeleted: 0` instead
- // TODO: redo this so it sets `undeleted: 0` instead
- // TODO: redo this so it sets `undeleted: 0` instead
- // TODO: redo this so it sets `undeleted: 0` instead
- // TODO: redo this so it sets `undeleted: 0` instead
- // TODO: redo this so it sets `undeleted: 0` instead
+ // release lock
+ try {
+ if (lock) {
+ await releaseLock(instance, db, lock);
+ }
+ } catch (err) {
+ logger.debug(err, { alias, session });
+ }
//
- // NOTE: we leave it up to the user to delete messages
- // but note that on CLOSE we call EXPUNGE on the mailbox
- //
- // remove messages in Junk/Trash folder that are >= 30 days old
- // (only do this once every day)
- /*
+ // NOTE: we remove messages in Junk/Trash folder that are >= 30 days old
+ // (but we only do this once every day)
try {
if (!trashCheck) {
const mailboxes = await Mailboxes.find(instance, session, {
path: {
- $in: ['Trash', 'Junk']
+ $in: ['Trash', 'Spam', 'Junk']
},
specialUse: {
$in: ['\\Trash', '\\Junk']
}
});
- if (mailboxes.length === 0) {
+
+ if (mailboxes.length === 0)
throw new TypeError('Trash folder(s) do not exist');
- }
- // NOTE: this does not support `prepareQuery` so you will need to convert _id -> id
- // (as we've done below by simply mapping and returning `id` vs `_id`)
- await Messages.deleteMany(instance, session, {
- $or: [
- {
- mailbox: {
- $in: mailboxes.map((m) => m._id.toString())
+ const sql = builder.build({
+ type: 'update',
+ table: 'Messages',
+ condition: {
+ $or: [
+ {
+ mailbox: {
+ $in: mailboxes.map((m) => m._id.toString())
+ },
+ exp: 1,
+ rdate: {
+ $lte: Date.now()
+ }
},
- exp: true,
- rdate: {
- $lte: Date.now()
+ {
+ mailbox: {
+ $in: mailboxes.map((m) => m._id.toString())
+ },
+ rdate: {
+ $lte: dayjs().subtract(30, 'days').toDate().getTime()
+ }
}
+ ]
+ },
+ modifier: {
+ $set: {
+ undeleted: false
}
- ]
+ }
});
+
+ db.prepare(sql.query).run(sql.values);
+
+ await Promise.all(
+ mailboxes.map((mailbox) =>
+ onExpungePromise.call(
+ instance,
+ mailbox._id.toString(),
+ { silent: true },
+ session
+ )
+ )
+ );
+
await instance.client.set(
`trash_check:${session.user.alias_id}`,
true,
@@ -792,37 +827,11 @@ async function getDatabase(
} catch (err) {
logger.fatal(err, { session });
}
- */
+ //
// TODO: delete orphaned attachments (those without messages that reference them)
-
- // release lock
- try {
- if (lock) {
- await releaseLock(instance, db, lock);
- }
- } catch (err) {
- logger.debug(err, { alias, session });
- }
-
- // if alias db size was 0 then we should update it
- /*
- try {
- const storageUsed = await Aliases.getStorageUsed({
- domain: new mongoose.Types.ObjectId(session.user.domain_id)
- });
- if (storageUsed === 0) {
- const size = await instance.wsp.request({
- action: 'size',
- timeout: ms('15s'),
- alias_id: alias.id
- });
- logger.debug('updating size', { size, alias, session });
- }
- } catch (err) {
- logger.fatal(err, { alias, session });
- }
- */
+ // (note this is unlikely as we already take care of this in EXPUNGE)
+ //
return db;
} catch (err) {
diff --git a/helpers/imap-notifier.js b/helpers/imap-notifier.js
index cde4c3e871..f939279e2f 100644
--- a/helpers/imap-notifier.js
+++ b/helpers/imap-notifier.js
@@ -15,7 +15,6 @@
const { EventEmitter } = require('node:events');
-const Axe = require('axe');
const Database = require('better-sqlite3-multiple-ciphers');
const _ = require('lodash');
const ms = require('ms');
@@ -26,13 +25,10 @@ const IMAPError = require('#helpers/imap-error');
const Journals = require('#models/journals');
const Mailboxes = require('#models/mailboxes');
const config = require('#config');
-const helperLogger = require('#helpers/logger');
+const logger = require('#helpers/logger');
const i18n = require('#helpers/i18n');
const { acquireLock, releaseLock } = require('#helpers/lock');
-const logger =
- config.env === 'development' ? helperLogger : new Axe({ silent: true });
-
const builder = new Builder();
class IMAPNotifier extends EventEmitter {
@@ -376,6 +372,7 @@ class IMAPNotifier extends EventEmitter {
data?.session?.db?.close === 'function'
) {
try {
+ data.session.db.pragma('analysis_limit=400');
data.session.db.pragma('optimize');
data.session.db.close();
} catch (err) {
diff --git a/helpers/imap/on-expunge.js b/helpers/imap/on-expunge.js
index c548bf3250..2a3267d41c 100644
--- a/helpers/imap/on-expunge.js
+++ b/helpers/imap/on-expunge.js
@@ -44,7 +44,7 @@ async function onExpunge(mailboxId, update, session, fn) {
update
});
- if (Array.isArray(writeStream)) {
+ if (session?.writeStream?.write && Array.isArray(writeStream)) {
for (const write of writeStream) {
if (Array.isArray(write)) {
session.writeStream.write(session.formatResponse(...write));
diff --git a/helpers/imap/on-fetch.js b/helpers/imap/on-fetch.js
index c5990559eb..8d0a58b2df 100644
--- a/helpers/imap/on-fetch.js
+++ b/helpers/imap/on-fetch.js
@@ -54,8 +54,10 @@ async function onFetch(mailboxId, options, session, fn) {
options
});
- for (const compiled of writeStream) {
- session.writeStream.write({ compiled });
+ if (session?.writeStream?.write) {
+ for (const compiled of writeStream) {
+ session.writeStream.write({ compiled });
+ }
}
fn(null, bool, response);
diff --git a/helpers/imap/on-move.js b/helpers/imap/on-move.js
index 1aadc50109..9eb98abbc9 100644
--- a/helpers/imap/on-move.js
+++ b/helpers/imap/on-move.js
@@ -49,7 +49,7 @@ async function onMove(mailboxId, update, session, fn) {
update
});
- if (Array.isArray(writeStream)) {
+ if (session?.writeStream?.write && Array.isArray(writeStream)) {
for (const write of writeStream) {
if (Array.isArray(write)) {
session.writeStream.write(session.formatResponse(...write));
diff --git a/helpers/imap/on-search.js b/helpers/imap/on-search.js
index 3e2e8ecc96..ac05047091 100644
--- a/helpers/imap/on-search.js
+++ b/helpers/imap/on-search.js
@@ -76,6 +76,8 @@ async function onSearch(mailboxId, options, session, fn) {
let highestModseq = 0;
let returned;
+ const set = new Set();
+
// eslint-disable-next-line complexity, no-inner-declarations
async function walkQuery(parent, ne, node) {
if (returned) {
@@ -153,9 +155,15 @@ async function onSearch(mailboxId, options, session, fn) {
const ids = session.db.prepare(sql.query).pluck().all(sql.values);
- parent.push({
- _id: { $in: ids }
- });
+ // SQLITE_MAX_VARIABLE_NUMBER which defaults to 999
+ // new approach:
+ for (const id of ids) {
+ set.add(id);
+ }
+ // old approach:
+ // parent.push({
+ // _id: { $in: ids }
+ // });
// NOTE: this is the wildduck reference (which does not support NOT matches)
// search over email body
@@ -309,7 +317,13 @@ async function onSearch(mailboxId, options, session, fn) {
.pluck()
.all(sql.values);
- entry._id = { $in: ids };
+ // SQLITE_MAX_VARIABLE_NUMBER which defaults to 999
+ // new approach:
+ for (const id of ids) {
+ set.add(id);
+ }
+ // old approach:
+ // entry._id = { $in: ids };
} else {
const sql = {
// NOTE: for array lookups:
@@ -322,7 +336,13 @@ async function onSearch(mailboxId, options, session, fn) {
.pluck()
.all(sql.values);
- entry._id = { $in: ids };
+ // SQLITE_MAX_VARIABLE_NUMBER which defaults to 999
+ // new approach:
+ for (const id of ids) {
+ set.add(id);
+ }
+ // old approach:
+ // entry._id = { $in: ids };
}
} else if (ne) {
const sql = {
@@ -334,7 +354,14 @@ async function onSearch(mailboxId, options, session, fn) {
.pluck()
.all(sql.values);
- entry._id = { $in: ids };
+ // SQLITE_MAX_VARIABLE_NUMBER which defaults to 999
+ // new approach:
+ for (const id of ids) {
+ set.add(id);
+ }
+
+ // old approach
+ // entry._id = { $in: ids };
} else {
const sql = {
query: `select _id from Messages, json_each(Messages.headers) where key = $p1;`,
@@ -345,7 +372,14 @@ async function onSearch(mailboxId, options, session, fn) {
.pluck()
.all(sql.values);
- entry._id = { $in: ids };
+ // SQLITE_MAX_VARIABLE_NUMBER which defaults to 999
+ // new approach:
+ for (const id of ids) {
+ set.add(id);
+ }
+
+ // old approach
+ // entry._id = { $in: ids };
}
// wildduck/mongodb version
@@ -378,7 +412,7 @@ async function onSearch(mailboxId, options, session, fn) {
// }
// : term.header
// };
- parent.push(entry);
+ if (!_.isEmpty(entry)) parent.push(entry);
}
break;
@@ -494,7 +528,7 @@ async function onSearch(mailboxId, options, session, fn) {
}
}
- parent.push(entry);
+ if (!_.isEmpty(entry)) parent.push(entry);
}
break;
@@ -549,7 +583,7 @@ async function onSearch(mailboxId, options, session, fn) {
: entry
};
- parent.push(entry);
+ if (!_.isEmpty(entry)) parent.push(entry);
}
break;
@@ -596,34 +630,25 @@ async function onSearch(mailboxId, options, session, fn) {
// NOTE: using `all()` currently for faster performance
// (since we don't write to the socket here)
//
- const messages = session.db.prepare(sql.query).all(sql.values);
try {
- for (const message of messages) {
- if (highestModseq < message.modseq) highestModseq = message.modseq;
- uidList.push(message.uid);
- }
- /*
- for (const result of stmt.iterate(sql.values)) {
- // eslint-disable-next-line no-await-in-loop
- const message = await convertResult(Messages, result, {
- uid: true,
- modseq: true
- });
-
- this.logger.debug('fetched message', {
- result,
- message,
- mailboxId,
- options,
- session
- });
+ // const messages = session.db.prepare(sql.query).all(sql.values);
+ // for (const message of messages) {
+ // // SQLITE_MAX_VARIABLE_NUMBER which defaults to 999
+ // if (set.size > 0 && !set.has(message._id)) continue;
- if (highestModseq < message.modseq) highestModseq = message.modseq;
+ // if (highestModseq < message.modseq) highestModseq = message.modseq;
+ // uidList.push(message.uid);
+ // }
+ // less memory consumption
+ for (const message of session.db.prepare(sql.query).iterate(sql.values)) {
+ // SQLITE_MAX_VARIABLE_NUMBER which defaults to 999
+ if (set.size > 0 && !set.has(message._id)) continue;
+
+ if (highestModseq < message.modseq) highestModseq = message.modseq;
uidList.push(message.uid);
}
- */
} catch (err) {
this.logger.fatal(err, { mailboxId, options, session });
throw new IMAPError(i18n.translateError('IMAP_INVALID_SEARCH'));
diff --git a/helpers/imap/on-store.js b/helpers/imap/on-store.js
index 16efe521b6..0fcdb34c3b 100644
--- a/helpers/imap/on-store.js
+++ b/helpers/imap/on-store.js
@@ -51,8 +51,10 @@ async function onStore(mailboxId, update, session, fn) {
update
});
- for (const write of writeStream) {
- session.writeStream.write(session.formatResponse(...write));
+ if (session?.writeStream?.write) {
+ for (const write of writeStream) {
+ session.writeStream.write(session.formatResponse(...write));
+ }
}
fn(null, bool, response);
diff --git a/helpers/mongoose-to-sqlite.js b/helpers/mongoose-to-sqlite.js
index 2df7c97344..eacaa9cb4f 100644
--- a/helpers/mongoose-to-sqlite.js
+++ b/helpers/mongoose-to-sqlite.js
@@ -134,7 +134,23 @@ async function updateMany(
}
}
- const condition = prepareQuery(mapping, filter);
+ {
+ const keys = Object.keys(filter);
+ for (const key of keys) {
+ if (key.startsWith('$') && key !== '$or')
+ throw new TypeError(`Key ${key} is not supported in updateMany`);
+ }
+ }
+
+ let condition = {};
+
+ if (filter.$or) {
+ condition = {
+ $or: filter.$or.map((v) => prepareQuery(mapping, v))
+ };
+ } else {
+ condition = prepareQuery(mapping, filter);
+ }
let beforeDocs = [];
diff --git a/helpers/on-auth.js b/helpers/on-auth.js
index c0343d8eb3..a6f520f3e7 100644
--- a/helpers/on-auth.js
+++ b/helpers/on-auth.js
@@ -17,6 +17,7 @@ const { isEmail } = require('validator');
const SMTPError = require('./smtp-error');
const ServerShutdownError = require('./server-shutdown-error');
const SocketError = require('./socket-error');
+const email = require('./email');
const parseRootDomain = require('./parse-root-domain');
const refineAndLogError = require('./refine-and-log-error');
const validateAlias = require('./validate-alias');
@@ -166,7 +167,7 @@ async function onAuth(auth, session, fn) {
})
.populate(
'user',
- `id ${config.userFields.isBanned} ${config.userFields.smtpLimit} email ${config.lastLocaleField} timezone`
+ `id has_imap ${config.userFields.isBanned} ${config.userFields.smtpLimit} email ${config.lastLocaleField} timezone`
)
.select('+tokens.hash +tokens.salt')
.lean()
@@ -421,55 +422,63 @@ async function onAuth(auth, session, fn) {
timezone: timeZone
};
- // TODO: redo this
- // used for imap backup
- if (alias) session.imap_backup_at = alias.imap_backup_at;
-
- //
- // TODO: run wsp to check if database can be opened
- // (if on IMAP/POP3/CalDAV) and if not then
- // do a hard reset and run a backup of the database
- //
- // if we're on IMAP/POP3/CalDAV then ensure that the password can open the database
- // TODO: probably should throw Invalid password error if it matches SQLITE not a db error (?)
- /*
- try {
- await refreshSession.call(
- this,
- {
- ...session,
- user
- },
- 'OPEN'
- );
- } catch (err) {
- // TODO: if an error occurs then decrease connection by 1
- err.isCodeBug = true;
- this.logger.fatal(err);
- }
- */
+ // this response object sets `session.user` to have `domain` and `alias`
+ //
+ fn(null, { user });
//
- // sync with tmp db in the background
- // (will cause imap-notifier to send IDLE notifications)
+ // if we're on IMAP or POP3 server then as a weekly courtesy
+ // if the user does not have IMAP storage enabled then
+ // alert them by email to inform them they need to enable IMAP
+ // (otherwise they're not going to have any mail received)
//
if (
- this.wsp &&
+ !alias.has_imap &&
(this.server instanceof IMAPServer || this.server instanceof POP3Server)
) {
- this.wsp
- .request({
- action: 'sync',
- timeout: ms('5m'),
- session: { user }
+ this.client
+ .get(`imap_check:${alias.id}`)
+ .then((cache) => {
+ if (cache) return;
+ this.client
+ .set(`imap_check:${alias.id}`, true, 'PX', ms('7d'))
+ .then(() => {
+ email({
+ template: 'alert',
+ message: {
+ to: user.owner_full_email,
+ bcc: config.email.message.from,
+ subject: i18n.translate(
+ 'IMAP_NOT_ENABLED_SUBJECT',
+ user.locale,
+ user.username
+ )
+ },
+ locals: {
+ message: i18n.translate(
+ 'IMAP_NOT_ENABLED_MESSAGE',
+ user.locale,
+ `${config.urls.web}/${user.locale}/my-account/domains/${
+ domain.name
+ }/aliases?q=${encodeURIComponent(user.username)}`,
+ user.username
+ )
+ }
+ })
+ .then()
+ .catch((err) => {
+ this.logger.fatal(err, { session });
+ // backoff for 30m for the next retry
+ this.client
+ .set(`imap_check:${alias.id}`, true, 'PX', ms('30m'))
+ .then()
+ .catch((err) => this.logger.fatal(err, { session }));
+ });
+ })
+ .catch((err) => this.logger.fatal(err, { session }));
})
- .then()
- .catch((err) => this.logger.fatal(err, { user }));
+ .catch((err) => this.logger.fatal(err, { session }));
}
-
- // this response object sets `session.user` to have `domain` and `alias`
- //
- fn(null, { user });
} catch (err) {
//
// NOTE: we should actually share error message if it was not a code bug
diff --git a/helpers/parse-error.js b/helpers/parse-error.js
index 8efce337ca..607e815b7d 100644
--- a/helpers/parse-error.js
+++ b/helpers/parse-error.js
@@ -4,6 +4,7 @@
*/
const ErrorStackParser = require('error-stack-parser');
+const _ = require('lodash');
const prepareStackTrace = require('prepare-stack-trace');
//
@@ -11,6 +12,11 @@ const prepareStackTrace = require('prepare-stack-trace');
//
//
function parseError(error) {
+ //
+ // since msgpackr can encode/decode Error objects
+ //
+ if (_.isError(error)) return error;
+
const err = new Error(error.message);
const { stack } = err;
diff --git a/helpers/parse-payload.js b/helpers/parse-payload.js
index 8f44864766..87296ab733 100644
--- a/helpers/parse-payload.js
+++ b/helpers/parse-payload.js
@@ -8,6 +8,8 @@ const os = require('node:os');
const path = require('node:path');
const { Buffer } = require('node:buffer');
const { isIP } = require('node:net');
+const { randomUUID } = require('node:crypto');
+
const { Headers, Splitter, Joiner } = require('mailsplit');
const Database = require('better-sqlite3-multiple-ciphers');
@@ -286,6 +288,8 @@ async function parsePayload(data, ws) {
//
if (ws && !isSANB(payload.id)) throw new TypeError('Payload id missing');
+ if (!isSANB(payload.id)) payload.id = randomUUID();
+
// action
if (!isSANB(payload.action) || !PAYLOAD_ACTIONS.has(payload.action))
throw new TypeError('Payload action missing or invalid');
@@ -754,6 +758,7 @@ async function parsePayload(data, ws) {
// TODO: unlock the temporary database
try {
+ tmpDb.pragma('analysis_limit=400');
tmpDb.pragma('optimize');
tmpDb.close();
} catch (err) {
@@ -876,7 +881,7 @@ async function parsePayload(data, ws) {
`id email ${config.userFields.isBanned} ${config.lastLocaleField}`
)
.select(
- 'id imap_backup_at 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'
)
.lean()
.exec();
@@ -924,8 +929,7 @@ async function parsePayload(data, ws) {
alias_public_key: alias.public_key,
locale: alias.user[config.lastLocaleField],
owner_full_email: alias.user.email
- },
- imap_backup_at: alias.imap_backup_at
+ }
};
// check quota
@@ -1376,6 +1380,7 @@ async function parsePayload(data, ws) {
}
try {
+ tmpDb.pragma('analysis_limit=400');
tmpDb.pragma('optimize');
tmpDb.close();
} catch (err) {
@@ -1387,7 +1392,9 @@ async function parsePayload(data, ws) {
} catch (err) {
logger.error(err, { payload });
err.isCodeBug = isCodeBug(err);
- errors[`${obj.address}`] = ws ? parseErr(err) : err;
+ // NOTE: since msgpackr supports encode/decode
+ // errors[`${obj.address}`] = ws ? parseErr(err) : err;
+ errors[`${obj.address}`] = err;
}
},
{ concurrency }
@@ -1838,6 +1845,7 @@ async function parsePayload(data, ws) {
backupDb.prepare('VACUUM').run();
try {
+ backupDb.pragma('analysis_limit=400');
backupDb.pragma('optimize');
backupDb.close();
} catch (err) {
@@ -1979,198 +1987,210 @@ async function parsePayload(data, ws) {
if (!_.isDate(new Date(payload.backup_at)))
throw new TypeError('Backup at invalid date');
- // only allow one backup at a time and once every hour
- const backupLock = await this.lock.waitAcquireLock(
- `${payload.session.user.alias_id}-backup`,
- ms('1h'), // expires after 1h
- ms('10s') // wait for 10s
+ // only allow one backup a day
+ const cache = await this.client.get(
+ `backup_check:${payload.session.user.alias_id}`
);
- if (!backupLock.success)
- throw i18n.translateError('IMAP_WRITE_LOCK_FAILED');
-
- let tmp;
- let backup;
- let err;
-
- try {
- // check how much space is remaining on storage location
- const storagePath = getPathToDatabase({
- id: payload.session.user.alias_id,
- storage_location: payload.session.user.storage_location
- });
- const diskSpace = await checkDiskSpace(storagePath);
- tmp = path.join(
- path.dirname(storagePath),
- `${payload.id}-backup.sqlite`
+ if (!cache) {
+ // set cache so we don't run two backups at once
+ await this.client.set(
+ `backup_check:${payload.session.user.alias_id}`,
+ true,
+ 'PX',
+ ms('1d')
);
- //
- const stats = await fs.promises.stat(storagePath);
- if (!stats.isFile() || stats.size === 0)
- throw new TypeError('Database empty');
-
- // we calculate size of db x 2 (backup + tarball)
- const spaceRequired = stats.size * 2;
-
- if (diskSpace.free < spaceRequired)
- throw new TypeError(
- `Needed ${prettyBytes(spaceRequired)} but only ${prettyBytes(
- diskSpace.free
- )} was available`
+ let tmp;
+ let backup;
+ let err;
+
+ try {
+ // check how much space is remaining on storage location
+ const storagePath = getPathToDatabase({
+ id: payload.session.user.alias_id,
+ storage_location: payload.session.user.storage_location
+ });
+ const diskSpace = await checkDiskSpace(storagePath);
+ tmp = path.join(
+ path.dirname(storagePath),
+ `${payload.id}-backup.sqlite`
);
- // create bucket on s3 if it doesn't already exist
- //
- const bucket = `${config.env}-${dashify(
- _.camelCase(payload.session.user.storage_location)
- )}`;
+ //
+ const stats = await fs.promises.stat(storagePath);
+ if (
+ !stats.isFile() ||
+ stats.size === 0 ||
+ stats.size <= config.INITIAL_DB_SIZE
+ )
+ throw new TypeError('Database empty');
+
+ // we calculate size of db x 2 (backup + tarball)
+ const spaceRequired = stats.size * 2;
+
+ if (diskSpace.free < spaceRequired)
+ throw new TypeError(
+ `Needed ${prettyBytes(spaceRequired)} but only ${prettyBytes(
+ diskSpace.free
+ )} was available`
+ );
- const key = `${payload.session.user.alias_id}.sqlite`;
+ // create bucket on s3 if it doesn't already exist
+ //
+ const bucket = `${config.env}-${dashify(
+ _.camelCase(payload.session.user.storage_location)
+ )}`;
- if (config.env !== 'test') {
- let res;
- try {
- res = await S3.send(
- new HeadBucketCommand({
- Bucket: bucket
- })
- );
- } catch (err) {
- if (err.name !== 'NotFound') throw err;
- }
+ const key = `${payload.session.user.alias_id}.sqlite`;
- if (res?.$metadata?.httpStatusCode !== 200) {
+ if (config.env !== 'test') {
+ let res;
try {
- await S3.send(
- new CreateBucketCommand({
- ACL: 'private',
+ res = await S3.send(
+ new HeadBucketCommand({
Bucket: bucket
})
);
} catch (err) {
- if (err.name !== 'BucketAlreadyOwnedByYou') throw err;
+ if (err.name !== 'NotFound') throw err;
}
- }
- }
- //
- // NOTE: we don't use `backup` command and instead use `VACUUM INTO`
- // because if a page is modified during backup, it has to start over
- //
- //
- //
- // also, if we used `backup` then for a temporary period
- // the database would be unencrypted on disk, and instead
- // we use VACUUM INTO which keeps the encryption as-is
- //
- //
- // const results = await db.backup(tmp);
- //
- // so instead we use the VACUUM INTO command with the `tmp` path
- //
+ if (res?.$metadata?.httpStatusCode !== 200) {
+ try {
+ await S3.send(
+ new CreateBucketCommand({
+ ACL: 'private',
+ Bucket: bucket
+ })
+ );
+ } catch (err) {
+ if (err.name !== 'BucketAlreadyOwnedByYou') throw err;
+ }
+ }
+ }
- // run a checkpoint to copy over wal to db
- db.pragma('wal_checkpoint(PASSIVE)');
+ //
+ // NOTE: we don't use `backup` command and instead use `VACUUM INTO`
+ // because if a page is modified during backup, it has to start over
+ //
+ //
+ //
+ // also, if we used `backup` then for a temporary period
+ // the database would be unencrypted on disk, and instead
+ // we use VACUUM INTO which keeps the encryption as-is
+ //
+ //
+ // const results = await db.backup(tmp);
+ //
+ // so instead we use the VACUUM INTO command with the `tmp` path
+ //
- // create backup
- const results = db.exec(`VACUUM INTO '${tmp}'`);
+ // run a checkpoint to copy over wal to db
+ db.pragma('wal_checkpoint(PASSIVE)');
- logger.debug('results', { results });
- backup = true;
+ // create backup
+ // takes approx 5-10s per GB
+ db.exec(`VACUUM INTO '${tmp}'`);
- // open the backup to ensure that encryption still valid
- const backupDb = await getDatabase(
- this,
- // alias
- {
- id: payload.session.user.alias_id,
- storage_location: payload.session.user.storage_location
- },
- payload.session,
- null,
- false,
- tmp
- );
+ backup = true;
- try {
- backupDb.pragma('optimize');
- backupDb.close();
- } catch (err) {
- logger.fatal(err, { payload });
- }
+ // open the backup to ensure that encryption still valid
+ const backupDb = await getDatabase(
+ this,
+ // alias
+ {
+ id: payload.session.user.alias_id,
+ storage_location: payload.session.user.storage_location
+ },
+ payload.session,
+ null,
+ false,
+ tmp
+ );
- // calculate hash of file
- const hash = await hasha.fromFile(tmp, { algorithm: 'sha256' });
+ try {
+ backupDb.pragma('analysis_limit=400');
+ backupDb.pragma('optimize');
+ backupDb.close();
+ } catch (err) {
+ logger.fatal(err, { payload });
+ }
- // check if hash already exists in s3
- try {
- const obj = await S3.send(
- new HeadObjectCommand({
- Bucket: bucket,
- Key: key
- })
- );
+ // calculate hash of file
+ const hash = await hasha.fromFile(tmp, { algorithm: 'sha256' });
- if (obj?.Metadata?.hash === hash)
- throw new TypeError('Hash already exists, returning early');
- } catch (err) {
- if (err.name !== 'NotFound') throw err;
- }
+ // check if hash already exists in s3
+ try {
+ const obj = await S3.send(
+ new HeadObjectCommand({
+ Bucket: bucket,
+ Key: key
+ })
+ );
- const upload = new Upload({
- client: S3,
- params: {
- Bucket: bucket,
- Key: key,
- Body: fs.createReadStream(tmp),
- Metadata: { hash }
+ if (obj?.Metadata?.hash === hash)
+ throw new TypeError('Hash already exists, returning early');
+ } catch (err) {
+ if (err.name !== 'NotFound') throw err;
}
- });
- await upload.done();
- // update alias imap backup date using provided time
- await Aliases.findOneAndUpdate(
- {
- _id: new mongoose.Types.ObjectId(payload.session.user.alias_id),
- domain: new mongoose.Types.ObjectId(
- payload.session.user.domain_id
- )
- },
- {
- $set: {
- imap_backup_at: new Date(payload.backup_at)
+ const upload = new Upload({
+ client: S3,
+ params: {
+ Bucket: bucket,
+ Key: key,
+ Body: fs.createReadStream(tmp),
+ Metadata: { hash }
}
- }
- );
- } catch (_err) {
- err = _err;
- }
-
- // always do cleanup in case of errors
- if (tmp && backup) {
- try {
- await fs.promises.rm(tmp, {
- force: true,
- recursive: true
});
- } catch (err) {
- logger.fatal(err, { payload });
+ await upload.done();
+
+ // update alias imap backup date using provided time
+ await Aliases.findOneAndUpdate(
+ {
+ _id: new mongoose.Types.ObjectId(payload.session.user.alias_id),
+ domain: new mongoose.Types.ObjectId(
+ payload.session.user.domain_id
+ )
+ },
+ {
+ $set: {
+ imap_backup_at: new Date(payload.backup_at)
+ }
+ }
+ );
+ } catch (_err) {
+ err = _err;
}
- }
- // release lock if any
- if (backupLock) {
- try {
- const result = await releaseLock(this, db, backupLock);
- if (!result.success)
- throw i18n.translateError('IMAP_RELEASE_LOCK_FAILED');
- } catch (err) {
- logger.fatal(err, { payload });
+ // always do cleanup in case of errors
+ if (tmp && backup) {
+ try {
+ await fs.promises.rm(tmp, {
+ force: true,
+ recursive: true
+ });
+ } catch (err) {
+ logger.fatal(err, { payload });
+ }
}
- }
- if (err) throw err;
+ // if an error occurred then allow cache to attempt again
+ // (but wait 30 minutes instead of 1 day)
+ if (err) {
+ this.client
+ .set(
+ `backup_check:${payload.session.user.alias_id}`,
+ true,
+ 'PX',
+ ms('30m')
+ )
+ .then()
+ .catch((err) => logger.fatal(err, { payload }));
+ if (err.message !== 'Database empty') throw err;
+ }
+ }
response = {
id: payload.id,
@@ -2195,7 +2215,11 @@ async function parsePayload(data, ws) {
if (db && db.open && typeof db.close === 'function') {
try {
- if (typeof db.pragma === 'function') db.pragma('optimize');
+ if (typeof db.pragma === 'function') {
+ db.pragma('analysis_limit=400');
+ db.pragma('optimize');
+ }
+
db.close();
} catch (err) {
logger.fatal(err, { payload });
@@ -2231,6 +2255,7 @@ async function parsePayload(data, ws) {
if (db && db.open && typeof db.close === 'function') {
try {
+ db.pragma('analysis_limit=400');
db.pragma('optimize');
db.close();
} catch (err) {
@@ -2244,7 +2269,9 @@ async function parsePayload(data, ws) {
ws.send(
encoder.pack({
id: payload.id,
- err: parseErr(err)
+ err
+ // err: parseErr(err), // NOTE: results in RangeError: Maximum call stack size exceeded
+ // err: safeStringify(parseErr(err))
})
);
}
diff --git a/helpers/refresh-session.js b/helpers/refresh-session.js
index 059b3d743f..a043d27407 100644
--- a/helpers/refresh-session.js
+++ b/helpers/refresh-session.js
@@ -3,10 +3,7 @@
* SPDX-License-Identifier: BUSL-1.1
*/
-const _ = require('lodash');
-const dayjs = require('dayjs-with-plugins');
const isSANB = require('is-string-and-not-blank');
-const ms = require('ms');
const Aliases = require('#models/aliases');
const Domains = require('#models/domains');
@@ -16,6 +13,7 @@ const SocketError = require('#helpers/socket-error');
const config = require('#config');
const getDatabase = require('#helpers/get-database');
const i18n = require('#helpers/i18n');
+const parsePayload = require('#helpers/parse-payload');
const validateAlias = require('#helpers/validate-alias');
const validateDomain = require('#helpers/validate-domain');
@@ -185,109 +183,36 @@ async function refreshSession(session, command) {
this.server.notifier.fire(session.user.alias_id);
//
- // if and only if we're not an instance of IMAP
- // (otherwise this would result in recursion)
+ // only perform sync and backup during read commands on sqlite server
+ // (these are specifically commands for when the user is attempting to get messages)
//
- // prevent circular dep (otherwise we could do instanceof)
if (
- // don't perform backup while during a write command
- ['POP3', 'FETCH', 'GETQUOTAROOT', 'GETQUOTA', 'LIST', 'STATUS'].includes(
- command
- ) &&
- config.env !== 'test' &&
- this?.constructor?.name !== 'IMAP' && //
- // NOTE: this takes 100ms+ so we put it in onAuth instead running in the background
- // (if a message gets delivered to tmp then it will notify IMAP connections already)
- //
- // sync with temp db on every request
- //
- // const sync = await this.wsp.request.call(this, {
- // action: 'sync',
- // timeout: ms('10s'),
- // session: { user: session.user }
- // });
- // this.logger.debug('sync', { sync });
-
- // TODO: this needs limited to only being one once per alias across all its IMAP connections
- // offset by 10s to prevent locking db while a read is in progress
- !session.backupInProgress
+ needsRefreshed &&
+ !this.wsp && // ensures SQLite only
+ ['POP3', 'GETQUOTAROOT', 'GETQUOTA', 'STATUS'].includes(command)
) {
- session.backupInProgress = true;
- setTimeout(() => {
- //
- // daily backups
- //
- const oneDayAgo = dayjs().subtract(1, 'day').toDate();
- const now = new Date();
- if (
- !_.isDate(session.imap_backup_at) ||
- new Date(session.imap_backup_at).getTime() <= oneDayAgo.getTime()
- ) {
- Aliases.findOneAndUpdate(
- {
- id: session.user.alias_id,
- imap_backup_at: _.isDate(session.imap_backup_at)
- ? {
- $exists: true,
- $lte: oneDayAgo
- }
- : {
- $exists: false
- }
- },
- {
- $set: {
- imap_backup_at: now
- }
- }
- )
- .then((alias) => {
- // return early if no alias found (point in time safeguard)
- if (!alias) {
- session.backupInProgress = false;
- return;
- }
+ // sync with tmp db
+ try {
+ const sync = await parsePayload.call(this, {
+ action: 'sync',
+ session: { user: session.user }
+ });
+ this.logger.debug('tmp db sync complete', { sync, session });
+ } catch (err) {
+ this.logger.fatal(err, { session });
+ }
- this.wsp.request
- .call(this, {
- action: 'backup',
- backup_at: now.toISOString(),
- session: { user: session.user }
- })
- .then(() => {
- this.logger.debug('backup performed', { session });
- session.backupInProgress = false;
- })
- .catch((err) => {
- this.logger.fatal(err, { session });
- // if the backup failed then we unset the imap_backup_at
- session.imap_backup_at = undefined;
- Aliases.findOneAndUpdate(
- {
- _id: alias._id,
- imap_backup_at: now
- },
- {
- $unset: {
- imap_backup_at: 1
- }
- }
- )
- .then(() => {
- session.backupInProgress = false;
- })
- .catch((err) => {
- this.logger.fatal(err, { session });
- session.backupInProgress = false;
- });
- });
- })
- .catch((err) => {
- this.logger.fatal(err, { session });
- session.backupInProgress = false;
- });
- }
- }, ms('10s'));
+ // daily backup (run in background)
+ parsePayload
+ .call(this, {
+ action: 'backup',
+ backup_at: new Date().toISOString(),
+ session: { user: session.user }
+ })
+ .then((backup) => {
+ this.logger.debug('backup complete', { backup, session });
+ })
+ .catch((err) => this.logger.fatal(err, { session }));
}
}
diff --git a/helpers/setup-pragma.js b/helpers/setup-pragma.js
index 06072bdc44..d9d7f6b43b 100644
--- a/helpers/setup-pragma.js
+++ b/helpers/setup-pragma.js
@@ -68,7 +68,7 @@ async function setupPragma(db, session, cipher = 'chacha20') {
db.pragma('secure_delete=ON');
//
- // NOTE: we still run a manual vacuum every 24 hours
+ // NOTE: we still run a mangal vacuum every 24 hours
//
// turn on auto vacuum (for large amounts of deleted content)
//
@@ -80,7 +80,7 @@ async function setupPragma(db, session, cipher = 'chacha20') {
//
db.pragma('synchronous=NORMAL');
- // TODO: db.pragma('synchronous=EXTRA');
+ // db.pragma('synchronous=EXTRA');
//
// NOTE: only if we're using Litestream
diff --git a/locales/ar.json b/locales/ar.json
index 41a39c47af..97e0a9400a 100644
--- a/locales/ar.json
+++ b/locales/ar.json
@@ -7402,5 +7402,7 @@
": This will override the existing alias password and database completely, and will permanently delete the existing IMAP storage and reset the alias' SQLite email database completely. Please make a backup if possible if you have an existing mailbox attached to this alias.": ": سيؤدي هذا إلى تجاوز كلمة المرور الحالية للاسم المستعار وقاعدة البيانات بالكامل، وسيؤدي إلى حذف مساحة تخزين IMAP الموجودة بشكل دائم وإعادة تعيين قاعدة بيانات البريد الإلكتروني SQLite الخاصة بالاسم المستعار بالكامل. الرجاء عمل نسخة احتياطية إن أمكن إذا كان لديك صندوق بريد موجود متصل بهذا الاسم المستعار.",
"We accept Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, SEPA Direct Debit, Canadian pre-authorized debits, and ACH Direct Debit.": "نحن نقبل Visa وMastercard وAmerican Express وDiscover وDiners Club وJCB وChina UnionPay وAlipay وApple Pay وGoogle Pay وAmazon Pay وCash App وLink وBancontact وEPS وgiropay وiDEAL وPrzelewy24 وSofort وAfirm وAfterpay. / Clearpay، وKlarna، والخصم المباشر من منطقة SEPA، والخصم الكندي المصرح به مسبقًا، والخصم المباشر من ACH.",
"500,000+ custom domain names": "أكثر من 500.000 اسم نطاق مخصص",
- "LineageOS": "LineageOS"
+ "LineageOS": "LineageOS",
+ "Enable IMAP to receive mail for %s": "Enable IMAP to receive mail for %s",
+ "Please edit your alias and enable IMAP to receive mail for %s.": "Please edit your alias and enable IMAP to receive mail for %s."
}
\ No newline at end of file
diff --git a/locales/cs.json b/locales/cs.json
index c0b7f00cd7..4d505cb5ab 100644
--- a/locales/cs.json
+++ b/locales/cs.json
@@ -7402,5 +7402,7 @@
": This will override the existing alias password and database completely, and will permanently delete the existing IMAP storage and reset the alias' SQLite email database completely. Please make a backup if possible if you have an existing mailbox attached to this alias.": ": Toto zcela přepíše stávající heslo aliasu a databázi a trvale odstraní stávající úložiště IMAP a úplně resetuje e-mailovou databázi aliasu SQLite. Pokud je to možné, proveďte zálohu, pokud máte k tomuto aliasu připojenou existující poštovní schránku.",
"We accept Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, SEPA Direct Debit, Canadian pre-authorized debits, and ACH Direct Debit.": "Přijímáme karty Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, SEPA inkaso, kanadská předautorizovaná inkasa a ACH Direct Debit.",
"500,000+ custom domain names": "Více než 500 000 vlastních doménových jmen",
- "LineageOS": "LineageOS"
+ "LineageOS": "LineageOS",
+ "Enable IMAP to receive mail for %s": "Enable IMAP to receive mail for %s",
+ "Please edit your alias and enable IMAP to receive mail for %s.": "Please edit your alias and enable IMAP to receive mail for %s."
}
\ No newline at end of file
diff --git a/locales/da.json b/locales/da.json
index cb9bad1c95..885d0dbcf9 100644
--- a/locales/da.json
+++ b/locales/da.json
@@ -3964,5 +3964,9 @@
"Storage used by this alias": "Lager, der bruges af dette alias",
"alias": "alias",
"Update Alias": "Opdater Alias",
- "Recipients must be a line-break/space/comma separated list of valid email addresses, fully-qualified domain names (\"FQDN\"), IP addresses, and/or webhook URL's. We will automatically remove duplicate entries for you and perform validation when you click \"Update Alias\" below.": "Modtagere skal være en linjeskift/mellemrum/kommasepareret liste over gyldige e-mailadresser, fuldt kvalificerede domænenavne (\"FQDN\"), IP-adresser og/eller webhook-URL'er. Vi fjerner automatisk duplikerede poster for dig og udfører validering, når du klikker på \"Opdater alias\" nedenfor."
+ "Recipients must be a line-break/space/comma separated list of valid email addresses, fully-qualified domain names (\"FQDN\"), IP addresses, and/or webhook URL's. We will automatically remove duplicate entries for you and perform validation when you click \"Update Alias\" below.": "Modtagere skal være en linjeskift/mellemrum/kommasepareret liste over gyldige e-mailadresser, fuldt kvalificerede domænenavne (\"FQDN\"), IP-adresser og/eller webhook-URL'er. Vi fjerner automatisk duplikerede poster for dig og udfører validering, når du klikker på \"Opdater alias\" nedenfor.",
+ "Payment due %s.": "Payment due %s.",
+ "Need to enable auto-renew?": "Need to enable auto-renew?",
+ "Enable IMAP to receive mail for %s": "Enable IMAP to receive mail for %s",
+ "Please edit your alias and enable IMAP to receive mail for %s.": "Please edit your alias and enable IMAP to receive mail for %s."
}
\ No newline at end of file
diff --git a/locales/de.json b/locales/de.json
index fcdc2bab94..5759f6bd44 100644
--- a/locales/de.json
+++ b/locales/de.json
@@ -6440,5 +6440,7 @@
": This will override the existing alias password and database completely, and will permanently delete the existing IMAP storage and reset the alias' SQLite email database completely. Please make a backup if possible if you have an existing mailbox attached to this alias.": ": Dadurch werden das vorhandene Alias-Passwort und die Datenbank vollständig überschrieben, der vorhandene IMAP-Speicher dauerhaft gelöscht und die SQLite-E-Mail-Datenbank des Alias vollständig zurückgesetzt. Wenn Sie ein vorhandenes Postfach an diesen Alias angehängt haben, erstellen Sie nach Möglichkeit eine Sicherungskopie.",
"We accept Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, SEPA Direct Debit, Canadian pre-authorized debits, and ACH Direct Debit.": "Wir akzeptieren Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, SEPA-Lastschrift, kanadische Lastschriftmandate und ACH-Lastschrift.",
"500,000+ custom domain names": "Über 500.000 benutzerdefinierte Domänennamen",
- "LineageOS": "LineageOS"
+ "LineageOS": "LineageOS",
+ "Enable IMAP to receive mail for %s": "Enable IMAP to receive mail for %s",
+ "Please edit your alias and enable IMAP to receive mail for %s.": "Please edit your alias and enable IMAP to receive mail for %s."
}
\ No newline at end of file
diff --git a/locales/en.json b/locales/en.json
index afe4d5b95f..4cf3930526 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -7154,5 +7154,8 @@
": This will override the existing alias password and database completely, and will permanently delete the existing IMAP storage and reset the alias' SQLite email database completely. Please make a backup if possible if you have an existing mailbox attached to this alias.": ": This will override the existing alias password and database completely, and will permanently delete the existing IMAP storage and reset the alias' SQLite email database completely. Please make a backup if possible if you have an existing mailbox attached to this alias.",
"We accept Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, SEPA Direct Debit, Canadian pre-authorized debits, and ACH Direct Debit.": "We accept Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, SEPA Direct Debit, Canadian pre-authorized debits, and ACH Direct Debit.",
"500,000+ custom domain names": "500,000+ custom domain names",
- "LineageOS": "LineageOS"
+ "LineageOS": "LineageOS",
+ "New password created for %s. This action was done by %s.": "New password created for %s. This action was done by %s.",
+ "Enable IMAP to receive mail for %s": "Enable IMAP to receive mail for %s",
+ "Please edit your alias and enable IMAP to receive mail for %s.": "Please edit your alias and enable IMAP to receive mail for %s."
}
\ No newline at end of file
diff --git a/locales/es.json b/locales/es.json
index 72f9c33d18..d54fee4105 100644
--- a/locales/es.json
+++ b/locales/es.json
@@ -7400,5 +7400,7 @@
": This will override the existing alias password and database completely, and will permanently delete the existing IMAP storage and reset the alias' SQLite email database completely. Please make a backup if possible if you have an existing mailbox attached to this alias.": ": Esto anulará por completo la contraseña del alias y la base de datos existentes, eliminará permanentemente el almacenamiento IMAP existente y restablecerá completamente la base de datos de correo electrónico SQLite del alias. Si es posible, haga una copia de seguridad si tiene un buzón de correo existente adjunto a este alias.",
"We accept Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, SEPA Direct Debit, Canadian pre-authorized debits, and ACH Direct Debit.": "Aceptamos Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay. / Clearpay, Klarna, Débito Directo SEPA, débitos preautorizados canadienses y Débito Directo ACH.",
"500,000+ custom domain names": "Más de 500.000 nombres de dominio personalizados",
- "LineageOS": "LineageOS"
+ "LineageOS": "LineageOS",
+ "Enable IMAP to receive mail for %s": "Enable IMAP to receive mail for %s",
+ "Please edit your alias and enable IMAP to receive mail for %s.": "Please edit your alias and enable IMAP to receive mail for %s."
}
\ No newline at end of file
diff --git a/locales/fi.json b/locales/fi.json
index 182acec32e..2ed59e104a 100644
--- a/locales/fi.json
+++ b/locales/fi.json
@@ -7249,5 +7249,7 @@
": This will override the existing alias password and database completely, and will permanently delete the existing IMAP storage and reset the alias' SQLite email database completely. Please make a backup if possible if you have an existing mailbox attached to this alias.": ": Tämä ohittaa nykyisen aliaksen salasanan ja tietokannan kokonaan ja poistaa pysyvästi olemassa olevan IMAP-tallennustilan ja nollaa aliaksen SQLite-sähköpostitietokannan kokonaan. Tee varmuuskopio, jos mahdollista, jos sinulla on tähän aliakseen liitetty postilaatikko.",
"We accept Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, SEPA Direct Debit, Canadian pre-authorized debits, and ACH Direct Debit.": "Hyväksymme Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, SEPA-suoraveloitus, Kanadan ennakkovaltuutetut veloitukset ja ACH-suoraveloitus.",
"500,000+ custom domain names": "Yli 500 000 mukautettua verkkotunnusta",
- "LineageOS": "LineageOS"
+ "LineageOS": "LineageOS",
+ "Enable IMAP to receive mail for %s": "Enable IMAP to receive mail for %s",
+ "Please edit your alias and enable IMAP to receive mail for %s.": "Please edit your alias and enable IMAP to receive mail for %s."
}
\ No newline at end of file
diff --git a/locales/fr.json b/locales/fr.json
index 4909403fda..fbe7af9be1 100644
--- a/locales/fr.json
+++ b/locales/fr.json
@@ -4905,5 +4905,7 @@
"Need secure and private email?": "Besoin d'une messagerie sécurisée et privée ?",
"LineageOS": "LignéeOS",
"Storage used by this alias": "Stockage utilisé par cet alias",
- "alias": "alias"
+ "alias": "alias",
+ "Enable IMAP to receive mail for %s": "Enable IMAP to receive mail for %s",
+ "Please edit your alias and enable IMAP to receive mail for %s.": "Please edit your alias and enable IMAP to receive mail for %s."
}
\ No newline at end of file
diff --git a/locales/he.json b/locales/he.json
index 116ddebe70..aaff08b14b 100644
--- a/locales/he.json
+++ b/locales/he.json
@@ -5401,5 +5401,7 @@
"Make payment immediately to avoid account termination.": "בצע תשלום מיידי כדי למנוע סגירת חשבון.",
"500,000+ custom domain names": "500,000+ שמות דומיין מותאמים אישית",
"Need secure and private email?": "זקוק לאימייל מאובטח ופרטי?",
- "LineageOS": "LineageOS"
+ "LineageOS": "LineageOS",
+ "Enable IMAP to receive mail for %s": "אפשר IMAP כדי לקבל דואר עבור %s",
+ "Please edit your alias and enable IMAP to receive mail for %s.": "אנא ערוך את הכינוי שלך ואפשר ל-IMAP לקבל דואר עבור %s ."
}
\ No newline at end of file
diff --git a/locales/hu.json b/locales/hu.json
index 74312fd189..41608dde74 100644
--- a/locales/hu.json
+++ b/locales/hu.json
@@ -7402,5 +7402,7 @@
": This will override the existing alias password and database completely, and will permanently delete the existing IMAP storage and reset the alias' SQLite email database completely. Please make a backup if possible if you have an existing mailbox attached to this alias.": ": Ez teljesen felülírja a meglévő alias jelszavát és adatbázisát, és véglegesen törli a meglévő IMAP tárhelyet, és teljesen visszaállítja az alias SQLite e-mail adatbázisát. Kérjük, ha lehetséges, készítsen biztonsági másolatot, ha ehhez az álnévhez már van postafiókja csatolva.",
"We accept Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, SEPA Direct Debit, Canadian pre-authorized debits, and ACH Direct Debit.": "Elfogadunk: Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, SEPA csoportos beszedési megbízás, kanadai előzetesen engedélyezett beszedési megbízások és ACH csoportos beszedési megbízások.",
"500,000+ custom domain names": "500 000+ egyéni domain név",
- "LineageOS": "LineageOS"
+ "LineageOS": "LineageOS",
+ "Enable IMAP to receive mail for %s": "Engedélyezze az IMAP-ot %s e-mailek fogadásához",
+ "Please edit your alias and enable IMAP to receive mail for %s.": "Kérjük, szerkessze az aliasát , és engedélyezze az IMAP-ot %s címhez tartozó levelek fogadásához."
}
\ No newline at end of file
diff --git a/locales/id.json b/locales/id.json
index 7e976b2291..0b25928fac 100644
--- a/locales/id.json
+++ b/locales/id.json
@@ -7402,5 +7402,7 @@
": This will override the existing alias password and database completely, and will permanently delete the existing IMAP storage and reset the alias' SQLite email database completely. Please make a backup if possible if you have an existing mailbox attached to this alias.": ": Ini akan sepenuhnya mengganti kata sandi dan basis data alias yang ada, dan akan menghapus secara permanen penyimpanan IMAP yang ada dan mengatur ulang basis data email SQLite alias sepenuhnya. Harap buat cadangan jika memungkinkan jika Anda memiliki kotak surat yang terpasang pada alias ini.",
"We accept Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, SEPA Direct Debit, Canadian pre-authorized debits, and ACH Direct Debit.": "Kami menerima Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Aplikasi Tunai, Tautan, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, Debit Langsung SEPA, debit pra-otorisasi Kanada, dan Debit Langsung ACH.",
"500,000+ custom domain names": "500.000+ nama domain khusus",
- "LineageOS": "LineageOS"
+ "LineageOS": "LineageOS",
+ "Enable IMAP to receive mail for %s": "Aktifkan IMAP untuk menerima email sebesar %s",
+ "Please edit your alias and enable IMAP to receive mail for %s.": "Harap edit alias Anda dan aktifkan IMAP untuk menerima email untuk %s ."
}
\ No newline at end of file
diff --git a/locales/it.json b/locales/it.json
index f4090e6d71..839ac6f771 100644
--- a/locales/it.json
+++ b/locales/it.json
@@ -7402,5 +7402,7 @@
": This will override the existing alias password and database completely, and will permanently delete the existing IMAP storage and reset the alias' SQLite email database completely. Please make a backup if possible if you have an existing mailbox attached to this alias.": ": questa operazione sovrascriverà completamente la password e il database dell'alias esistente, eliminerà permanentemente l'archivio IMAP esistente e reimposterà completamente il database di posta elettronica SQLite dell'alias. Se possibile, esegui un backup se hai una casella di posta esistente collegata a questo alias.",
"We accept Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, SEPA Direct Debit, Canadian pre-authorized debits, and ACH Direct Debit.": "Accettiamo Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, addebito diretto SEPA, addebiti preautorizzati canadesi e addebito diretto ACH.",
"500,000+ custom domain names": "Oltre 500.000 nomi di dominio personalizzati",
- "LineageOS": "LineageOS"
+ "LineageOS": "LineageOS",
+ "Enable IMAP to receive mail for %s": "Abilita IMAP per ricevere posta per %s",
+ "Please edit your alias and enable IMAP to receive mail for %s.": "Modifica il tuo alias e abilita IMAP per ricevere posta per %s ."
}
\ No newline at end of file
diff --git a/locales/ja.json b/locales/ja.json
index cde4368ee0..c9b0ce0166 100644
--- a/locales/ja.json
+++ b/locales/ja.json
@@ -7402,5 +7402,7 @@
": This will override the existing alias password and database completely, and will permanently delete the existing IMAP storage and reset the alias' SQLite email database completely. Please make a backup if possible if you have an existing mailbox attached to this alias.": ": これにより、既存のエイリアスのパスワードとデータベースが完全に上書きされ、既存の IMAP ストレージが完全に削除され、エイリアスの SQLite 電子メール データベースが完全にリセットされます。このエイリアスに既存のメールボックスが接続されている場合は、可能であればバックアップを作成してください。",
"We accept Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, SEPA Direct Debit, Canadian pre-authorized debits, and ACH Direct Debit.": "弊社では、Visa、Mastercard、American Express、Discover、Diners Club、JCB、China UnionPay、Alipay、Apple Pay、Google Pay、Amazon Pay、Cash App、Link、Bancontact、EPS、giropay、iDEAL、Przelewy24、Sofort、Affirm、Afterpay / Clearpay、Klarna、SEPA 口座振替、カナダの事前承認口座振替、ACH 口座振替をご利用いただけます。",
"500,000+ custom domain names": "500,000以上のカスタムドメイン名",
- "LineageOS": "リネージュOS"
+ "LineageOS": "リネージュOS",
+ "Enable IMAP to receive mail for %s": "%sのメールを受信するには IMAP を有効にしてください",
+ "Please edit your alias and enable IMAP to receive mail for %s.": "エイリアスを編集し、IMAP を有効にして%sのメールを受信してください。"
}
\ No newline at end of file
diff --git a/locales/ko.json b/locales/ko.json
index e032ec5079..38735bf940 100644
--- a/locales/ko.json
+++ b/locales/ko.json
@@ -7402,5 +7402,7 @@
": This will override the existing alias password and database completely, and will permanently delete the existing IMAP storage and reset the alias' SQLite email database completely. Please make a backup if possible if you have an existing mailbox attached to this alias.": ": 이렇게 하면 기존 별칭 비밀번호와 데이터베이스가 완전히 무시되고 기존 IMAP 저장소가 영구적으로 삭제되며 별칭의 SQLite 이메일 데이터베이스가 완전히 재설정됩니다. 이 별칭에 기존 사서함이 연결되어 있는 경우 가능하면 백업을 만드십시오.",
"We accept Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, SEPA Direct Debit, Canadian pre-authorized debits, and ACH Direct Debit.": "Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay를 사용할 수 있습니다. / Clearpay, Klarna, SEPA 자동 이체, 캐나다 사전 승인 이체, ACH 자동 이체.",
"500,000+ custom domain names": "500,000개 이상의 사용자 정의 도메인 이름",
- "LineageOS": "리니지OS"
+ "LineageOS": "리니지OS",
+ "Enable IMAP to receive mail for %s": "%s 에 대한 메일을 받으려면 IMAP을 활성화하세요.",
+ "Please edit your alias and enable IMAP to receive mail for %s.": "%s 에 대한 메일을 수신하려면 별칭을 편집 하고 IMAP을 활성화하십시오."
}
\ No newline at end of file
diff --git a/locales/nl.json b/locales/nl.json
index b8bf6522f7..b6e9fc4898 100644
--- a/locales/nl.json
+++ b/locales/nl.json
@@ -7402,5 +7402,7 @@
": This will override the existing alias password and database completely, and will permanently delete the existing IMAP storage and reset the alias' SQLite email database completely. Please make a backup if possible if you have an existing mailbox attached to this alias.": ": Dit zal het bestaande aliaswachtwoord en de database volledig overschrijven, en zal de bestaande IMAP-opslag permanent verwijderen en de SQLite-e-maildatabase van de alias volledig opnieuw instellen. Maak indien mogelijk een back-up als u een bestaande mailbox aan deze alias heeft gekoppeld.",
"We accept Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, SEPA Direct Debit, Canadian pre-authorized debits, and ACH Direct Debit.": "Wij accepteren Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, SEPA-automatische incasso, Canadese vooraf geautoriseerde afschrijvingen en ACH-automatische incasso.",
"500,000+ custom domain names": "Meer dan 500.000 aangepaste domeinnamen",
- "LineageOS": "LineageOS"
+ "LineageOS": "LineageOS",
+ "Enable IMAP to receive mail for %s": "Schakel IMAP in om e-mail te ontvangen voor %s",
+ "Please edit your alias and enable IMAP to receive mail for %s.": "Bewerk uw alias en schakel IMAP in om e-mail te ontvangen voor %s ."
}
\ No newline at end of file
diff --git a/locales/no.json b/locales/no.json
index c862028a6c..d7e56c0ce7 100644
--- a/locales/no.json
+++ b/locales/no.json
@@ -7407,5 +7407,7 @@
": This will override the existing alias password and database completely, and will permanently delete the existing IMAP storage and reset the alias' SQLite email database completely. Please make a backup if possible if you have an existing mailbox attached to this alias.": ": Dette vil overstyre det eksisterende aliaspassordet og databasen fullstendig, og vil permanent slette den eksisterende IMAP-lagringen og tilbakestille aliasets SQLite-e-postdatabase fullstendig. Ta en sikkerhetskopi hvis mulig hvis du har en eksisterende postboks knyttet til dette aliaset.",
"We accept Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, SEPA Direct Debit, Canadian pre-authorized debits, and ACH Direct Debit.": "Vi aksepterer Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, SEPA direkte belastning, kanadisk forhåndsgodkjent belastning og ACH direkte belastning.",
"500,000+ custom domain names": "500 000+ egendefinerte domenenavn",
- "LineageOS": "LineageOS"
+ "LineageOS": "LineageOS",
+ "Enable IMAP to receive mail for %s": "Aktiver IMAP for å motta e-post for %s",
+ "Please edit your alias and enable IMAP to receive mail for %s.": "Rediger aliaset ditt og aktiver IMAP for å motta e-post for %s ."
}
\ No newline at end of file
diff --git a/locales/pl.json b/locales/pl.json
index 2521195327..b75b40a10a 100644
--- a/locales/pl.json
+++ b/locales/pl.json
@@ -7402,5 +7402,7 @@
": This will override the existing alias password and database completely, and will permanently delete the existing IMAP storage and reset the alias' SQLite email database completely. Please make a backup if possible if you have an existing mailbox attached to this alias.": ": Spowoduje to całkowite zastąpienie istniejącego hasła i bazy danych aliasu oraz trwałe usunięcie istniejącej pamięci IMAP i całkowite zresetowanie bazy danych e-mail SQLite aliasu. Jeśli to możliwe, wykonaj kopię zapasową, jeśli masz istniejącą skrzynkę pocztową podłączoną do tego aliasu.",
"We accept Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, SEPA Direct Debit, Canadian pre-authorized debits, and ACH Direct Debit.": "Akceptujemy Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, SEPA Direct Debit, preautoryzowane polecenia zapłaty w Kanadzie i polecenie zapłaty ACH.",
"500,000+ custom domain names": "Ponad 500 000 niestandardowych nazw domen",
- "LineageOS": "LineageOS"
+ "LineageOS": "LineageOS",
+ "Enable IMAP to receive mail for %s": "Włącz IMAP, aby odbierać pocztę dla %s",
+ "Please edit your alias and enable IMAP to receive mail for %s.": "Zmień swój alias i włącz protokół IMAP, aby odbierać pocztę dla %s ."
}
\ No newline at end of file
diff --git a/locales/pt.json b/locales/pt.json
index e79eda623f..9367a29a92 100644
--- a/locales/pt.json
+++ b/locales/pt.json
@@ -7402,5 +7402,7 @@
": This will override the existing alias password and database completely, and will permanently delete the existing IMAP storage and reset the alias' SQLite email database completely. Please make a backup if possible if you have an existing mailbox attached to this alias.": ": isso substituirá completamente a senha e o banco de dados do alias existente, excluirá permanentemente o armazenamento IMAP existente e redefinirá completamente o banco de dados de e-mail SQLite do alias. Faça um backup, se possível, se você tiver uma caixa de correio anexada a este alias.",
"We accept Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, SEPA Direct Debit, Canadian pre-authorized debits, and ACH Direct Debit.": "Aceitamos Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, débito direto SEPA, débitos pré-autorizados canadenses e débito direto ACH.",
"500,000+ custom domain names": "Mais de 500.000 nomes de domínio personalizados",
- "LineageOS": "Lineage OS"
+ "LineageOS": "Lineage OS",
+ "Enable IMAP to receive mail for %s": "Habilite o IMAP para receber e-mails de %s",
+ "Please edit your alias and enable IMAP to receive mail for %s.": "Edite seu alias e ative o IMAP para receber mensagens de %s ."
}
\ No newline at end of file
diff --git a/locales/ru.json b/locales/ru.json
index e1dbaa47ed..91f7c5f666 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -7402,5 +7402,7 @@
": This will override the existing alias password and database completely, and will permanently delete the existing IMAP storage and reset the alias' SQLite email database completely. Please make a backup if possible if you have an existing mailbox attached to this alias.": ": это полностью заменит существующий пароль и базу данных псевдонима, а также навсегда удалит существующее хранилище IMAP и полностью сбросит базу данных электронной почты SQLite псевдонима. Если возможно, сделайте резервную копию, если у вас уже есть почтовый ящик, привязанный к этому псевдониму.",
"We accept Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, SEPA Direct Debit, Canadian pre-authorized debits, and ACH Direct Debit.": "Мы принимаем Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, прямой дебет SEPA, предварительно авторизованный дебет в Канаде и прямой дебет ACH.",
"500,000+ custom domain names": "Более 500 000 пользовательских доменных имен",
- "LineageOS": "LineageOS"
+ "LineageOS": "LineageOS",
+ "Enable IMAP to receive mail for %s": "Включите IMAP для получения почты для %s",
+ "Please edit your alias and enable IMAP to receive mail for %s.": "Пожалуйста, измените свой псевдоним и включите IMAP для получения почты для %s ."
}
\ No newline at end of file
diff --git a/locales/sv.json b/locales/sv.json
index a7882143e2..b670a61a9a 100644
--- a/locales/sv.json
+++ b/locales/sv.json
@@ -7402,5 +7402,7 @@
": This will override the existing alias password and database completely, and will permanently delete the existing IMAP storage and reset the alias' SQLite email database completely. Please make a backup if possible if you have an existing mailbox attached to this alias.": ": Detta kommer att åsidosätta det befintliga aliaslösenordet och databasen helt och ta bort det befintliga IMAP-lagringsutrymmet permanent och återställa aliasets SQLite-e-postdatabas helt. Vänligen gör en säkerhetskopia om det är möjligt om du har en befintlig brevlåda kopplad till detta alias.",
"We accept Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, SEPA Direct Debit, Canadian pre-authorized debits, and ACH Direct Debit.": "Vi accepterar Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, SEPA autogiro, kanadensiska förauktoriserade debiteringar och ACH direktdebitering.",
"500,000+ custom domain names": "500 000+ anpassade domännamn",
- "LineageOS": "LineageOS"
+ "LineageOS": "LineageOS",
+ "Enable IMAP to receive mail for %s": "Aktivera IMAP för att ta emot e-post för %s",
+ "Please edit your alias and enable IMAP to receive mail for %s.": "Vänligen redigera ditt alias och aktivera IMAP för att ta emot e-post för %s ."
}
\ No newline at end of file
diff --git a/locales/th.json b/locales/th.json
index ec7e56e1bb..e3144148f6 100644
--- a/locales/th.json
+++ b/locales/th.json
@@ -7402,5 +7402,7 @@
": This will override the existing alias password and database completely, and will permanently delete the existing IMAP storage and reset the alias' SQLite email database completely. Please make a backup if possible if you have an existing mailbox attached to this alias.": ": สิ่งนี้จะแทนที่รหัสผ่านและฐานข้อมูลนามแฝงที่มีอยู่ทั้งหมด และจะลบที่เก็บข้อมูล IMAP ที่มีอยู่อย่างถาวรและรีเซ็ตฐานข้อมูลอีเมล SQLite ของนามแฝงทั้งหมด โปรดทำการสำรองข้อมูลหากเป็นไปได้หากคุณมีกล่องจดหมายแนบอยู่กับนามแฝงนี้",
"We accept Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, SEPA Direct Debit, Canadian pre-authorized debits, and ACH Direct Debit.": "เรารับ Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, แอปเงินสด, ลิงก์, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, ยืนยัน, Afterpay / Clearpay, Klarna, SEPA Direct Debit, การหักบัญชีที่ได้รับการอนุมัติล่วงหน้าของแคนาดา และ ACH Direct Debit",
"500,000+ custom domain names": "ชื่อโดเมนที่กำหนดเองมากกว่า 500,000+ ชื่อ",
- "LineageOS": "LineageOS"
+ "LineageOS": "LineageOS",
+ "Enable IMAP to receive mail for %s": "เปิดใช้งาน IMAP เพื่อรับจดหมายสำหรับ %s",
+ "Please edit your alias and enable IMAP to receive mail for %s.": "โปรด แก้ไขนามแฝงของคุณ และเปิดใช้งาน IMAP เพื่อรับจดหมายสำหรับ %s"
}
\ No newline at end of file
diff --git a/locales/tr.json b/locales/tr.json
index f0b608e5bc..590db26282 100644
--- a/locales/tr.json
+++ b/locales/tr.json
@@ -7402,5 +7402,7 @@
": This will override the existing alias password and database completely, and will permanently delete the existing IMAP storage and reset the alias' SQLite email database completely. Please make a backup if possible if you have an existing mailbox attached to this alias.": ": Bu, mevcut takma ad şifresini ve veritabanını tamamen geçersiz kılacak ve mevcut IMAP depolama alanını kalıcı olarak silecek ve takma adın SQLite e-posta veritabanını tamamen sıfırlayacaktır. Bu takma isme eklenmiş mevcut bir posta kutunuz varsa lütfen mümkünse bir yedekleme yapın.",
"We accept Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, SEPA Direct Debit, Canadian pre-authorized debits, and ACH Direct Debit.": "Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay'i kabul ediyoruz / Clearpay, Klarna, SEPA Otomatik Ödeme, Kanada ön provizyonlu ödemeleri ve ACH Otomatik Ödeme.",
"500,000+ custom domain names": "500.000'den fazla özel alan adı",
- "LineageOS": "LineageOS"
+ "LineageOS": "LineageOS",
+ "Enable IMAP to receive mail for %s": "%s için posta almak üzere IMAP'yi etkinleştirin",
+ "Please edit your alias and enable IMAP to receive mail for %s.": "Lütfen takma adınızı düzenleyin ve %s için posta almak üzere IMAP'yi etkinleştirin."
}
\ No newline at end of file
diff --git a/locales/uk.json b/locales/uk.json
index 823a97ceab..af39a4addb 100644
--- a/locales/uk.json
+++ b/locales/uk.json
@@ -7402,5 +7402,7 @@
": This will override the existing alias password and database completely, and will permanently delete the existing IMAP storage and reset the alias' SQLite email database completely. Please make a backup if possible if you have an existing mailbox attached to this alias.": ": це повністю замінить існуючий пароль псевдоніма та базу даних, назавжди видалить наявне сховище IMAP і повністю скине псевдонім бази даних електронної пошти SQLite. Будь ласка, зробіть резервну копію, якщо це можливо, якщо у вас є існуюча поштова скринька, прикріплена до цього псевдоніма.",
"We accept Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, SEPA Direct Debit, Canadian pre-authorized debits, and ACH Direct Debit.": "Ми приймаємо Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, прямий дебет SEPA, попередньо авторизоване дебетування в Канаді та прямий дебет ACH.",
"500,000+ custom domain names": "Більше 500 000 доменних імен",
- "LineageOS": "LineageOS"
+ "LineageOS": "LineageOS",
+ "Enable IMAP to receive mail for %s": "Увімкніть IMAP, щоб отримувати пошту для %s",
+ "Please edit your alias and enable IMAP to receive mail for %s.": "Відредагуйте свій псевдонім і ввімкніть IMAP, щоб отримувати пошту для %s ."
}
\ No newline at end of file
diff --git a/locales/vi.json b/locales/vi.json
index a3a2abb091..955a96902c 100644
--- a/locales/vi.json
+++ b/locales/vi.json
@@ -4911,5 +4911,7 @@
"Need secure and private email?": "Cần email an toàn và riêng tư?",
"LineageOS": "LineageOS",
"Storage used by this alias": "Bộ nhớ được sử dụng bởi bí danh này",
- "alias": "bí danh"
+ "alias": "bí danh",
+ "Enable IMAP to receive mail for %s": "Bật IMAP để nhận thư cho %s",
+ "Please edit your alias and enable IMAP to receive mail for %s.": "Vui lòng chỉnh sửa bí danh của bạn và bật IMAP để nhận thư cho %s ."
}
\ No newline at end of file
diff --git a/locales/zh.json b/locales/zh.json
index 8f1dbc1170..bea0c61f99 100644
--- a/locales/zh.json
+++ b/locales/zh.json
@@ -7095,5 +7095,7 @@
": This will override the existing alias password and database completely, and will permanently delete the existing IMAP storage and reset the alias' SQLite email database completely. Please make a backup if possible if you have an existing mailbox attached to this alias.": ":这将完全覆盖现有别名的密码和数据库,并将永久删除现有 IMAP 存储并完全重置别名的 SQLite 电子邮件数据库。如果您有与此别名关联的现有邮箱,请尽可能进行备份。",
"We accept Visa, Mastercard, American Express, Discover, Diners Club, JCB, China UnionPay, Alipay, Apple Pay, Google Pay, Amazon Pay, Cash App, Link, Bancontact, EPS, giropay, iDEAL, Przelewy24, Sofort, Affirm, Afterpay / Clearpay, Klarna, SEPA Direct Debit, Canadian pre-authorized debits, and ACH Direct Debit.": "我们接受 Visa、万事达卡、美国运通卡、Discover、大来卡、JCB、中国银联、支付宝、Apple Pay、Google Pay、亚马逊支付、Cash App、Link、Bancontact、EPS、giropay、iDEAL、Przelewy24、Sofort、Affirm、Afterpay / Clearpay、Klarna、SEPA 直接借记、加拿大预授权借记和 ACH 直接借记。",
"500,000+ custom domain names": "500,000+ 个自定义域名",
- "LineageOS": "LineageOS"
+ "LineageOS": "LineageOS",
+ "Enable IMAP to receive mail for %s": "启用 IMAP 来接收%s的邮件",
+ "Please edit your alias and enable IMAP to receive mail for %s.": "请编辑您的别名并启用 IMAP 来接收%s的邮件。"
}
\ No newline at end of file
diff --git a/package.json b/package.json
index 5ceab011f2..6bdee78623 100644
--- a/package.json
+++ b/package.json
@@ -51,6 +51,7 @@
"@mermaid-js/mermaid-cli": "10.9.1",
"@octokit/core": "4.2.4",
"@openpgp/wkd-client": "0.0.4",
+ "@opensumi/reconnecting-websocket": "4.4.0",
"@sidoshi/random-string": "1.0.0",
"@tkrotoff/bootstrap-floating-label": "0.8",
"@ungap/structured-clone": "1.2.0",
@@ -228,7 +229,6 @@
"qrcode": "1.5.3",
"qs": "6.12.1",
"re2": "1.21.3",
- "reconnecting-websocket": "4.4.0",
"regex-parser": "2.3.0",
"reserved-email-addresses-list": "2.0.13",
"rev-hash": "3",
diff --git a/test.js b/test.js
index 0a12d943db..f5b95b3e39 100644
--- a/test.js
+++ b/test.js
@@ -206,6 +206,7 @@ const { encrypt } = require('#helpers/encrypt-decrypt');
console.log('message', message);
console.timeEnd('read and write to database');
+ db.pragma('analysis_limit=400');
db.pragma('optimize');
db.close();
diff --git a/test/pop3/index.js b/test/pop3/index.js
index b0127194d2..588a8fef54 100644
--- a/test/pop3/index.js
+++ b/test/pop3/index.js
@@ -382,4 +382,6 @@ ZXhhbXBsZQo=
t.log('dele1', dele1);
t.is(dele1, 'message 1 deleted');
+
+ await t.context.pop3Command.command('QUIT');
});