Skip to content

Commit

Permalink
fix: added Received and X-Original-To header, fixed DMARC/friendly-fr…
Browse files Browse the repository at this point in the history
…om issues, fixed PGP forwarding, fixed logger usage for optimization/performance, added back reviews link to footer, added ?return_to querystring for login links, updated FAQ docs, fixed `return` -> `continue` in isDenylisted function, fixed APN logger output issue, bump deps
  • Loading branch information
titanism committed Dec 26, 2024
1 parent 0f42a73 commit 06e03dc
Show file tree
Hide file tree
Showing 35 changed files with 493 additions and 201 deletions.
1 change: 1 addition & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ licensed under the [Mozilla Public License 2.0](mozilla-public-license-20) below
* [helpers/encrypt-message.js](https://github.com/forwardemail/forwardemail.net/blob/master/helpers/encrypt-message.js)
* [helpers/get-key-info.js](https://github.com/forwardemail/forwardemail.net/blob/master/helpers/get-key-info.js)
* [helpers/get-query-response.js](https://github.com/forwardemail/forwardemail.net/blob/master/helpers/get-query-response.js)
* [helpers/get-received-header.js](https://github.com/forwardemail/forwardemail.net/blob/master/helpers/get-received-header.js)
* [helpers/imap-notifier.js](https://github.com/forwardemail/forwardemail.net/blob/master/helpers/imap-notifier.js)
* [helpers/imap/\*\*/\*](https://github.com/forwardemail/forwardemail.net/blob/master/helpers/imap)
* With the exception of [helpers/imap/on-xapplepushservice.js](https://github.com/forwardemail/forwardemail.net/blob/master/helpers/imap/on-xapplepushservice.js) which is licensed under BSL (see below).
Expand Down
5 changes: 5 additions & 0 deletions app/models/logs.js
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,11 @@ Logs.pre('save', function (next) {
try {
// convert ansi (chalk) colors to html (mainly for HTTP request logging)
this.text_message = ansiHTML(this.message);
//
// TODO: we should conditionally not do this if MX server where
// we already are using `striptags()` on the message
//
// only do this if on server where `err.message` is not `striptags()`
// tokenization and search will be more accurate without HTML in messages
this.text_message = convert(this.text_message, {
wordwrap: false,
Expand Down
19 changes: 19 additions & 0 deletions app/views/_footer.pug
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,25 @@ footer.mt-auto(aria-label=t("Footer"))
if (domain && domain.name && config.ubuntuTeamMapping && Object.keys(config.ubuntuTeamMapping).includes(domain.name)) || ['/ubuntu','/kubuntu','/lubuntu','/edubuntu','/ubuntu-studio'].includes(ctx.pathWithoutLocale)
//- Empty on purpose
else
.text-center.mb-3
a(
href="https://www.trustpilot.com/review/forwardemail.net",
target="_blank",
rel="noopener noreferrer"
)
noscript
img(
alt=t("Review us on Trustpilot"),
src=manifest("img/trustpilot.png"),
width="216.5",
height="52"
)
img.lazyload(
alt=t("Review us on Trustpilot"),
data-src=manifest("img/trustpilot.png"),
width="216.5",
height="52"
)
ul.list-inline.mb-3.text-center
li.list-inline-item.border.rounded-lg.bg-dark.text-white.border-light.h6.p-2.mb-1
i.fa.fa-lock.mr-2
Expand Down
11 changes: 7 additions & 4 deletions app/views/_register-or-login.pug
Original file line number Diff line number Diff line change
Expand Up @@ -53,23 +53,23 @@ mixin registerOrLogin(verb, isModal = false)
//- <https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple>
if passport && passport.apple
a.btn-auth.text-center.mb-3.text-decoration-none.rounded-lg(
href="/auth/apple",
href=`/auth/apple?return_to=${encodeURIComponent(ctx.query.return_to || ctx.url)}`,
role="button"
)
span.btn-auth-icon.btn-auth-icon-fill.btn-auth-apple-logo
span.btn-auth-text.font-weight-bold= t(`${humanize(verb)} with Apple`)
//- <https://developers.google.com/identity/branding-guidelines>
if passport && passport.google
a.btn-auth.text-center.mb-3.text-decoration-none.rounded-lg(
href="/auth/google",
href=`/auth/google?return_to=${encodeURIComponent(ctx.query.return_to || ctx.url)}`,
role="button"
)
span.btn-auth-icon.btn-auth-google-logo
span.btn-auth-text.font-weight-bold= t(`${humanize(verb)} with Google`)
//- <https://github.com/logos>
if passport && passport.github
a.btn-auth.text-center.mb-3.text-decoration-none.rounded-lg(
href="/auth/github",
href=`/auth/github?return_to=${encodeURIComponent(ctx.query.return_to || ctx.url)}`,
role="button"
)
span.btn-auth-icon.btn-auth-icon-fill.btn-auth-github-logo
Expand All @@ -78,7 +78,10 @@ mixin registerOrLogin(verb, isModal = false)
if passport && (passport.apple || passport.google || passport.github || (verb === 'sign in' && passport.webauthn))
.hr-text.d-flex.text-secondary.align-items-center= t("or")
- const action = verb === "sign up" ? "/register" : config.loginRoute;
form.ajax-form(action=l(action), method="POST")
form.ajax-form(
action=`${l(action)}?return_to=${encodeURIComponent(ctx.query.return_to || ctx.url)}`,
method="POST"
)
.form-group.floating-label
input.form-control(
id=`input-email-${dashify(verb)}`,
Expand Down
7 changes: 4 additions & 3 deletions app/views/faq/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -1613,9 +1613,10 @@ This section describes our process related to the SMTP protocol command `DATA` i

9. We will add the following headers to the message for debugging and abuse prevention purposes:

* `X-Original-To` - the original `RCPT TO` email address for the message.
* This header's value has `Bcc` header parsed addresses removed from it.
* This is useful for determining where an email was originally delivered to.
* `Received` - we add this standard Received header with origin IP and host, transmission type, TLS connection information, date/time, and recipient.
* `X-Original-To` - the original recipient for the message:
* This is useful for determining where an email was originally delivered to (in addition to the "Received" header).
* BCC header addresses are removed from `RCPT TO` values in order to preserve privacy.
* `X-ForwardEmail-Version` - the current [SemVer](https://semver.org/) version from `package.json` of our codebase.
* `X-ForwardEmail-Session-ID` - a session ID value used for debug purposes (only applies in non-production environments).
* `X-ForwardEmail-Sender` - a comma separated list containing the original envelope MAIL FROM address (if it was not blank), the reverse PTR client FQDN (if it exists), and the sender's IP address.
Expand Down
2 changes: 1 addition & 1 deletion app/views/my-account/billing/index.pug
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ block body
)
input(type="hidden", name="_method", value="DELETE")
button.btn.btn-sm.btn-danger.mt-3(type="submit")
= t("Cancel Subscription")
= t("Disable Auto-Renew")
else
li.list-inline-item
a.btn.btn-sm.btn-success.mt-3(
Expand Down
22 changes: 16 additions & 6 deletions assets/js/logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,20 @@
* SPDX-License-Identifier: BUSL-1.1
*/

if (process.env.NODE_ENV === 'production') {
const logger = require('../../helpers/logger');
// expose it to the global window object
window.console = logger;
}
const Cabin = require('cabin');

module.exports = window.console;
const logger = require('../../helpers/logger');

// setup our Cabin instance
const cabin = new Cabin({ logger });

// set the user if we're logged in
if (typeof window === 'object' && typeof window.USER === 'object')
cabin.setUser(window.USER);

// if (process.env.NODE_ENV !== 'test')

// expose it to the global window object
window.console = logger;

module.exports = logger;
6 changes: 5 additions & 1 deletion config/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* SPDX-License-Identifier: BUSL-1.1
*/

const Cabin = require('cabin');
const ipaddr = require('ipaddr.js');
const isFQDN = require('is-fqdn');
const sharedConfig = require('@ladjs/shared-config');
Expand All @@ -20,6 +21,9 @@ const parseRootDomain = require('#helpers/parse-root-domain');

const sharedAPIConfig = sharedConfig('API');

// setup our Cabin instance
const cabin = new Cabin({ logger });

const RATELIMIT_ALLOWLIST =
typeof env.RATELIMIT_ALLOWLIST === 'string'
? env.RATELIMIT_ALLOWLIST.split(',')
Expand All @@ -45,7 +49,7 @@ module.exports = {
...config,
rateLimit,
routes: routes.api,
logger,
logger: cabin,
i18n,
hookBeforeSetup(app) {
app.context.resolver = createTangerine(
Expand Down
6 changes: 5 additions & 1 deletion config/caldav.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* SPDX-License-Identifier: BUSL-1.1
*/

const Cabin = require('cabin');
const ipaddr = require('ipaddr.js');
const isFQDN = require('is-fqdn');
const sharedConfig = require('@ladjs/shared-config');
Expand Down Expand Up @@ -59,6 +60,9 @@ const RATELIMIT_ALLOWLIST =
// TODO: move this to `caldav-server.js` similar to `imap-server.js` (?)
// <https://github.com/sedenardi/node-caldav-adapter/issues/14>

// setup our Cabin instance
const cabin = new Cabin({ logger });

module.exports = {
...sharedCalDAVConfig,
...config,
Expand All @@ -70,7 +74,7 @@ module.exports = {
passport: false,
auth: false,
routes: routes.caldav,
logger,
logger: cabin,
i18n,
hookBeforeSetup(app) {
app.context.resolver = createTangerine(
Expand Down
6 changes: 5 additions & 1 deletion config/web.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const path = require('node:path');
const process = require('node:process');

const Boom = require('@hapi/boom');
const Cabin = require('cabin');
const _ = require('lodash');
const dayjs = require('dayjs-with-plugins');
const ipaddr = require('ipaddr.js');
Expand Down Expand Up @@ -165,6 +166,9 @@ const reportUri = isSANB(process.env.WEB_URL)

const sharedWebConfig = sharedConfig('WEB');

// setup our Cabin instance
const cabin = new Cabin({ logger });

module.exports = (redis) => ({
...sharedWebConfig,
...config,
Expand All @@ -173,7 +177,7 @@ module.exports = (redis) => ({
...config.rateLimit
},
routes: routes.web,
logger,
logger: cabin,
i18n,
cookies: cookieOptions,
meta: config.meta,
Expand Down
4 changes: 4 additions & 0 deletions helpers/get-bounce-info.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ const REGEX_DENYLIST = new RE2(/denylist|deny\s+list/im);
const REGEX_BLACKLIST = new RE2(/blacklist|black\s+list/im);
const REGEX_BLOCKLIST = new RE2(/blocklist|block\s+list/im);

// TODO: add these to bounces.txt
// <https://github.com/zone-eu/zone-mta/issues/435>
// <https://postmaster-earthlink.vadesecure.com/inbound_error_codes/>

//
// NOTE: we have access to `err.truthSource` if needed here
// (e.g. google.com, qq.com, is the value)
Expand Down
32 changes: 32 additions & 0 deletions helpers/get-rcpt-to-without-bcc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Copyright (c) Forward Email LLC
* SPDX-License-Identifier: BUSL-1.1
*/

const punycode = require('node:punycode');

const getHeaders = require('#helpers/get-headers');
const parseAddresses = require('#helpers/parse-addresses');

function getRcptToWithoutBcc(session, headers) {
const rcptTo = new Set();

// add RCPT TO addresses
for (const to of session.envelope.rcptTo) {
rcptTo.add(punycode.toASCII(to.address).toLowerCase());
}

// strip BCC addresses
const bcc = parseAddresses(getHeaders(headers, 'bcc'));

if (bcc.length > 0) {
for (const addr of bcc) {
rcptTo.delete(punycode.toASCII(addr).toLowerCase());
}
}

// convert to Array as return value
return [...rcptTo];
}

module.exports = getRcptToWithoutBcc;
91 changes: 91 additions & 0 deletions helpers/get-received-header.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright (c) Forward Email LLC
* SPDX-License-Identifier: MPL-2.0
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* ZoneMTA is licensed under the European Union Public License 1.2 or later.
* https://github.com/zone-eu/zone-mta
*/

const os = require('node:os');

const isEmail = require('#helpers/is-email');

const HOSTNAME = os.hostname();

function getReceivedHeader(delivery) {
let origin = delivery.origin ? '[' + delivery.origin + ']' : '';
const originhost =
delivery.originhost && delivery.originhost.charAt(0) !== '['
? delivery.originhost
: false;
// eslint-disable-next-line unicorn/prefer-spread
origin = [origin || []].flat().concat(originhost || []);

origin =
origin.length > 1
? '(' + origin.join(' ') + ')'
: origin.join(' ').trim() || 'localhost';

// safeguard
if (delivery.recipient && !isEmail(delivery.recipient)) {
const err = new TypeError('Recipient must be a valid email address');
err.isCodeBug = true;
throw err;
}

const value =
'' +
// from ehlokeyword
'from' +
(delivery.transhost ? ' ' + delivery.transhost : '') +
// [1.2.3.4]
' ' +
origin +
(originhost ? '\r\n' : '') +
// by smtphost
' by ' +
HOSTNAME +
' (Forward Email)' +
// with ESMTP
' with ' +
delivery.transtype +
// id 12345678
// ' id ' +
// delivery.id +
// '.' +
// delivery.seq +
'\r\n' +
//
// NOTE: according to RFC 5321 Section 4.4 and 7.2 a FOR clause in the Received header
// (which is used for "Trace Information" can only contain exactly ONE entry even when multiple RCPT TO appears
//
// > 'Multiple <path>s raise some security issues and have been deprecated'
//
// <https://www.rfc-editor.org/rfc/rfc5321#section-4.4:~:text=If%20the%20FOR%20clause%20appears%2C%20it%20MUST%20contain%20exactly%20one%20%3Cpath%3E%0A%20%20%20%20%20%20entry%2C%20even%20when%20multiple%20RCPT%20commands%20have%20been%20given.%20%20Multiple%0A%20%20%20%20%20%20%3Cpath%3Es%20raise%20some%20security%20issues%20and%20have%20been%20deprecated%2C%20see%0A%20%20%20%20%20%20Section%207.2.>
//
// for <[email protected]>
(delivery.recipient ? ` for <${delivery.recipient}>` : '') +
// (version=TLSv1/SSLv3 cipher=ECDHE-RSA-AES128-GCM-SHA256)
(delivery.tls
? '\r\n (version=' +
delivery.tls.version +
' cipher=' +
delivery.tls.name +
')'
: '') +
';' +
'\r\n' +
// Wed, 03 Aug 2016 11:32:07 +0000
' ' +
new Date(delivery.time).toUTCString().replace(/GMT/, '+0000');
return value;
}

module.exports = getReceivedHeader;
6 changes: 5 additions & 1 deletion helpers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ const sendPaginationCheck = require('./send-pagination-check');
const populateDomainStorage = require('./populate-domain-storage');
const parseAddresses = require('./parse-addresses');
const isEmail = require('./is-email');
const getReceivedHeader = require('./get-received-header');
const getRcptToWithoutBcc = require('./get-rcpt-to-without-bcc');

const REGEX_LOCALHOST = require('./regex-localhost');

Expand Down Expand Up @@ -261,5 +263,7 @@ module.exports = {
populateDomainStorage,
parseAddresses,
REGEX_LOCALHOST,
isEmail
isEmail,
getReceivedHeader,
getRcptToWithoutBcc
};
5 changes: 3 additions & 2 deletions helpers/is-authenticated-message.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

const os = require('node:os');
const { Buffer } = require('node:buffer');

const _ = require('lodash');
const isSANB = require('is-string-and-not-blank');
Expand All @@ -19,7 +20,7 @@ const HOSTNAME = os.hostname();
const UBUNTU_DOMAINS = Object.keys(config.ubuntuTeamMapping);

// eslint-disable-next-line complexity
async function isAuthenticatedMessage(raw, session, resolver) {
async function isAuthenticatedMessage(headers, body, session, resolver) {
const options = {
ip: session.remoteAddress,
helo: session.hostNameAppearsAs,
Expand All @@ -28,7 +29,7 @@ async function isAuthenticatedMessage(raw, session, resolver) {
};

const [results, spfFromHeader] = await Promise.all([
authenticate(raw, {
authenticate(Buffer.concat([headers.build(), body]), {
...options,
sender: session.envelope.mailFrom.address,
seal: config.signatureData
Expand Down
Loading

0 comments on commit 06e03dc

Please sign in to comment.