diff --git a/app/controllers/web/my-account/index.js b/app/controllers/web/my-account/index.js index bb43eed16f..3ce428377f 100644 --- a/app/controllers/web/my-account/index.js +++ b/app/controllers/web/my-account/index.js @@ -64,6 +64,7 @@ const changeModulusLength = require('./change-modulus-length'); const checkVerifiedEmail = require('./check-verified-email'); const updateAllowlistAndDenylist = require('./update-allowlist-and-denylist'); const updateTimezone = require('./update-timezone'); +const uploadAliasMbox = require('./upload-alias-mbox'); module.exports = { cancelEmailChange, @@ -126,5 +127,6 @@ module.exports = { changeModulusLength, checkVerifiedEmail, updateAllowlistAndDenylist, - updateTimezone + updateTimezone, + uploadAliasMbox }; diff --git a/app/controllers/web/my-account/upload-alias-mbox.js b/app/controllers/web/my-account/upload-alias-mbox.js new file mode 100644 index 0000000000..338b801a1f --- /dev/null +++ b/app/controllers/web/my-account/upload-alias-mbox.js @@ -0,0 +1,36 @@ +/** + * Copyright (c) Forward Email LLC + * SPDX-License-Identifier: BUSL-1.1 + */ + +async function uploadAliasMbox() { + throw new Error('Coming soon'); + /* + const redirectTo = ctx.state.l( + `/my-account/domains/${ctx.state.domain.name}/aliases` + ); + try { + // ensure the size is not more than 2 GB + // store the file to the server + + // + // copy the file to the sqlite server in the background + // once it's done then fire a websocket request to parse payload + // which will then kick off the mbox import and email the user once done + // + + } catch (err) { + if (err && err.isBoom) throw err; + if (isErrorConstructorName(err, 'ValidationError')) throw err; + ctx.logger.fatal(err); + ctx.flash('error', ctx.translate('UNKNOWN_ERROR')); + const redirectTo = ctx.state.l( + `/my-account/domains/${ctx.state.domain.name}/aliases` + ); + if (ctx.accepts('html')) ctx.redirect(redirectTo); + else ctx.body = { redirectTo }; + } + */ +} + +module.exports = uploadAliasMbox; diff --git a/app/models/emails.js b/app/models/emails.js index fe0b2ff0ce..a53a97fc95 100644 --- a/app/models/emails.js +++ b/app/models/emails.js @@ -224,6 +224,7 @@ const Emails = new mongoose.Schema( rejectedErrors: [mongoose.Schema.Types.Mixed] }, { + versionKey: false, writeConcern: { w: 'majority' } diff --git a/app/models/users.js b/app/models/users.js index 426ba666a2..2601dc670e 100644 --- a/app/models/users.js +++ b/app/models/users.js @@ -139,7 +139,8 @@ const Users = new mongoose.Schema({ unique: true, validate: (value) => validator.isEmail(value) }, - scheduled_send_sent_at: Date + scheduled_send_sent_at: Date, + smtp_rate_limit_sent_at: Date }); // Additional variable based properties to add to the schema diff --git a/config/index.js b/config/index.js index ddc874ec68..5f9032208a 100644 --- a/config/index.js +++ b/config/index.js @@ -188,8 +188,8 @@ const config = { }, defaultModulusLength: 1024, defaultStoragePath: env.SQLITE_STORAGE_PATH, - // 60 items (50 MB * 60 = 3000 MB = 3 GB) - smtpMaxQueue: 60, + // 100 items (50 MB * 100 = 5000 MB = 5 GB) + smtpMaxQueue: 100, smtpQueueTimeout: ms('180s'), smtpLimitMessages: env.NODE_ENV === 'test' ? 10 : 300, smtpLimitAuth: env.NODE_ENV === 'test' ? Number.MAX_VALUE : 10, diff --git a/config/phrases.js b/config/phrases.js index 26cfa7a8e4..58fefabd66 100644 --- a/config/phrases.js +++ b/config/phrases.js @@ -20,6 +20,8 @@ for (const key of Object.keys(statuses.message)) { } module.exports = { + SMTP_RATE_LIMIT_EXCEEDED: + 'You have exceeded your daily SMTP outbound rate limit.', UBUNTU_NOT_ALLOWED_EMAIL: 'You cannot use that email address as a forwarding recipient.', UBUNTU_PERMISSIONS: diff --git a/helpers/smtp/on-connect.js b/helpers/smtp/on-connect.js index b8e456d434..04a31e8ce0 100644 --- a/helpers/smtp/on-connect.js +++ b/helpers/smtp/on-connect.js @@ -8,9 +8,9 @@ const punycode = require('node:punycode'); const isFQDN = require('is-fqdn'); const { boolean } = require('boolean'); -const SMTPError = require('#helpers/smtp-error'); +// const SMTPError = require('#helpers/smtp-error'); const ServerShutdownError = require('#helpers/server-shutdown-error'); -const config = require('#config'); +// const config = require('#config'); const env = require('#config/env'); const parseRootDomain = require('#helpers/parse-root-domain'); const refineAndLogError = require('#helpers/refine-and-log-error'); @@ -59,6 +59,10 @@ async function onConnect(session, fn) { if (boolean(result)) { session.allowlistValue = rootDomain || session.remoteAddress; } else { + // + // NOTE: because there are too many false positives with actual users + // we're not going to do denylist/silent/backscatter lookup anymore + /* // // prevent connections from backscatter, silent ban, and denylist // @@ -89,6 +93,7 @@ async function onConnect(session, fn) { { ignoreHook: true } ); } + */ } fn(); diff --git a/helpers/smtp/on-data.js b/helpers/smtp/on-data.js index 99e96c91bd..3d0f9db9c9 100644 --- a/helpers/smtp/on-data.js +++ b/helpers/smtp/on-data.js @@ -5,6 +5,7 @@ const _ = require('lodash'); const bytes = require('bytes'); +const dayjs = require('dayjs-with-plugins'); const getStream = require('get-stream'); const mongoose = require('mongoose'); const safeStringify = require('fast-safe-stringify'); @@ -18,16 +19,56 @@ const ServerShutdownError = require('#helpers/server-shutdown-error'); const Users = require('#models/users'); const config = require('#config'); const createSession = require('#helpers/create-session'); +const emailHelper = require('#helpers/email'); const env = require('#config/env'); +const i18n = require('#helpers/i18n'); +const isValidPassword = require('#helpers/is-valid-password'); const logger = require('#helpers/logger'); const refineAndLogError = require('#helpers/refine-and-log-error'); const validateAlias = require('#helpers/validate-alias'); const validateDomain = require('#helpers/validate-domain'); -const isValidPassword = require('#helpers/is-valid-password'); const { decrypt } = require('#helpers/encrypt-decrypt'); const MAX_BYTES = bytes(env.SMTP_MESSAGE_MAX_SIZE); +async function sendRateLimitEmail(user) { + // if the user received rate limit email in past 30d + if ( + _.isDate(user.smtp_rate_limit_sent_at) && + dayjs().isBefore(dayjs(user.smtp_rate_limit_sent_at).add(30, 'days')) + ) { + logger.info('user was already rate limited'); + return; + } + + await emailHelper({ + template: 'alert', + message: { + to: user[config.userFields.fullEmail], + bcc: config.email.message.from, + locale: user[config.lastLocaleField], + subject: i18n.translate( + 'SMTP_RATE_LIMIT_EXCEEDED', + user[config.lastLocaleField] + ) + }, + locals: { + locale: user[config.lastLocaleField], + message: i18n.translate( + 'SMTP_RATE_LIMIT_EXCEEDED', + user[config.lastLocaleField] + ) + } + }); + + // otherwise send the user an email and update the user record + await Users.findByIdAndUpdate(user._id, { + $set: { + smtp_rate_limit_sent_at: new Date() + } + }); +} + // // NOTE: we can merge SMTP/FE codebase in future and simply check if auth disabled // then this will act as a forwarding server only (MTA) @@ -80,7 +121,7 @@ async function onData(stream, _session, fn) { }) .populate( 'user', - `id ${config.userFields.isBanned} ${config.userFields.smtpLimit}` + `id ${config.userFields.isBanned} ${config.userFields.smtpLimit} smtp_rate_limit_sent_at ${config.userFields.fullEmail} ${config.lastLocaleField}` ) .select('+tokens.hash +tokens.salt') .lean() @@ -106,7 +147,7 @@ async function onData(stream, _session, fn) { }) .populate( 'members.user', - `id plan email ${config.userFields.isBanned} ${config.userFields.hasVerifiedEmail} ${config.userFields.planExpiresAt} ${config.userFields.smtpLimit} ${config.userFields.stripeSubscriptionID} ${config.userFields.paypalSubscriptionID}` + `id plan email ${config.userFields.isBanned} ${config.userFields.hasVerifiedEmail} ${config.userFields.planExpiresAt} ${config.userFields.smtpLimit} ${config.userFields.stripeSubscriptionID} ${config.userFields.paypalSubscriptionID} smtp_rate_limit_sent_at ${config.userFields.fullEmail} ${config.lastLocaleField}` ) .select('+tokens +tokens.hash +tokens.salt') .exec(); @@ -342,8 +383,13 @@ async function onData(stream, _session, fn) { `${config.smtpLimitNamespace}:${domain.id}` ); // return 550 error code - if (count >= max) + if (count >= max) { + // send one-time email alert to admin + user + sendRateLimitEmail(user) + .then() + .catch((err) => logger.fatal(err, { session })); throw new SMTPError('Rate limit exceeded', { ignoreHook: true }); + } } // rate limit to X emails per day by alias user id then denylist @@ -352,8 +398,13 @@ async function onData(stream, _session, fn) { `${config.smtpLimitNamespace}:${user.id}` ); // return 550 error code - if (count >= max) + if (count >= max) { + // send one-time email alert to admin + user + sendRateLimitEmail(user) + .then() + .catch((err) => logger.fatal(err, { session })); throw new SMTPError('Rate limit exceeded', { ignoreHook: true }); + } } } diff --git a/jobs/check-smtp.js b/jobs/check-smtp.js index c263e4563e..53ff16a91d 100644 --- a/jobs/check-smtp.js +++ b/jobs/check-smtp.js @@ -60,8 +60,7 @@ const DNS_RETRY_CODES = new Set([ // 'ENODATA', 'ENOMEM', 'ENONAME', - // NOTE: ENOTFOUND indicates the domain doesn't exist - // (and we don't want to send emails to people that didn't even register it yet) + // NOTE: ENOTFOUND indicates the record doesn't exist 'ENOTFOUND', 'ENOTIMP', 'ENOTINITIALIZED', diff --git a/jobs/send-emails.js b/jobs/send-emails.js index b7c36e7e9c..4b7940f7ae 100644 --- a/jobs/send-emails.js +++ b/jobs/send-emails.js @@ -44,7 +44,8 @@ const graceful = new Graceful({ }); const queue = new PQueue({ - concurrency: config.concurrency * 16 + concurrency: Math.round(config.smtpMaxQueue / 2) + // concurrency: config.concurrency * 30 // timeout: config.smtpQueueTimeout }); @@ -74,12 +75,12 @@ async function sendEmails() { if (queue.size >= config.smtpMaxQueue) { logger.info(`queue has more than ${config.smtpMaxQueue} tasks`); - // wait 1 second - await delay(1000); - // queue more messages once finished processing - return sendEmails(); + await delay(5000); + return; } + // TODO: capacity/recipient issues should be hard 550 bounce for outbound + const now = new Date(); const limit = config.smtpMaxQueue - queue.size; @@ -199,43 +200,59 @@ async function sendEmails() { // return early if the job was already cancelled if (isCancelled) break; // TODO: implement queue on a per-target/provider basis (e.g. 10 at once to Cox addresses) - queue.add(() => processEmail({ email, resolver, client }), { - // TODO: if the email was admin owned domain then priority higher (see email pre-save hook) - // priority: email.priority || 0 - }); + queue.add( + async () => { + try { + await processEmail({ email, resolver, client }); + } catch (err) { + logger.error(err, { email }); + } + }, + { + // TODO: if the email was admin owned domain then priority higher (see email pre-save hook) + // priority: email.priority || 0 + } + ); } - // wait 1 second - await delay(1000); - - // queue more messages once finished processing - return sendEmails(); + await delay(5000); } (async () => { await setupMongoose(logger); - try { - await sendEmails(); - } catch (err) { - await logger.error(err); + (async function startRecursion() { + if (isCancelled) { + if (parentPort) parentPort.postMessage('done'); + else process.exit(0); + return; + } - await emailHelper({ - template: 'alert', - message: { - to: config.email.message.from, - subject: 'Send emails had an error' - }, - locals: { - message: `
${JSON.stringify(
-          parseErr(err),
-          null,
-          2
-        )}
` - } - }); + try { + await sendEmails(); + } catch (err) { + logger.error(err); + + emailHelper({ + template: 'alert', + message: { + to: config.email.message.from, + subject: 'Send emails had an error' + }, + locals: { + message: `
${JSON.stringify(
+            parseErr(err),
+            null,
+            2
+          )}
` + } + }) + .then() + .catch((err) => logger.fatal(err)); + + await delay(5000); + } - if (parentPort) parentPort.postMessage('done'); - else process.exit(0); - } + startRecursion(); + })(); })(); diff --git a/jobs/unlock-emails.js b/jobs/unlock-emails.js index 9e6bfe01ab..9b974b33ca 100644 --- a/jobs/unlock-emails.js +++ b/jobs/unlock-emails.js @@ -15,7 +15,9 @@ require('#config/mongoose'); const Graceful = require('@ladjs/graceful'); const dayjs = require('dayjs-with-plugins'); const mongoose = require('mongoose'); +const _ = require('lodash'); +const Domains = require('#models/domains'); const Emails = require('#models/emails'); const logger = require('#helpers/logger'); const setupMongoose = require('#helpers/setup-mongoose'); @@ -61,6 +63,42 @@ graceful.listen(); await logger.error(err); } + // + // go through all pending emails and check if they belong back in queue + // (or if they need deleted because the domain doesn't exist anymore) + // + try { + for await (const email of Emails.find({ + status: 'pending' + }) + .cursor() + .addCursorFlag('noCursorTimeout', true)) { + // delete emails that don't have domains that correspond to them + const domain = await Domains.findById(email.domain).lean().exec(); + if (!domain) { + await Emails.findByIdAndRemove(email._id); + continue; + } + + // otherwise check if `smtp_suspended_sent_at` does not exist + if (!_.isDate(domain.smtp_suspended_sent_at)) { + // TODO: check if we're rate limited, if so then keep it pending + await Emails.findByIdAndUpdate(email._id, { + $set: { + is_locked: false, + status: 'queued' + }, + $unset: { + locked_by: 1, + locked_at: 1 + } + }); + } + } + } catch (err) { + await logger.error(err); + } + if (parentPort) parentPort.postMessage('done'); else process.exit(0); })(); diff --git a/locales/ar.json b/locales/ar.json index 6333939d1c..738e50db67 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -7364,5 +7364,6 @@ "You can only read or manage your existing aliases, and do not have permission to create new ones.": "يمكنك فقط قراءة الأسماء المستعارة الحالية أو إدارتها، وليس لديك إذن بإنشاء أسماء جديدة.", "You cannot have more than 3 aliases for this domain.": "لا يمكن أن يكون لديك أكثر من 3 أسماء مستعارة لهذا المجال.", "Launchpad username was missing or not detected, please try again later.": "اسم مستخدم Launchpad مفقود أو لم يتم اكتشافه، يرجى المحاولة مرة أخرى لاحقًا.", - "Invalid response from Launchpad API, please try again later.": "استجابة غير صالحة من Launchpad API، يرجى المحاولة مرة أخرى لاحقًا." + "Invalid response from Launchpad API, please try again later.": "استجابة غير صالحة من Launchpad API، يرجى المحاولة مرة أخرى لاحقًا.", + "You have exceeded your daily SMTP outbound rate limit.": "لقد تجاوزت حد معدل الصادر اليومي لـ SMTP." } \ No newline at end of file diff --git a/locales/cs.json b/locales/cs.json index b504b99a4d..44035c6822 100644 --- a/locales/cs.json +++ b/locales/cs.json @@ -7364,5 +7364,6 @@ "You can only read or manage your existing aliases, and do not have permission to create new ones.": "Můžete pouze číst nebo spravovat své stávající aliasy a nemáte oprávnění vytvářet nové.", "You cannot have more than 3 aliases for this domain.": "Pro tuto doménu nemůžete mít více než 3 aliasy.", "Launchpad username was missing or not detected, please try again later.": "Uživatelské jméno Launchpadu chybělo nebo nebylo zjištěno, zkuste to prosím znovu později.", - "Invalid response from Launchpad API, please try again later.": "Neplatná odpověď z Launchpad API, zkuste to znovu později." + "Invalid response from Launchpad API, please try again later.": "Neplatná odpověď z Launchpad API, zkuste to znovu později.", + "You have exceeded your daily SMTP outbound rate limit.": "Překročili jste svůj denní limit odchozí rychlosti SMTP." } \ No newline at end of file diff --git a/locales/da.json b/locales/da.json index fffe9ee0a7..c471c632fc 100644 --- a/locales/da.json +++ b/locales/da.json @@ -3790,5 +3790,20 @@ "You can only read or manage your existing aliases, and do not have permission to create new ones.": "Du kan kun læse eller administrere dine eksisterende aliaser og har ikke tilladelse til at oprette nye.", "You cannot have more than 3 aliases for this domain.": "Du kan ikke have mere end 3 aliaser for dette domæne.", "Launchpad username was missing or not detected, please try again later.": "Launchpad-brugernavnet manglede eller blev ikke fundet, prøv venligst igen senere.", - "Invalid response from Launchpad API, please try again later.": "Ugyldigt svar fra Launchpad API. Prøv venligst igen senere." + "Invalid response from Launchpad API, please try again later.": "Ugyldigt svar fra Launchpad API. Prøv venligst igen senere.", + "Admin - Emails": "Admin - E-mails", + "Search for emails": "Søg efter e-mails", + "See mongodb-query-parser for more insight.": "Se mongodb-query-parser for mere indsigt.", + "Basic search": "Grundlæggende søgning", + "This splits by space and requires an equals sign for values. You can use quotes and also escape quotes in values. Numbers and Booleans are parsed too.": "Dette opdeles efter mellemrum og kræver et lighedstegn for værdier. Du kan bruge citater og også undslippe citater i værdier. Tal og booleaner analyseres også.", + "ID": "ID", + "Updated": "Opdateret", + "Status": "Status", + "Subject": "Emne", + "No emails exist for that search.": "Der findes ingen e-mails til den søgning.", + "You have used %d out of your daily limit of %d outbound SMTP messages.": "Du har brugt %d ud af din daglige grænse på %d udgående SMTP-meddelelser.", + "Outbound SMTP emails are shown below – click here to setup your email client to receive email.": "Udgående SMTP-e-mails er vist nedenfor – klik her for at konfigurere din e-mail-klient til at modtage e-mail.", + "Email": "E-mail", + "No emails have been stored yet. Please check back later.": "Ingen e-mails er blevet gemt endnu. Kom venligst tilbage senere.", + "You have exceeded your daily SMTP outbound rate limit.": "Du har overskredet din daglige grænse for udgående SMTP-hastighed." } \ No newline at end of file diff --git a/locales/de.json b/locales/de.json index c204fe4805..63325e85eb 100644 --- a/locales/de.json +++ b/locales/de.json @@ -6402,5 +6402,6 @@ "You can only read or manage your existing aliases, and do not have permission to create new ones.": "Sie können Ihre vorhandenen Aliase nur lesen oder verwalten und sind nicht berechtigt, neue zu erstellen.", "You cannot have more than 3 aliases for this domain.": "Sie können nicht mehr als 3 Aliase für diese Domäne haben.", "Launchpad username was missing or not detected, please try again later.": "Der Launchpad-Benutzername fehlte oder wurde nicht erkannt. Bitte versuchen Sie es später erneut.", - "Invalid response from Launchpad API, please try again later.": "Ungültige Antwort von der Launchpad-API. Bitte versuchen Sie es später erneut." + "Invalid response from Launchpad API, please try again later.": "Ungültige Antwort von der Launchpad-API. Bitte versuchen Sie es später erneut.", + "You have exceeded your daily SMTP outbound rate limit.": "Sie haben Ihr tägliches SMTP-Ausgangsratenlimit überschritten." } \ No newline at end of file diff --git a/locales/en.json b/locales/en.json index 81eeb03e7e..cf89468de0 100644 --- a/locales/en.json +++ b/locales/en.json @@ -7126,5 +7126,6 @@ "What is your Ubuntu One login?": "What is your Ubuntu One login?", "You must be a member of a specific Launchpad group to get access. Supported groups include ~ubuntumembers, ~kubuntu-members, ~lubuntu-members, ~edubuntu-members, and ~ubuntustudio-core.": "You must be a member of a specific Launchpad group to get access. Supported groups include ~ubuntumembers, ~kubuntu-members, ~lubuntu-members, ~edubuntu-members, and ~ubuntustudio-core.", "Log in with Ubuntu One": "Log in with Ubuntu One", - "470,000+ custom domain names": "470,000+ custom domain names" + "470,000+ custom domain names": "470,000+ custom domain names", + "You have exceeded your daily SMTP outbound rate limit.": "You have exceeded your daily SMTP outbound rate limit." } \ No newline at end of file diff --git a/locales/es.json b/locales/es.json index bea714f6ba..d2c97c8970 100644 --- a/locales/es.json +++ b/locales/es.json @@ -7362,5 +7362,6 @@ "You can only read or manage your existing aliases, and do not have permission to create new ones.": "Solo puede leer o administrar sus alias existentes y no tiene permiso para crear nuevos.", "You cannot have more than 3 aliases for this domain.": "No puedes tener más de 3 alias para este dominio.", "Launchpad username was missing or not detected, please try again later.": "Faltaba el nombre de usuario de Launchpad o no se detectó. Vuelva a intentarlo más tarde.", - "Invalid response from Launchpad API, please try again later.": "Respuesta no válida de la API de Launchpad. Vuelva a intentarlo más tarde." + "Invalid response from Launchpad API, please try again later.": "Respuesta no válida de la API de Launchpad. Vuelva a intentarlo más tarde.", + "You have exceeded your daily SMTP outbound rate limit.": "Ha excedido su límite de tasa de salida SMTP diaria." } \ No newline at end of file diff --git a/locales/fi.json b/locales/fi.json index 1d28d291ac..b7fd4a0658 100644 --- a/locales/fi.json +++ b/locales/fi.json @@ -7211,5 +7211,6 @@ "You can only read or manage your existing aliases, and do not have permission to create new ones.": "Voit vain lukea tai hallita olemassa olevia aliaksiasi, eikä sinulla ole lupaa luoda uusia.", "You cannot have more than 3 aliases for this domain.": "Sinulla voi olla enintään 3 aliasta tälle verkkotunnukselle.", "Launchpad username was missing or not detected, please try again later.": "Launchpad-käyttäjänimi puuttui tai sitä ei havaittu. Yritä myöhemmin uudelleen.", - "Invalid response from Launchpad API, please try again later.": "Virheellinen vastaus Launchpad API:lta, yritä myöhemmin uudelleen." + "Invalid response from Launchpad API, please try again later.": "Virheellinen vastaus Launchpad API:lta, yritä myöhemmin uudelleen.", + "You have exceeded your daily SMTP outbound rate limit.": "Olet ylittänyt päivittäisen lähtevän SMTP-nopeusrajan." } \ No newline at end of file diff --git a/locales/fr.json b/locales/fr.json index 76b5b82d92..a19968fdaf 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -4837,5 +4837,6 @@ "You can only read or manage your existing aliases, and do not have permission to create new ones.": "Vous pouvez uniquement lire ou gérer vos alias existants et n'êtes pas autorisé à en créer de nouveaux.", "You cannot have more than 3 aliases for this domain.": "Vous ne pouvez pas avoir plus de 3 alias pour ce domaine.", "Launchpad username was missing or not detected, please try again later.": "Le nom d'utilisateur du Launchpad était manquant ou n'était pas détecté, veuillez réessayer plus tard.", - "Invalid response from Launchpad API, please try again later.": "Réponse invalide de l'API Launchpad. Veuillez réessayer plus tard." + "Invalid response from Launchpad API, please try again later.": "Réponse invalide de l'API Launchpad. Veuillez réessayer plus tard.", + "You have exceeded your daily SMTP outbound rate limit.": "Vous avez dépassé votre limite quotidienne de débit sortant SMTP." } \ No newline at end of file diff --git a/locales/he.json b/locales/he.json index d618def480..a54da85ffc 100644 --- a/locales/he.json +++ b/locales/he.json @@ -5337,5 +5337,6 @@ "You can only read or manage your existing aliases, and do not have permission to create new ones.": "אתה יכול רק לקרוא או לנהל את הכינויים הקיימים שלך, ואין לך הרשאה ליצור חדשים.", "You cannot have more than 3 aliases for this domain.": "אתה לא יכול לקבל יותר מ-3 כינויים עבור הדומיין הזה.", "Launchpad username was missing or not detected, please try again later.": "שם המשתמש של Launchpad היה חסר או לא זוהה, אנא נסה שוב מאוחר יותר.", - "Invalid response from Launchpad API, please try again later.": "תגובה לא חוקית מממשק ה-API של Launchpad, אנא נסה שוב מאוחר יותר." + "Invalid response from Launchpad API, please try again later.": "תגובה לא חוקית מממשק ה-API של Launchpad, אנא נסה שוב מאוחר יותר.", + "You have exceeded your daily SMTP outbound rate limit.": "חרגת ממגבלת תעריף היציאה היומי של SMTP." } \ No newline at end of file diff --git a/locales/hu.json b/locales/hu.json index 33c0e549d2..0a4a902a01 100644 --- a/locales/hu.json +++ b/locales/hu.json @@ -7364,5 +7364,6 @@ "You can only read or manage your existing aliases, and do not have permission to create new ones.": "Csak a meglévő álneveit olvashatja vagy kezelheti, újak létrehozására nincs engedélye.", "You cannot have more than 3 aliases for this domain.": "Ehhez a domainhez legfeljebb 3 alias lehet.", "Launchpad username was missing or not detected, please try again later.": "Az indítópult-felhasználónév hiányzik vagy nem észlelhető. Kérjük, próbálja újra később.", - "Invalid response from Launchpad API, please try again later.": "Érvénytelen válasz a Launchpad API-tól, próbálkozzon újra később." + "Invalid response from Launchpad API, please try again later.": "Érvénytelen válasz a Launchpad API-tól, próbálkozzon újra később.", + "You have exceeded your daily SMTP outbound rate limit.": "Túllépte a napi SMTP kimenő sebességkorlátot." } \ No newline at end of file diff --git a/locales/id.json b/locales/id.json index 1bd0aa9c78..1558001c87 100644 --- a/locales/id.json +++ b/locales/id.json @@ -7364,5 +7364,6 @@ "You can only read or manage your existing aliases, and do not have permission to create new ones.": "Anda hanya dapat membaca atau mengelola alias yang sudah ada, dan tidak memiliki izin untuk membuat alias baru.", "You cannot have more than 3 aliases for this domain.": "Anda tidak boleh memiliki lebih dari 3 alias untuk domain ini.", "Launchpad username was missing or not detected, please try again later.": "Nama pengguna Launchpad hilang atau tidak terdeteksi, silakan coba lagi nanti.", - "Invalid response from Launchpad API, please try again later.": "Respons tidak valid dari Launchpad API, silakan coba lagi nanti." + "Invalid response from Launchpad API, please try again later.": "Respons tidak valid dari Launchpad API, silakan coba lagi nanti.", + "You have exceeded your daily SMTP outbound rate limit.": "Anda telah melampaui batas kecepatan keluar SMTP harian." } \ No newline at end of file diff --git a/locales/it.json b/locales/it.json index c19a2556ed..3af951e2f8 100644 --- a/locales/it.json +++ b/locales/it.json @@ -7364,5 +7364,6 @@ "You can only read or manage your existing aliases, and do not have permission to create new ones.": "Puoi solo leggere o gestire i tuoi alias esistenti e non hai il permesso di crearne di nuovi.", "You cannot have more than 3 aliases for this domain.": "Non puoi avere più di 3 alias per questo dominio.", "Launchpad username was missing or not detected, please try again later.": "Il nome utente del Launchpad mancava o non veniva rilevato, riprova più tardi.", - "Invalid response from Launchpad API, please try again later.": "Risposta non valida dall'API Launchpad, riprova più tardi." + "Invalid response from Launchpad API, please try again later.": "Risposta non valida dall'API Launchpad, riprova più tardi.", + "You have exceeded your daily SMTP outbound rate limit.": "Hai superato il limite di velocità in uscita SMTP giornaliero." } \ No newline at end of file diff --git a/locales/ja.json b/locales/ja.json index deb023f415..8edfeb4d43 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -7364,5 +7364,6 @@ "You can only read or manage your existing aliases, and do not have permission to create new ones.": "既存のエイリアスを読み取ったり管理したりすることしかできず、新しいエイリアスを作成する権限はありません。", "You cannot have more than 3 aliases for this domain.": "このドメインには 3 つ以上のエイリアスを設定することはできません。", "Launchpad username was missing or not detected, please try again later.": "Launchpad のユーザー名が見つからないか、検出されませんでした。後でもう一度お試しください。", - "Invalid response from Launchpad API, please try again later.": "Launchpad API からの応答が無効です。後でもう一度お試しください。" + "Invalid response from Launchpad API, please try again later.": "Launchpad API からの応答が無効です。後でもう一度お試しください。", + "You have exceeded your daily SMTP outbound rate limit.": "1 日の SMTP 送信レート制限を超えました。" } \ No newline at end of file diff --git a/locales/ko.json b/locales/ko.json index d35f4be50c..65d3460dda 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -7364,5 +7364,6 @@ "You can only read or manage your existing aliases, and do not have permission to create new ones.": "기존 별칭을 읽거나 관리할 수만 있고 새 별칭을 만들 수 있는 권한은 없습니다.", "You cannot have more than 3 aliases for this domain.": "이 도메인에는 3개 이상의 별칭을 사용할 수 없습니다.", "Launchpad username was missing or not detected, please try again later.": "Launchpad 사용자 이름이 누락되었거나 감지되지 않았습니다. 나중에 다시 시도해 주세요.", - "Invalid response from Launchpad API, please try again later.": "Launchpad API의 응답이 잘못되었습니다. 나중에 다시 시도해 주세요." + "Invalid response from Launchpad API, please try again later.": "Launchpad API의 응답이 잘못되었습니다. 나중에 다시 시도해 주세요.", + "You have exceeded your daily SMTP outbound rate limit.": "일일 SMTP 아웃바운드 속도 제한을 초과했습니다." } \ No newline at end of file diff --git a/locales/nl.json b/locales/nl.json index 5254a6532d..b7bb82e885 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -7364,5 +7364,6 @@ "You can only read or manage your existing aliases, and do not have permission to create new ones.": "U kunt alleen uw bestaande aliassen lezen of beheren, en u heeft geen toestemming om nieuwe aan te maken.", "You cannot have more than 3 aliases for this domain.": "U kunt niet meer dan drie aliassen hebben voor dit domein.", "Launchpad username was missing or not detected, please try again later.": "De Launchpad-gebruikersnaam ontbreekt of is niet gedetecteerd. Probeer het later opnieuw.", - "Invalid response from Launchpad API, please try again later.": "Ongeldig antwoord van Launchpad API. Probeer het later opnieuw." + "Invalid response from Launchpad API, please try again later.": "Ongeldig antwoord van Launchpad API. Probeer het later opnieuw.", + "You have exceeded your daily SMTP outbound rate limit.": "U heeft uw dagelijkse uitgaande SMTP-limiet overschreden." } \ No newline at end of file diff --git a/locales/no.json b/locales/no.json index a69d1c50fa..5deefb4bac 100644 --- a/locales/no.json +++ b/locales/no.json @@ -7369,5 +7369,6 @@ "You can only read or manage your existing aliases, and do not have permission to create new ones.": "Du kan bare lese eller administrere dine eksisterende aliaser, og du har ikke tillatelse til å opprette nye.", "You cannot have more than 3 aliases for this domain.": "Du kan ikke ha mer enn 3 aliaser for dette domenet.", "Launchpad username was missing or not detected, please try again later.": "Launchpad-brukernavnet manglet eller ble ikke oppdaget. Prøv igjen senere.", - "Invalid response from Launchpad API, please try again later.": "Ugyldig svar fra Launchpad API. Prøv igjen senere." + "Invalid response from Launchpad API, please try again later.": "Ugyldig svar fra Launchpad API. Prøv igjen senere.", + "You have exceeded your daily SMTP outbound rate limit.": "Du har overskredet den daglige grensen for utgående SMTP-hastighet." } \ No newline at end of file diff --git a/locales/pl.json b/locales/pl.json index 36563076ab..287a5e2772 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -7364,5 +7364,6 @@ "You can only read or manage your existing aliases, and do not have permission to create new ones.": "Możesz jedynie czytać istniejące aliasy lub zarządzać nimi i nie masz uprawnień do tworzenia nowych.", "You cannot have more than 3 aliases for this domain.": "Nie możesz mieć więcej niż 3 aliasów dla tej domeny.", "Launchpad username was missing or not detected, please try again later.": "Brakowało nazwy użytkownika Launchpada lub nie została wykryta. Spróbuj ponownie później.", - "Invalid response from Launchpad API, please try again later.": "Nieprawidłowa odpowiedź z API Launchpad. Spróbuj ponownie później." + "Invalid response from Launchpad API, please try again later.": "Nieprawidłowa odpowiedź z API Launchpad. Spróbuj ponownie później.", + "You have exceeded your daily SMTP outbound rate limit.": "Przekroczono dzienny limit szybkości ruchu wychodzącego SMTP." } \ No newline at end of file diff --git a/locales/pt.json b/locales/pt.json index 155156b039..c378443322 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -7364,5 +7364,6 @@ "You can only read or manage your existing aliases, and do not have permission to create new ones.": "Você só pode ler ou gerenciar seus aliases existentes e não tem permissão para criar novos.", "You cannot have more than 3 aliases for this domain.": "Você não pode ter mais de três aliases para este domínio.", "Launchpad username was missing or not detected, please try again later.": "O nome de usuário do Launchpad estava ausente ou não foi detectado. Tente novamente mais tarde.", - "Invalid response from Launchpad API, please try again later.": "Resposta inválida da API do Launchpad. Tente novamente mais tarde." + "Invalid response from Launchpad API, please try again later.": "Resposta inválida da API do Launchpad. Tente novamente mais tarde.", + "You have exceeded your daily SMTP outbound rate limit.": "Você excedeu seu limite diário de taxa de saída SMTP." } \ No newline at end of file diff --git a/locales/ru.json b/locales/ru.json index 6858207ebc..075c9ca893 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -7364,5 +7364,6 @@ "You can only read or manage your existing aliases, and do not have permission to create new ones.": "Вы можете только читать существующие псевдонимы или управлять ими и не имеете разрешения на создание новых.", "You cannot have more than 3 aliases for this domain.": "Для этого домена не может быть более трех псевдонимов.", "Launchpad username was missing or not detected, please try again later.": "Имя пользователя Launchpad отсутствует или не обнаружено. Повторите попытку позже.", - "Invalid response from Launchpad API, please try again later.": "Неверный ответ от API Launchpad. Повторите попытку позже." + "Invalid response from Launchpad API, please try again later.": "Неверный ответ от API Launchpad. Повторите попытку позже.", + "You have exceeded your daily SMTP outbound rate limit.": "Вы превысили дневной лимит исходящей скорости SMTP." } \ No newline at end of file diff --git a/locales/sv.json b/locales/sv.json index b85bfaff3b..739ccbd3b7 100644 --- a/locales/sv.json +++ b/locales/sv.json @@ -7364,5 +7364,6 @@ "You can only read or manage your existing aliases, and do not have permission to create new ones.": "Du kan bara läsa eller hantera dina befintliga alias och har inte behörighet att skapa nya.", "You cannot have more than 3 aliases for this domain.": "Du kan inte ha fler än 3 alias för den här domänen.", "Launchpad username was missing or not detected, please try again later.": "Launchpad-användarnamnet saknades eller upptäcktes inte, försök igen senare.", - "Invalid response from Launchpad API, please try again later.": "Ogiltigt svar från Launchpad API, försök igen senare." + "Invalid response from Launchpad API, please try again later.": "Ogiltigt svar från Launchpad API, försök igen senare.", + "You have exceeded your daily SMTP outbound rate limit.": "Du har överskridit din dagliga gräns för utgående SMTP-hastighet." } \ No newline at end of file diff --git a/locales/th.json b/locales/th.json index 9ede246fcd..156b28e719 100644 --- a/locales/th.json +++ b/locales/th.json @@ -7364,5 +7364,6 @@ "You can only read or manage your existing aliases, and do not have permission to create new ones.": "คุณสามารถอ่านหรือจัดการได้เฉพาะนามแฝงที่มีอยู่เท่านั้น และไม่มีสิทธิ์สร้างนามแฝงใหม่", "You cannot have more than 3 aliases for this domain.": "คุณไม่สามารถมีชื่อแทนได้มากกว่า 3 ชื่อสำหรับโดเมนนี้", "Launchpad username was missing or not detected, please try again later.": "ไม่พบชื่อผู้ใช้ Launchpad หรือตรวจไม่พบ โปรดลองอีกครั้งในภายหลัง", - "Invalid response from Launchpad API, please try again later.": "การตอบกลับจาก Launchpad API ไม่ถูกต้อง โปรดลองอีกครั้งในภายหลัง" + "Invalid response from Launchpad API, please try again later.": "การตอบกลับจาก Launchpad API ไม่ถูกต้อง โปรดลองอีกครั้งในภายหลัง", + "You have exceeded your daily SMTP outbound rate limit.": "คุณเกินขีดจำกัดอัตราขาออก SMTP รายวันของคุณแล้ว" } \ No newline at end of file diff --git a/locales/tr.json b/locales/tr.json index e7e3f904a1..1fcde5b3d7 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -7364,5 +7364,6 @@ "You can only read or manage your existing aliases, and do not have permission to create new ones.": "Yalnızca mevcut takma adlarınızı okuyabilir veya yönetebilirsiniz; yeni takma ad oluşturma izniniz yoktur.", "You cannot have more than 3 aliases for this domain.": "Bu alan adı için 3'ten fazla takma adınız olamaz.", "Launchpad username was missing or not detected, please try again later.": "Launchpad kullanıcı adı eksik veya algılanamadı, lütfen daha sonra tekrar deneyin.", - "Invalid response from Launchpad API, please try again later.": "Launchpad API'sinden geçersiz yanıt. Lütfen daha sonra tekrar deneyin." + "Invalid response from Launchpad API, please try again later.": "Launchpad API'sinden geçersiz yanıt. Lütfen daha sonra tekrar deneyin.", + "You have exceeded your daily SMTP outbound rate limit.": "Günlük SMTP giden hız sınırınızı aştınız." } \ No newline at end of file diff --git a/locales/uk.json b/locales/uk.json index b621307ac5..44bfff5e44 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -7364,5 +7364,6 @@ "You can only read or manage your existing aliases, and do not have permission to create new ones.": "Ви можете лише читати наявні псевдоніми або керувати ними, і не маєте дозволу створювати нові.", "You cannot have more than 3 aliases for this domain.": "Ви не можете мати більше 3 псевдонімів для цього домену.", "Launchpad username was missing or not detected, please try again later.": "Ім’я користувача Launchpad відсутнє або не виявлено, спробуйте пізніше.", - "Invalid response from Launchpad API, please try again later.": "Недійсна відповідь від API Launchpad, спробуйте пізніше." + "Invalid response from Launchpad API, please try again later.": "Недійсна відповідь від API Launchpad, спробуйте пізніше.", + "You have exceeded your daily SMTP outbound rate limit.": "Ви перевищили щоденний ліміт вихідної швидкості SMTP." } \ No newline at end of file diff --git a/locales/vi.json b/locales/vi.json index 0cd9f4a0f4..0254190b13 100644 --- a/locales/vi.json +++ b/locales/vi.json @@ -4843,5 +4843,6 @@ "You can only read or manage your existing aliases, and do not have permission to create new ones.": "Bạn chỉ có thể đọc hoặc quản lý các bí danh hiện có của mình và không có quyền tạo bí danh mới.", "You cannot have more than 3 aliases for this domain.": "Bạn không thể có nhiều hơn 3 bí danh cho tên miền này.", "Launchpad username was missing or not detected, please try again later.": "Tên người dùng Launchpad bị thiếu hoặc không được phát hiện, vui lòng thử lại sau.", - "Invalid response from Launchpad API, please try again later.": "Phản hồi không hợp lệ từ API Launchpad, vui lòng thử lại sau." + "Invalid response from Launchpad API, please try again later.": "Phản hồi không hợp lệ từ API Launchpad, vui lòng thử lại sau.", + "You have exceeded your daily SMTP outbound rate limit.": "Bạn đã vượt quá giới hạn tốc độ gửi đi SMTP hàng ngày của mình." } \ No newline at end of file diff --git a/locales/zh.json b/locales/zh.json index aaeadeb2de..a2e6768e04 100644 --- a/locales/zh.json +++ b/locales/zh.json @@ -7057,5 +7057,6 @@ "You can only read or manage your existing aliases, and do not have permission to create new ones.": "您只能读取或管理现有的别名,而无权创建新的别名。", "You cannot have more than 3 aliases for this domain.": "此域名的别名不能超过 3 个。", "Launchpad username was missing or not detected, please try again later.": "Launchpad 用户名缺失或未被检测到,请稍后重试。", - "Invalid response from Launchpad API, please try again later.": "Launchpad API 的响应无效,请稍后重试。" + "Invalid response from Launchpad API, please try again later.": "Launchpad API 的响应无效,请稍后重试。", + "You have exceeded your daily SMTP outbound rate limit.": "您已超出每日 SMTP 出站速率限制。" } \ No newline at end of file diff --git a/routes/web/my-account.js b/routes/web/my-account.js index 1a456c762c..8e78e61baf 100644 --- a/routes/web/my-account.js +++ b/routes/web/my-account.js @@ -372,6 +372,17 @@ router rateLimit(10, 'download alias backup'), web.myAccount.downloadAliasBackup ) + .post( + '/domains/:domain_id/aliases/:alias_id/upload-mbox', + web.myAccount.retrieveDomain, + web.myAccount.ensureUpgradedPlan, + web.myAccount.ensureSMTPAccess, + web.myAccount.retrieveAlias, + web.myAccount.ensureAliasAdmin, + policies.ensureTurnstile, + rateLimit(5, 'upload alias mbox'), + web.myAccount.uploadAliasMbox + ) .get( '/domains/:domain_id/billing', web.myAccount.retrieveDomain, diff --git a/test/web/snapshots/index.js.md b/test/web/snapshots/index.js.md index 68161d1986..17df59dbe3 100644 --- a/test/web/snapshots/index.js.md +++ b/test/web/snapshots/index.js.md @@ -24,7 +24,7 @@ Generated by [AVA](https://avajs.dev). signed forwards don't trip email filters 👏. I'm a happy user! ❤️
Creator of Ruby on Rails, Founder & CTO at Basecamp & HEY
abhinemani
abhi nemaniVerified
@abhinemani
Have now switched email forwarding from MailGun to ForwardEmail.net␊ . Simple and painless (and free!). Just some DNS changes, and it just works. Thanks
Government Technology Advisor, Sacramento and Los Angeles
andrewe
Andrew Escobar (Andres)Verified
@andrewe
This is so dope. Thank you. forwardemail.net
Fintech Explorer and Open Finance Advocate
stigi
Ullrich Schäfer
@stigi
Thanks so much for forwardemail.net␊ ! It solves a real problem for our little org!
Mobile Lead at Pitch, Formerly at Facebook and Soundcloud
andregce
Andre Goncalves
@andregce
So they made this cool app that forwards email from your own domain to your Gmail inbox. There is even a catch all option, so sales@, support@, etc all goes to your own inbox. Check it out! It's free! forwardemail.net
Computer Engineer, Software Developer
philcockfield
Phil
@philcockfield
Thanks for your forwardemail.net␊ - . What you've done is a beautiful thing! Your FAQ just smacks of integrity, and is just the thing I need.
hypersheet, db.team
Made for you

Open-source

Unlike other services, we do not store logs (with the exception of errors and outbound SMTP) and are 100% open-source. We're the only service that never stores nor writes to disk any emails – it's all done in-memory.

Features

Privacy-focused

We created this service because you have a right to privacy. Existing services did not respect it. We use robust encryption with TLS, do not store SMTP logs (with the exception of errors and outbound SMTP), and do not write your emails to disk storage.

Regain your privacy Regain your privacy

Disposable Addresses

Create a specific or an anonymous email address that forwards to you. You can even assign it a label and enable or disable it at any time to keep your inbox tidy. Your actual email address is never exposed.

Disposable addresses Disposable addresses

Multiple Recipients and Wildcards

You can forward a single address to multiple, and even use wildcard addresses – also known as catch-all's. Managing your company's inboxes has never been easier.

Start forwarding now Start forwarding now

"Send mail as" with Gmail and Outlook

You'll never have to leave your inbox to send out emails as if they're from your company. Send and reply-to messages as if they're from you@company.com directly from you@gmail.com or you@outlook.com.

Configure your inbox Configure your inbox

Enterprise-grade

We're email security and deliverability experts.

  • Protection against phishing, malware, viruses, and spam.
  • Industry standard checks for DMARC, SPF, DKIM, SRS, ARC, and MTA-STS.
  • SOC 2 Type 2 compliant bare metal servers from Vultr and Digital Ocean.
  • Unlike other services, we use 100% open-source software.
  • Backscatter prevention, denylists, and rate limiting.
  • Global load balancers and application-level DNS over HTTPS ("DoH").