Forward Email

forwardemail.net
Forward Email

Catch-all email forwarding service with unlimited aliases and custom domains, configured via DNS records. Self-hostable; the hosted version has a limited free plan, with paid plans (from $3/month) adding encrypted IMAP/POP3 mailboxes. Audited by Cure53 in 2026.

Security Audited Open Source

Forward Email Source Code

Author

forwardemail

Description

Privacy-focused encrypted email for everyone. All-in-one alternative to Gmail + Mailchimp + Sendgrid.

#aes256#chacha20#custom#domain#email#encrypted#forwarder#forwarding#free#imap#mail#newsletter#pop#privacy#send#smtp#sqlite

Homepage

https://forwardemail.net

Repository

  • LicenseOther
  • Created17 Dec 19
  • Primary languageJavaScript
  • Size1,287,045 KB
  • Stars1,680
  • Forks208
  • Watchers1,680

Language Usage

Language Usage

Project Health

  • Last commit15 hours ago
  • Open issues72
  • Latest releasev2.18.3

Recent Commits

  • titanism(07 Oct 26)

    fix(web): API reference failing with "tte.build is not a function" Revisioning (gulp-rev-all) also rewrote file contents: every quoted name of another script in the same folder became its revisioned name, as if it were a reference. Since the API reference loads the revisioned scalar.js (a1ca030), Scalar's "build" method was "build.693f86c2" there, and the reference stopped rendering. "core", "scalar", "download", "deferred" and "debounce" were rewritten in it too, and "download", "logger" and "apexcharts" in build.js. Revisioning now only renames files. No script or stylesheet refers to another by name, so nothing else changes. The /v1/log 400s on that page were the browser reporting the same error repeatedly; the API keeps one per hour and refuses the rest. New test: every revisioned script and stylesheet in the build is the same file as its original (fails without the fix).

  • titanism(07 Oct 26)

    fix(imap): reliable iOS Mail push on the Apple-issued topic only iOS Mail does not take pushes on the XServer Mail topic, and APNs answers 200 for a push on any topic while a device only accepts pushes on the topic the IMAP server gave it in its last XAPPLEPUSHSERVICE reply. Mail push now uses only the Apple-issued certificate and topic; Calendar and Contacts keep the XServer certificates. Topics: - send Mail pushes only with APNS_MAIL_CERT_PATH on its topic, with no XServer fallback; without the certificate XAPPLEPUSHSERVICE is not advertised and registration is refused - store the topic given to the device with each registration, and skip registrations on another topic (e.g. XServer) until the device registers again - read the XServer certificates for Calendar and Contacts from Redis every 5 minutes, so renewed certificates and topics are used without a restart Connections: - time out connects and unanswered pushes after 15 seconds and retry once on a new connection; a connection dropped silently by a firewall or NAT used to hold every later push in that process forever - close connections idle for 5 minutes - treat a stream closed or reset before a response as a failed push instead of waiting forever Delivery: - take the coalescing lock atomically (SET NX) for exactly the 10 second window; it used to last a minute after a 10 second wait, dropping new mail that arrived in the 50 seconds after any push - check the mailbox subscription before the lock, matching INBOX in any case (RFC 3501) - send only {"aps":{"account-id":"..."}} as dovecot-xaps-daemon and WildDuck do; aps.m named only the first coalesced mailbox Registrations: - remove Mail rows stored before subtopics were recorded when a device registers again - on 410 or BadDeviceToken, keep a registration made after the 410 timestamp, and remove atomically with $pull instead of rewriting the array Diagnostics: - APNS_DEBUG=true prints an [APNs] line to stdout for each registration, skip, queued push, send, retry and APNs response, with shortened device tokens and no certificates or keys; production logs only error and fatal - log the Mail certificate expiry warning as an error so it shows in production - scripts/debug-apns.js shows each registration's topic and queued push, and sends Mail pushes only on the Apple-issued topic like production - PUSH_NOTIFICATIONS.md covers topics per registration and each [APNs] line

  • titanism(06 Oct 26)

    feat: warn before any certificate we use expires The daily SSL monitor only checked the certificate served at WEB_URL. It now checks every certificate and key the servers use, and sends one email listing all of them with renewal steps for the ones that need attention (warning at 30 days, critical at 7, expired, or unreadable). - Certificate files: the TLS certificate and CA bundle from certificates.yml (app servers, MongoDB, Valkey), the Apple Mail push certificate (IMAP XAPPLEPUSHSERVICE), and every *_CERT_PATH and *_CA_PATH in the app's .env, following {{{VAR}}} references. Every certificate in a chain or bundle is checked; an expired root left in a bundle is noted instead of reported every day. - The OpenPGP key that signs security.txt: when it can still sign and encrypt, ignoring expired subkeys that were replaced; a revoked key is an error. - The certificate each local TLS service serves (node, mongod, valkey), over implicit TLS and STARTTLS. A process keeps the certificate it started with, so this catches one that still needs a reload after a renewal. Processes are matched by executable, since PM2 renames the cluster-mode process that owns the public ports. - The Calendar and Contacts push certificates cached in Redis (aps_certs), from one host, through the new scripts/apn-cert-expiry.js. The cache renews itself before they expire, so these are reported only when it would not. Apple .p8 keys (Sign in with Apple, APNs token auth), DKIM keys and Firebase service-account keys do not expire, so they are not checked. Also: - The cooldown is 23 hours, so a daily timer that finishes sooner than the day before still sends the day's alert. - WEB_URL may include a port. - security.yml tags the monitor's tasks forwardemail-certificate-monitor, installs its dependencies, and gives it its own restart handler. - certificate_monitor_extra_paths lists more files to check. - ansible/scripts/test-certificate-monitor.sh runs the monitor against certificates, OpenPGP keys and TLS services (including a PM2-style cluster primary) it creates, with no email sent.

  • titanism(06 Oct 26)

    chore: removed x artifact

  • titanism(06 Oct 26)

    chore: ignore aps.cer

  • titanism(06 Oct 26)

    2.18.3

  • titanism(06 Oct 26)

    feat(imap): push iOS Mail on our Apple-issued APNs topic Apple granted Forward Email the com.apple.mobilemail.push.net.forwardemail topic for IMAP XAPPLEPUSHSERVICE. Mail push can now use a certificate for that topic instead of the XServer certificates; Calendar and Contacts are unchanged. Mail push: - load APNS_MAIL_CERT_PATH and APNS_MAIL_KEY_PATH; APNS_MAIL_TOPIC defaults to the certificate subject UID - check the key matches, the certificate has not expired and the topic is in the certificate; fall back to the XServer certificate on failure and retry the load after 5 minutes - warn 30 days before expiry and reconnect when the certificate changes - keep the private key out of Redis XAPPLEPUSHSERVICE: - reply with the configured topic - reject device tokens that are not 64 hex characters and account IDs that are not UUIDs - advertise the capability when the Mail certificate or APPLE_ID is set, not the app's APPLE_KEY_* credentials APNs responses: - keep registrations on 400 DeviceTokenNotForTopic until the device re-registers with the new topic - remove registrations on 400 BadDeviceToken, as on 410 Deploy and docs: - ansible certificates playbook uploads apns-mail.pem and apns-mail.key - PUSH_NOTIFICATIONS.md covers the CSR, certificate and verification - scripts/debug-apns.js uses the Mail certificate when set

  • titanism(06 Oct 26)

    2.18.2

  • titanism(06 Oct 26)

    fix: repeated welcome messages for aliases without IMAP - An SMTP login is kept in the auth cache for a minute, shared by every server, and a cache hit skipped the IMAP check. A phone that sends mail through an alias without IMAP and then checks for new mail got into IMAP with that login and opened the mailbox. The hourly cleanup deletes the mailbox of an alias without IMAP, so the next check set up a new one, each with the welcome message: several a day. Other servers now treat such a cached login as a miss and refuse it as before; SMTP is unchanged. - Only an alias with IMAP gets the welcome message (once). A mailbox of an alias without IMAP is deleted every hour, and each new one got another copy.

  • titanism(05 Oct 26)

    fix: serve current robots.txt and llms.txt, readable home page text Discovery files - robots.txt, llms.txt, llms-full.txt, site.webmanifest, browserconfig.xml, opensearch.xml and /.well-known/* were kept in Redis (koa-cash) for a year under their path, and a deploy does not clear that cache. The live robots.txt was still the March version, with the Crawl-delay for every crawler; the per-crawler groups and Content-Signal lines never reached it. Only files whose path changes with their content (css, js, img, fonts) are cached there now. The others are served from disk or their route, with an hour of browser caching as before. - The API reference loads the revisioned scalar.js, so a new build reaches it. Home page - Words no longer run together in the page's text ("Zero-knowledge Quantum-resistant encryption" read "Zero-knowledgeQuantum-resistant encryption", "ProductProduct", "SignupSignup"). Search engines, AI crawlers and text extractors read the page without its CSS, so adjacent elements now have a space between them; the layout is unchanged. The nav link's name is "Forward Email" once for screen readers. - "Privacy by default" says what happens to mail, with sources: forwarded mail is processed in memory and never written to disk, each mailbox is its own encrypted SQLite file that only its password opens, the Cure53 report is linked, and so is the FAQ answer on what is stored and for how long. Translated in the 25 locales. - Public pages link the About page as rel="author". Tests - New test: discovery files are not cached in Redis, revisioned files are (fails without the fix). - Home page snapshots updated; they were missing the Bunny DNS guide in the nav.

  • titanism(04 Oct 26)

    fix: FAQ examples, duplicate welcome messages, zone file test FAQ and onboarding pages - Show the visitor's domain and address only in place of the example ones ("example.com", "[email protected]", "[email protected]"). The whole page was rewritten as a string: every "admin" became part of the address ("your domain's first.lasts"), "admin.google.com" and "admin.microsoft.com" broke, and the canonical link and FAQ schema changed. Now only visible text and mailto: links change, and the values are HTML-escaped (helpers/personalize-examples.js). - The Encrypt button posts the address itself, not its escaped HTML. - Pricing link in the nav and footer: one query string instead of "?domain=...?pricing=true". - Logs breadcrumb: encode the domains filter. Welcome message - An alias could get the IMAP welcome message more than once: an attempt that stored it and then failed (e.g. on a closed handle) was retried and stored it again. The message now has a fixed Message-ID per alias, an attempt first looks for it in the mailbox, and the append drops a copy that is already there. Tests - zone-file: check the advanced settings page before the setup page, which verifies the domain again and replaces its nameservers with the live ones (failed in CI). - New tests for the FAQ examples and the welcome message retries, each failing without its fix.

  • titanism(04 Oct 26)

    2.18.1

  • titanism(04 Oct 26)

    fix: keep forwarding destinations private, zone file export, shorter capacity greylist MX replies to senders - keep the account's plan and billing out of replies: a mailbox error that is not written for the sender now says the mailbox is unavailable (the original goes to logs and the owner's email), with the same response code as before - deferral replies say which aliases already received the message and which were delivered to only some of their destinations - never name a forwarding destination: replies from the destination's server mask it in every form (punycode, Unicode, IP literal without brackets), errors written while reaching it (DNS, connection, TLS) that name its domain, mail servers or IPs are replaced, and a destination denylisted mid-delivery is reported as the alias - webhook errors without a status or code no longer echo the endpoint - greylist a message for 5 minutes (1 minute if allowlisted) instead of up to 2 hours after a capacity bounce (full mailbox or system, too many connections, recipient receiving mail too quickly), as for network errors Logs - a log of a message to several recipients only shows a viewer their own deliveries: other customers' and members' forwarding destinations, destination replies and bounces are filtered out of the log list, log detail, API and CSV export (deliveries now record the alias they were for) Cloudflare Family DNS - cache its answers apart from the default resolver (a blocked 0.0.0.0 answer was returned to every other lookup of that domain, and an unfiltered answer cached first hid the block), for at most 30 minutes Domains - export every record a domain needs (MX, verification, SPF, DKIM, Return-Path, DMARC, autoconfig/autodiscover CNAME and SRV) as a zone file with absolute names and quoted TXT, from Domains, Setup and Settings, with import steps per DNS provider from config - the API returns it as `zone_file` when a domain is retrieved (GET /v1/domains/:domain) or created (POST /v1/domains), documented in the API docs and OpenAPI specs, whose Domain schema also gains the response fields it was missing (domain_updates, has_pending_domain_updates, members, invites, has_strict_dmarc, has_spf_record, has_autoconfig_record, has_autodiscover_record) - the zone file is shown in the app's code colors in light and dark, one record per line (a long DKIM key scrolls sideways) - setup page offers all records at once instead of only after step 2 - add Bunny DNS to the DNS providers (guide page and import steps) - add a Team link to each domain that opens Manage Team - translations for all locales, API docs and OpenAPI specs

  • titanism(04 Oct 26)

    fix(web): smaller home page, one page script, sourced copy Page size - The navbar menus and the sign in, sign up, search and domain search dialogs ship in <template> elements (assets/js/deferred.js). The browser adds them once it is idle after load, or on the first interaction if that comes sooner. With a mouse, core.js adds them at load, before the dropdown hover plugin binds the menus. The home page as served drops from 1,294 to 934 elements. - build.js ends with the WebMCP tools (their data attributes moved to its tag), and the home page loads home.js (domain search, hero console and video player) in place of three scripts: 3 scripts instead of 6. - Font Awesome's faces list WOFF2 only. The stylesheet named 13 font files and now names 5. - The video still and the testimonial avatars offer smaller copies (640px and 44px) through srcset. The still and the first avatar load at low priority as the first images after the logo; the rest stay lazy. Copy - The Enterprise card names the Linux Foundation and Canonical, each linked to its case study. - The sending section cites Gmail's sender guidelines and the spam rate that lowers a sender's threshold (config.smtpReputationBadDayReportRate). - The testimonials lede says what Cure53's audit covered, according to its report. The privacy panel says Cure53 audited the code twice, and the developer panel says our webmail and apps use the same API. - The four route steps are list labels instead of headings. - New strings in the 25 locales. Fixes - The search dialog's title id matches its aria-labelledby. - /search declares the WebMCP search tool on its own form.

  • titanism(04 Oct 26)

    2.18.0

  • titanism(04 Oct 26)

    feat(web): app chips in the footer The footer on every page has a row of chips under the payment methods, one per app: macOS, Windows, Linux, Android, iOS and Terminal. They look like the platform chips on the download page and link to that platform's card there (/download#fe-download-<platform>), which lists its builds. The links work without JavaScript. - Centered on every width; the six chips wrap into two rows of three on a phone. - The row is labeled "Apps", which is translated already, and the icons are hidden from screen readers. - A test checks each chip's link in English and Spanish, and that the download page has the card it points at. The home page and OTP snapshots include the new row.

  • titanism(02 Oct 26)

    fix: failing tests, regressions from recent commits, SRS replies, FAQ links Tests (time, environment and race independent): - reputation: the midnight UTC test no longer trips the hourly burst limit in the first hour of a UTC day - srs-reverse: the sender is on a domain of its own, not under example.com (WEB_HOST in CI, and a real p=reject DMARC domain) - invite-lifecycle: wait for the SCHEDULE-STATUS write-back before taking the snapshot - model-indexes: build indexes in a database of their own, so background log writes cannot add duplicate hashes before the unique index exists - teardown force-closes the Mongoose connections: since CalDAV and CardDAV requests count their bandwidth (a Lua script on ioredis-mock), a normal close never finished in caldav/mkcol-calendar-create, so its worker never exited and the shard timed out - welcome-message: wait for the mailbox handle to be evicted, as the maintenance that opening a mailbox starts in the background holds it for a moment (longer on a loaded runner) - reset-mailbox, sqlite-file-utils, alias-password-reset: where locks cannot be listed (e.g. macOS), leftover -wal/-shm files still count as an open connection and the reset is refused Mailbox reset: - /proc/locks is only trusted in the initial PID namespace; a container of its own (self-hosted Docker) does not see the locks of another one, so a reset could replace a mailbox under a live connection (there, leftover -wal/-shm files count as an open connection, as before) - compare inodes exactly (bigint) Accounts: - an account is no longer verified when its code cannot be sent (our mail server down, our own login refused, or a 4xx): /verify answers 503 with the error and the user tries again, and the onboarding form leaves the account unverified; a permanent refusal of the address (a 5xx to RCPT TO, or nodemailer's own check of the recipient) answers 400 - a rejected address on /verify shows the error instead of redirecting back (which sent the code again and looped) - a pending recovery is signed out when verified from the emailed link (the redirect to /logout stayed cross-site and was ignored) Mail (replies to SRS addresses): - each destination that accepts a forwarded message, and each recipient of outbound SMTP mail, allows 3 replies to its SRS address (a delay notice, the bounce and an auto-reply), once per message and SRS address however often it is retried (a retry on a later day has a new SRS address); one reply per message was not enough when an alias forwards to several destinations, and outbound SMTP mail allowed none - a destination that refuses the message allows none (its refusal is answered here) - a reply only uses up its allowance once relayed; one refused (for now or for good) or skipped gives it back, so the sender's retry is relayed too - a retry of a reply already relayed is accepted and skipped even with no allowance left (e.g. it was refused for now for another recipient), and a reply is never relayed without an allowance in hand (it is refused for now, without greylisting the message) - of several SRS addresses of one sender in a message, the one holding an allowance is used - use an allowance atomically Calendar: - an iMIP REPLY, COUNTER or REFRESH from an attendee whose domain has no DMARC record is accepted when SPF passed for an envelope sender on the same organizational domain (DMARC's relaxed alignment, with tldts and the private suffixes, so victim.eu.org and evil.eu.org differ); a DMARC result other than none or pass rules it out, and the organizer check is unchanged API: - GET /v1/domains/:id shows every member and invite to an admin again after an update (the domain fetched again has no group); a non-admin still sees only their own membership - /v1/emails multipart (authenticated) keeps 1000 fields and parts, so 50 attachments with their fields fit - calendar and calendar event create generate a null or empty optional id again instead of answering 400 - labels given to a new message come before the labels from its keywords, so they are kept within the limit Bandwidth, contacts and WKD (from 663d60f8): - REST API requests signed in with an alias were counted, and refused, as CalDAV traffic; only CalDAV and CardDAV requests are counted - $elemMatch keys may contain "-" again (a CardDAV param-filter on a vCard parameter such as X-SERVICE-TYPE failed); quotes, dots and other characters are still refused - a WKD reply with no body (204, 205 or 304) no longer throws a TypeError Labels: - only valid keywords count toward the label limit - a STORE that removes labels reports all of them Aliases: - a restricted name in another script (e.g. "josé") no longer covers the name its ASCII letters spell ("jos"); restricted names are compared NFKC Help: - FAQ suggestions look words up in each answer's sorted words instead of searching the whole answer per word (same results, about 2.5x faster) FAQ: - each topic title links to its topic, and each question to its answer, so a link can be copied, shared or opened in a new tab; a plain click on a question still opens and closes it (without a scroll jump) - three English headings carried their own id, which put it in the title and broke their table of contents links - remove an unused video thumbnail Billing: - the PayPal subscription sync treats PAYMENT_ALREADY_EXISTS as a duplicate - a plan change link opened while signed out and followed from the sign-in page once signed in (same-origin) is asked about, like any other link from elsewhere

  • titanism(02 Oct 26)

    feat(web): Share button, readable emails and pages, clearer team invites Share - A Share button in the footer of every public page, and a Share entry in the byline of articles and guides, open a dialog with the page's canonical link, Copy link, and plain links to Messages, Email, WhatsApp, Telegram, Mastodon, Bluesky, X, Reddit, Hacker News, LinkedIn and Facebook (the list Terminal Email and Privacy Ratings use). No script or widget from those sites loads. - On phones and tablets the button opens the device share sheet. If the sheet fails for a reason other than the reader closing it, the dialog opens instead. Permissions-Policy now allows web-share for our own origin; web-share=() made Chrome refuse navigator.share(). - Mastodon asks for the reader's server and takes it as typed: "hachyderm.io", "https://hachyderm.io/", "fosstodon.org/@name", "@[email protected]", a port, or an international name. - The dialog ships in a <template> and joins the page on the first click (assets/js/share.js), so it adds no elements to the page. The buttons are hidden without JavaScript, and private pages (account, admin, login) have neither. Home page - Pricing and testimonials ask the question a reader has ("How many domains can I use?", "Why do people switch to Forward Email?") and answer it in the first sentence below. The product lede opens with what the product does. The testimonials lede links Cure53, the auditor. - The Time to Inbox intro comes before the timestamp, says who runs the monitor, and links its source. - Shorter title (68 characters) and description (148), without the year and the "#1" claim. - The WebPage node names Forward Email as author and publisher. - A "Skip to content" link is the first focusable element on every page and moves focus to <main>. - The small-screen search field and the domain search field have labels, and the domain search button has a name. - The tour video has an English WebVTT captions track, off by default since the captions are also in the picture. - Testimonial avatars and video card stills are WebP. - The nav logo is an <img> instead of the inlined SVG (about 80 elements and 19 KB on every page), and the home page no longer renders the storage modal, which nothing on it opens. The home page drops from 1,396 to 1,272 elements. - @font-face lists WOFF2 only. - robots.txt gives Googlebot and Bingbot their own groups, so the Crawl-delay for * does not apply to them. All new strings are translated in the 25 locales. Code blocks are dark text on a light panel. The dark panel needed its light text color to arrive as well: an older build, a forward or a client that lost the color showed dark text on it (#212529 on #070b16 in the outbound SMTP emails), and one that lost the background showed light text on white. Dark on light reads in both cases and in clients that recolor for dark mode. Checking every template turned up more text under 4.5:1: - Muted text and footers ($fe-l-500, 4.48:1 on the light panels) now use a darker grey. - Inline code was Bootstrap's pink (3.8:1). - Danger text and badges ($fe-danger-deep, 4.1:1 on the badge tint), success buttons (white on Bootstrap green, 3.1:1), and primary, info and secondary text take darker steps of the same hues. - The DMARC report and the daily log alert colored numbers with Bootstrap's green and yellow (down to 1.5:1); they use the email palette, and their status pills are tinted like the other badges. - The upgrade panel's dark code blocks showed the address in light red and the encrypted value in green on near black (3.6:1). - The calendar invitation's Accept button was white on green (3.1:1), and the admin abuse reports used orange and yellow text (down to 1.6:1). - A .bg-dark panel sets light text on itself, so text inside it that sets no color stays readable. The layout declares color-scheme: light only, so Apple Mail and iOS Mail keep the colors instead of recoloring them. A test renders every template under emails/ in English and German and checks each piece of text against the background it sits on. In dark mode a light section takes the dark ramp, but the eyebrow pill kept its -deep blue (1.7:1 on the raised panel behind "Unlimited sending", 2.8:1 on the canvas). The eyebrow, the home page's step labels and figures, and the onboarding step numbers now read their color from the surface (--fe-eyebrow-*, --fe-fg-accent): -deep blue on light, Signal and the lift blue on dark. The mint pricing eyebrow lost to the blue rule on specificity and rendered blue; it is mint again. Other text that measured under 4.5:1 in a browser, in light and dark mode, across 31 pages: - Bootstrap's grey for muted text, placeholders and floating labels (4.45:1 on #f8f9fa, 4.19:1 on Ink in dark mode). .text-secondary follows .text-muted in dark mode. - Links inside alerts (3.9:1 on the primary tint in light mode, 4.4:1 on the warning tint in dark mode). - The light code theme's red, orange and green (3.3:1 to 4.5:1). An invitee with only a mailbox on the domain opened the invite link, was sent to the login page, and tried the mailbox password there, which never works on the website. Signed in with another account, they got "Invite does not exist with your email address for this domain.", the same message as for an expired, accepted or unknown invite. - A signed-out invite link shows the domain and the invited address, says a Forward Email account (with its own password) is needed, and links to sign up with the address filled in or to sign in, both returning to the invite. - Signed in with another account: the page names both addresses and offers "Sign out and continue", which returns to the invite. Logout accepts a return_to path on this site. - An expired invite says it expired; inviting the address again replaces it. The domain settings mark it Expired and hide its copy link. - Opening an accepted invite again takes a member to the domain. - Addresses are compared lowercase with the domain in ASCII form. - Accepting takes a verified email address. It used to verify the account on acceptance, so whoever held a copied link could sign up with the shown address and accept. - The sign in form and the invite email say a mailbox password does not work on the website. The invite email gives the address and the days until it expires. - New FAQ entry: How do I add team members to my domain. - API docs and spec: accepting an invite is GET /v1/domains/:domain_id/invites/:token, answered in plain text, with 410 for an expired invite.

  • titanism(02 Oct 26)

    fix: privacy policy, push tokens, analytics paths, welcome message, labels Privacy policy (English and all 24 translations) - New Apps and Webmail section: what the apps keep on the device, what they send, push notifications (what a token is stored with and when it is deleted, delivery through Apple and Google with the sender, subject, preview and folder name, silent change notifications, Firebase in the Google Play app), images and links in emails, and GitHub version checks. - Error Logs now covers failed and slow website and API requests and errors on the IMAP, POP3, CalDAV and CardDAV servers, and a new Server Logs section covers request logs. - Analytics, Cookies and Sessions, and signup attribution say what signed-in events carry, that paths are stored with values such as domain names, IDs and tokens replaced by placeholders, what the session keeps for analytics, and that hourly totals are kept for 90 days. - Information Shared names the service providers (Cloudflare, Stripe, PayPal and the push services), and Information Removal says what deleting an account removes, revokes and keeps. - Fixes broken links in the German, Finnish, Japanese, Portuguese and Turkish pages. Push tokens - Deleting an alias, a domain or an account deletes the push tokens registered for its aliases, and an alias that moves to another owner stops notifying the previous owner's devices (hooks on the Aliases model). - A new alias password stops notifying the devices signed in with the previous one: finalizeRekey, which every password rotation ends in, deletes the alias's push tokens. Rollbacks keep them. - The hourly database cleanup deletes expired tokens, tokens whose alias is gone, and tokens registered while someone else owned the alias. The TTL index on expires_at cannot be built where an older plain index holds its name, so expired tokens otherwise stay. Account deletion - The account record that stays also loses its password, password reset token and passkeys. Analytics - Page views, API calls and the signup landing page store the pattern of the route a request matched (/my-account/domains/:domain_id/aliases), never the path, which carried domain names, alias names, IDs and one-time tokens into the events and the hourly totals. Sessions keep the landing page under a new key, so paths stored by older sessions are dropped instead of saved on new accounts. - scripts/normalize-analytics-paths.js replaces the paths stored before in events, hourly totals and accounts, and removes those that match no route. Run it once after every web and API server runs this version. Welcome message - A new mailbox gets the welcome message in its INBOX also when the alias does not have IMAP enabled yet. The first password sets the mailbox up either way, so an alias that got IMAP later never got one. For such an alias the message is not marked as sent: the hourly cleanup deletes the mailbox of an alias without IMAP, and the mailbox set up once IMAP is enabled gets it then. A Redis key held for an hour keeps two concurrent first opens from writing it twice. - A mailbox set up in place of a deleted one gets its folders again. The Redis keys that mark a mailbox's checks as done outlived the file, so the new mailbox got no INBOX and no welcome message. Creating a new mailbox file now clears them. - The hourly cleanup tells the SQLite servers to drop their cached handles on a mailbox before it deletes the file. - Writing it holds the mailbox handle like a request does. The password reset that sets the mailbox up ends by evicting the handle, which could close it while the message was being stored, and the message was lost. - A failed attempt is made again twice, a few seconds apart. An alias whose welcome message could not be written is not marked as having one, and the failure is logged with its reason. Labels and keywords IMAP clients see a message's flags and labels together as its keywords, and the API, webmail and the apps read the labels. A keyword from a Sieve script (addflag, fileinto :flags), an IMAP APPEND or the API's flags stayed out of the labels, so IMAP showed it and the API listed none. - Keywords that the API's flags or flags_add add become labels. Of a whole flags list, only keywords new to the message count, so a label removed with a whole labels list stays removed. - A message Sieve files into a folder gets the flags of that fileinto, with the ones its :flags names (RFC 5232). Delivery dropped them. - SEARCH KEYWORD and UNKEYWORD match labels as well as flags, in any case. - The new-message notification of mail stored while no client was signed in carries its labels. - Messages stored earlier with keywords and no labels get the labels, once per mailbox, in the background when the mailbox opens. The changed messages get a new modseq, so clients that sync changes pick them up. A pass that stops (a restart, a handle evicted for a rekey) runs again on a later open. - test/api/labels-sync.js delivers mail through the MX server with Sieve scripts and checks the labels through the API and the keywords over IMAP. About page - An October 2026 entry, in all 25 languages: the terminal app, TerminalEmail.com and PrivacyRatings.com.

  • titanism(02 Oct 26)

    feat(mx): reject unauthenticated mail from unconfigured and compromised servers Unconfirmed reverse DNS - new rule: a 421 when an IPv4 host has no forward-confirmed reverse DNS (no PTR record, or a PTR hostname that does not resolve back to it), its HELO name cannot be checked either (an address literal, or a public hostname that does not resolve to it), the sending domain's SPF fails or softfails, and there is no aligned DKIM, DMARC pass or trusted ARC - an SPF permerror counts only when it provably hides no authorization: the error is an invalid term that is the last one in the domain's only SPF record (e.g. a missing space before "~all"), so every other term was checked - not judged: a HELO name that resolves to the host, is in the sending domain, or is internal ("localhost", "srv.corp"), other SPF permerrors (too many lookups, multiple records), IPv6 clients, bounces, allowlisted addresses, and any DNS timeout or server failure (fail open) Root scripts on compromised servers - new rule: a 421 when root submitted the message on the sending server as root@ that server (Postfix, Exim or Sendmail local submission as the newest Received header, on the host that greeted us), with no SPF or DKIM pass for any domain, under an unrelated From domain that publishes no SPF record, from a server without its own reverse hostname - not judged: cron mail sent as the server itself, other local users (web applications), From domains that publish SPF (a contact form putting a visitor's address in From), relayed mail, confirmed reverse hostnames, bounces, allowlisted addresses and DNS errors Defaults - these two rules and the generic reverse DNS rule now reject by default; set GENERIC_RDNS_SPAM_MONITOR_ONLY, UNCONFIRMED_RDNS_SPAM_MONITOR_ONLY or ROOT_SCRIPT_SPAM_MONITOR_ONLY to true to only log and count matches Reverse DNS - checkForwardConfirmedRdns tells "does not resolve back" apart from "no answer"; isForwardConfirmedRdns is unchanged - on-connect records a definite miss only, checking up to three PTR names

  • titanism(02 Oct 26)

    feat: add Privacy Ratings badges to comparison pages Comparison pages: - show a Privacy Ratings badge first in every service's badge list, above Hardenize Test, on the best-of, alternatives and vs pages - each badge links to that service's own rating page on privacyratings.com and renders through the Shields.io endpoint like the other badges Config: - add a privacy_ratings field (<category>/<entry>) to every service in config/alternatives.js - Pobox, Altospam, Drift, Crisp, Olark and HelpCrunch are set to false and show no badge until a rating page exists Tests: - add test/web/compare.js covering the badge order, links and uniqueness

  • titanism(02 Oct 26)

    fix(security): verified OAuth sign-in, revoked sessions, bandwidth limits Accounts - sign in with Google or GitHub only by an address the provider verified; an unverified address could sign in to another account - account deletion refuses while another admin manages a team domain (the guard read `.length` of a boolean, so it never applied) - alias backup downloads count wrong passwords toward the alias password limit and are rate limited - escape alias names in member removal and demotion messages and in the restricted alias names alert - FAQ/onboarding inserts the email address literally and caps it - admin quota changes clear the edited user's cache, not the admin's - cap the User-Agent at 500 characters before parsing it Mail servers - disabling or deleting an alias, or banning its owner, now closes its open IMAP, POP3 and SMTP sessions instead of after up to a day - enforce the bandwidth limits (50 GB a day, 10 GB an hour per protocol): IMAP FETCH and APPEND answer NO [LIMIT], POP3 RETR -ERR, SMTP 452 and CalDAV/CardDAV 429 once one is used up; FAQ updated - IMAP applies PGP and S/MIME changes to open sessions - IMAP limits mailbox name length and depth and SEARCH terms, and refuses invalid SEARCH dates - ManageSieve limits data buffered while the connection is checked - keep at most 200 APNs registrations per alias - escape the alias and pattern in the invalid regex alert email Calendars and contacts - CalDAV and CardDAV refuse event, contact and attachment writes over quota (507) - iMIP HTML replies, ICS filenames and DMARC report checks are parsed in linear time; generated VTIMEZONEs are limited Other - DANE-TA requires the pinned anchor to issue the chain and the certificate to name the MX host - limit WKD response size - json-sql validates $is/$isnot values and $elemMatch keys

  • titanism(02 Oct 26)

    fix(security): escape user values, limit member data, stop SRS relaying Views and emails: - escape the address in the new alias notice (`?new=`), the regex alias name in recipient verification emails, account addresses on the verify, change email and profile pages, and account addresses and other values in staff alert emails (denylist removal, Ubuntu sync, disputes) API: - creating an alias as a non-admin member no longer returns the other members, pending invites or admin-only domain settings - GET /v1/domains and /v1/domains/:id hide admin-only settings from non-admin members Accounts: - sign up takes the addresses our mail servers accept, and an address that refuses the verification email is no longer verified as if our mail server were down - reserved and restricted alias names also match look-alike, fullwidth and punctuated forms (e.g. "аdmin", "billing.team") Mail: - mail to an SRS address is relayed to the original sender only in reply to a message we forwarded (once per message), not to anyone for 10 days - reject an envelope MAIL FROM on our own domain unless the sender is our own domain (it skipped SRS and, as support@, the outbound scan) - webhook and error response bodies are read with size and time limits - DMARC reports: only for the domain (or its organizational domain), unauthenticated reports have their own daily limit, and the envelope sender is only a truth source when SPF passed - an alias without a generated password fails login like any other alias, so it cannot be used to find private aliases Calendar: - an event is only sent when every VEVENT has its UID and the sender as ORGANIZER, so an override cannot update another user's meeting Billing: - plan changes on the billing GET route only come from this site (Sec-Fetch-Site, or the Referer when it is missing); a link from elsewhere asks to confirm; Stripe and PayPal returns are unchanged Models: - build every declared index: a field-level index took the name of the unique `reference`, partial (`user`, `locked_at`, `smtp_suspended_sent_at`) and TTL (push token `expires_at`) indexes, so those were never built and `Payments.init()` failed (CI) - a duplicate payment ID rejected by the unique validator now carries PAYMENT_ALREADY_EXISTS, as the Stripe/PayPal race handlers expect Outbound requests: - connect-time lookups check every address when asked for all of them (Node 20+ in self-hosted builds) Self-hosting and CI: - MongoDB and Redis listen on loopback only; SMTP transport password is random; dev compose binds database ports to loopback - pin lkiesow/matrix-notification by commit

  • titanism(01 Oct 26)

    feat(web): link Terminal Email from the terminal client pages The two pages that list terminal email clients, /blog/open-source/terminal-email-clients and /blog/open-source/command-line--cli-email-clients, now open with a box under the title that links to Terminal Email (terminalemail.com), our comparison of terminal email clients for Linux, macOS, Windows, BSD and Android. It is a followed link with "Terminal Email" as its text, and the sentence is translated in all 25 locales. On /download, the hero's "Install in the terminal" button is hidden below 768px with bootstrap's display classes (d-none d-md-inline-flex), so phones no longer show it. The Terminal section further down still lists every build. That section's "Terminal" label read as an airport hall in Arabic, a handset in Korean and "the end" in Vietnamese. All three now name a computer terminal.

  • titanism(01 Oct 26)

    fix: harden query building, access control, sender checks and request limits security audit fixes across the web app, API, mail storage and mail handling. - sqlite query builder (helpers/json-sql.js): reject a plain-object value (e.g. `{ "$eq": { "expression": "1 OR 1" } }` from a JSON body) before it reaches the builder. json-sql-enhanced inlines a `{ expression }` value into the SQL text verbatim, so a calendar/calendar-events/contacts request could run arbitrary SQL against its own alias database and tie up the sqlite worker. Values are now always bound; raw SQL stays available for fields and subquery terms, which our code uses and a request cannot reach. - calendars / calendar-events API: require string `name`, `calendar_id` and `event_id` (isSANB) before any lookup, so a non-string 400s instead of flowing into a query (contacts already validated its ids). - outbound mail (SMTP and API): reject a message with more than one From field. Header names were compared case-sensitively and the alias check read the last From, so `FROM: someone-else` followed by `From: own-alias` was queued and DKIM-signed with both, and a recipient could be shown the first. `From :`, a first line after whitespace, and a field after a bare CR count too. - iMIP replies (REPLY/REFRESH/COUNTER): a reply only counts for the mailbox it was delivered to, which must be its ORGANIZER. The queued record was keyed to the ORGANIZER named in the message, so a reply sent to any mailbox here (the sender's own included) could change another user's event. The attendee is now matched against the authenticated From address (DMARC pass or aligned DKIM, on the authenticated domain) instead of the envelope sender, and a null reverse-path no longer skips the check. A related-domain match needs one domain to be a subdomain of the other (evil.co.uk no longer matches victim.co.uk), the pending-reply lookup includes the organizer, and an `xn--` ORGANIZER matches an alias on an IDN domain. A reply from a sender without DMARC pass or aligned DKIM is still delivered, but not applied. - `?limit=0` no longer means "no limit": a shared middleware drops a limit below 1 on the web and API so koa-ctx-paginate applies the route default instead of paging a whole collection. - logout: ignore a cross-site top-level navigation (Sec-Fetch-Site) so another site cannot sign a user out via the GET logout link. - logs: only server-written logs (is_restricted) can page admins; a log from the public POST /v1/log can no longer trigger a code-bug alert email. - API generate-password: enforce ensureSMTPAccess like the website form (no global or suspended domains). - advanced-settings GET: require domain admin, matching the PUT route. - multipart: lower the pre-auth field/part caps (1000 -> 200). - websocket: answer and close an upgrade to any path other than /v1/ws so the detached socket is not left open. - import-aliases: escape DNS-derived names/recipients in the rendered import error messages. tests: builder value binding, the limit sanitizer, logout CSRF, the log-alert restriction, API rejection of object-typed calendar fields, a second From field over the API and SMTP, and iMIP reply binding and sender checks (each fails without its fix). Existing iMIP tests now pass the authenticated From.

  • titanism(01 Oct 26)

    fix(rekey): reset a mailbox whose -wal/-shm files outlived their connection Resetting an alias password with "Don't remember your current password" failed every time with "Mailbox creation failed" (or timed out) when the mailbox had -wal/-shm files that no connection owned anymore. A reset only replaces the mailbox once no -wal/-shm file proves an open connection. A read-only connection cannot checkpoint, so when it is the last one to close it leaves both files behind, and so does a process that is killed while connected. On a mailbox that is rarely opened nothing ever removes them, so the check could never pass. Every connection to a WAL database, read-only or not, holds a lock on the database file and on the -shm file until it closes or its process dies. The reset now only counts -wal/-shm files that a lock refers to, removes the rest with the old mailbox, and keeps treating them as a connection when the locks cannot be read. A rollback journal still always counts.

  • titanism(01 Oct 26)

    feat(web): add the apps and terminal videos to the download page /download gets two videos, each in its own modal, like the tour on the home page. A "Watch the video" link in the hero opens a 1:49 video of the apps on macOS, Windows, Linux, iOS and Android, and a card beside the lede of the Terminal section opens a 1:44 video of the terminal app. A link to /download#video or /download#video-terminal opens that video on arrival. The terminal card and its modal render only when the release has the terminal app's builds, as the section itself does. As on the home page, a video loads only when its modal opens, the card's image is lazy, and without JavaScript the link and the card open the MP4. Each video has an H.264 MP4, a VP9 WebM and a 1280x720 poster. The mixins in _fe-video.pug now take the video to show (the tour when none is given), and each video names its modal and the hash that opens it. Each Watch link and card is described by its modal's title, so a screen reader can tell the two on /download apart. js/home-video.js is now js/video-modal.js, which handles every video modal on a page. The tour on the home page plays as before. All 25 locales have the two new titles.

  • titanism(01 Oct 26)

    fix(realtime): send every mailbox change to WebSocket and push clients once A message deleted in Thunderbird (or moved, flagged, read or relabeled in any client) reached webmail and the apps twice, without the mailbox path, and some changes never reached them at all. IMAP - Only the SQLite server publishes the event. The IMAP, API and POP3 servers published a second copy with its own notificationId. - messagesExpunged carries the mailbox path, the UIDs and the message ids; flagsUpdated and labelsUpdated the path; messagesMoved and messagesCopied the source path and the stored destination path. - labelsUpdated from a STORE carries the labels added or removed (or all of them after FLAGS), and only the UIDs whose labels changed; a client reading "set" with no list cleared every label. - A change to more than 1,000 messages is sent as several events of up to 1,000 UIDs, so no WebSocket frame gets near the 1 MB payload limit. - Reading a message over IMAP (implicit \Seen) also sends an Apple Mail push for the unread count. Other changes that were not published - The daily Trash, Spam and Junk cleanup sends messagesExpunged, an IMAP EXPUNGE (journal and IDLE) and an Apple Mail push. - POP3 RETR sends flagsUpdated and an Apple Mail push. - A folder made by Sieve fileinto :create, or a required folder made again (e.g. Trash after a DELETE), sends mailboxCreated. - CardDAV PROPPATCH of an address book sends the new addressBookUpdated. - POST /v1/messages with labels sends labelsUpdated once they are saved, and keeps the labels the keywords in its flags gave it. REST API - PUT /v1/messages/:id publishes flagsUpdated and labelsUpdated only when the stored flags or labels changed, right after the save (so a quick read then unread arrives in order), with the full IMAP flags, the path of the folder the request named, and an Apple Mail push. Push - Push data names the mailbox ids, UIDs and message ids of a change, so an app woken by a push applies it without refetching the folder; a move carried no mailbox at all before. Folder paths and flags stay out of APNs and FCM data, which is not encrypted end to end. A list that does not fit in one FCM value is left out whole, never sent in part. Docs - The /v1/ws events in the OpenAPI spec (all locales) match what the server sends: one flat object with timestamp and notificationId, labelsUpdated and addressBookUpdated, aliasId in connected, events split at 1,000 UIDs, and $refs that pointed at their own schema.

  • shaunwarman(01 Oct 26)

    fix(imap): apply labels from keywords on append, so filter labels show in webmail

  • shaunwarman(01 Oct 26)

    fix(help): match FAQ suggestions against full answer bodies

Forward Email Website

Website

Free Email Forwarding for Custom Domains - Open Source Email Service

Get free email forwarding for custom domains. Send & receive as [email protected] with unlimited aliases, 10GB storage & 100% open-source security.

Redirects

Redirects to https://forwardemail.net/000+

Security Checks

1 security checks failed (64 passed)

  • Password Field Present

Server Details

  • IP Address121.127.44.69
  • Hostnameforwardemail.net
  • LocationLondon,Kentucky,United States of America,NA
  • ISPDataCamp Limited
  • ASNAS60068

Associated Countries

  • USUS

Safety Score

Website marked as moderately safe

90%

Blacklist Check

forwardemail.net was found on 0 blacklists

  • AntiSocial Blacklist
  • Artists Against 419
  • Badbitcoin
  • Bambenek Consulting
  • CERT Polska
  • CoinBlockerLists
  • CRDF
  • CryptoScamDB
  • EtherAddressLookup
  • EtherScamDB
  • Fake Website Buster
  • MetaMask EthPhishing
  • NABP Not Recommended Sites
  • OpenPhish
  • PetScams
  • PhishFeed
  • PhishFort
  • Phishing.Database
  • PhishStats
  • PhishTank
  • Phishunt
  • RPiList Not Serious
  • Scam.Directory
  • SecureReload Phishing List
  • Spam404
  • StopGunScams
  • Suspicious Hosting IP
  • ThreatFox
  • ThreatLog
  • TweetFeed
  • URLhaus
  • ViriBack C2 Tracker

Website Preview

Website preview

Forward Email Reviews

More Mail Forwarding

About the Data: Forward Email

Change History

  • Moved from Communication › Encrypted Email by @lissy93 #660

Edit Forward Email Data

You can edit Forward Email's entry in this section of awesome-privacy.yml by submitting a PR to our GitHub repo.
Note that some of the information shown above has been aggregated from external sources, a list of these can be found data documentation.

Origin Data

Modify Data

API

You can access Forward Email's data programmatically via our API. Simply make a GET request to:

https://api.awesome-privacy.xyz/v1/services/forward-email

The REST API is free, no-auth and CORS-enabled. To learn more, view the API Docs or read the API Usage Guide.

Share Forward Email

Help your friends compare Mail Forwarding, and pick privacy-respecting software and services.
Share Forward Email and Awesome Privacy with your network!