Skip to content

Commit

Permalink
fix: fixed WKD cache issue, added rudimentary phishing email detectio…
Browse files Browse the repository at this point in the history
…n, bump deps for graceful
  • Loading branch information
titanism committed Dec 14, 2024
1 parent 80d9c31 commit 1f7418f
Show file tree
Hide file tree
Showing 35 changed files with 546 additions and 58 deletions.
Binary file added assets/img/emails/phishing.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions config/default.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# <https://github.com/nodemailer/wildduck/issues/768>
[log]
[log.gelf]
enabled=false
hostname=false # defaults to os.hostname()
component="forwardemail"
155 changes: 155 additions & 0 deletions emails/phishing/html.pug
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
extends ../layout

block content
.container.mt-3
.row
.col-12
.card.border-dark.d-block.text-center
h1.h5.card-header= t("We detected a possible phishing email")
.card-body.text-center.p-0
a(href=config.urls.web)
img.img-fluid.d-block.align-top(
src=manifest("img/emails/phishing.png"),
alt=""
)
.p-3
h1.h3!= t("Don't worry &ndash; everything is OK!")
p.card-text
!= t('We detected that an email sent to you from <strong class="notranslate">%s</strong> may be a phishing email. If you received this message, then please move it to your junk/spam folder if it is not a false positive.', from)
ul.list-inline
li
strong= t("Date")
= ": "
= date
li
strong= t("Subject")
= ": "
= subject
li
strong= t("Message-ID")
= ": "
= messageId
li
strong= t("IP")
= ": "
= remoteAddress
.p-3
h2.h5= t("Do you send this every time?")
p.card-text
= t("No, we only send it once a month per sender's root domain.")
.p-3
h2.h5= t("Why are you sending this email?")
p.card-text.small
!= t("This email is sent for your protection and in the event a developer or IT specialist did not configure SPF/DKIM correctly.")
= " "
= t("The reason we sent this email is because we detected that it did not have passing SPF nor DKIM and the sender's root domain did not match the From address root domain.")
.p-3
h2= t("What is Forward Email?")
p.card-text
!= t('For <span class="notranslate">%d</span> years and counting, we are the go-to email service for hundreds of thousands of creators, developers, and businesses.', dayjs().endOf("year").diff(dayjs("1/1/17", "M-D/YY"), "year"))
= " "
!= t('Send and receive email as <span class="notranslate font-weight-bold text-nowrap">[email protected]</span>.')
ul.list-unstyled.text-left.mb-3.d-inline-block.mx-auto
li
= emoji("white_check_mark")
= " "
= t("Unlimited domains and aliases")
= " "
span.badge.badge-success
= t("100% Free")
li
= emoji("white_check_mark")
= " "
= t("10 GB encrypted email storage")
= " "
a.badge.badge-dark(
href=`${config.urls.web}/${locale}/blog/docs/best-quantum-safe-encrypted-email-service`
)
= t("Learn more")
= " "
!= "&rarr;"
li
= emoji("white_check_mark")
= " "
= t("Privacy-focused email")
= " "
a.badge.badge-dark.align-middle(
href=`${config.urls.web}/privacy`
)
= t("Privacy Policy")
= " "
!= "&rarr;"
li
= emoji("white_check_mark")
= " "
= t("Send outbound SMTP email")
= " "
a.badge.badge-dark(
href=`${config.urls.web}/guides/send-email-with-custom-domain-smtp`
)
= t("SMTP Guide")
= " "
!= "&rarr;"
li
= emoji("white_check_mark")
= " "
= t("Works with Apple, Outlook, Gmail, Thunderbird, Android")
= " "
a.badge.badge-dark(
href=`${config.urls.web}/${locale}/faq#do-you-support-receiving-email-with-imap`
)
= t("Apps")
= " "
!= "&rarr;"
li
= emoji("white_check_mark")
= " "
= t("Enterprise-grade 99.99% uptime SLA")
= " "
a.badge.badge-dark(href=`${config.urls.web}/faq`)
= t("View FAQ")
= " "
!= "&rarr;"
li
= emoji("white_check_mark")
= " "
= t("Powered by bare metal servers")
= " "
a.badge.badge-dark(
href="https://status.forwardemail.net",
target="_blank",
rel="noopener noreferrer"
)
= t("Status Page")
= " "
!= "&rarr;"
li
= emoji("white_check_mark")
= " "
= t("100% open-source software")
= " "
a.badge.badge-dark(
href="https://github.com/forwardemail",
target="_blank",
rel="noopener noreferrer"
)
= t("View")
= " "
= t("GitHub")
= " "
!= "&rarr;"
li
= emoji("white_check_mark")
= " "
= t("Email API designed for developers")
= " "
a.badge.badge-dark(href=`${config.urls.web}/email-api`)
= t("Email API")
= " "
!= "&rarr;"
.d-block
a.btn.btn-lg.btn-success.text-uppercase.font-weight-bold(
href=config.urls.web
)
= t("Sign up free")
.card-footer.small.text-muted= t("If you have any questions or comments, then please let us know.")
1 change: 1 addition & 0 deletions emails/phishing/subject.pug
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
= striptags(`${emoji("fishing_pole_and_fish")} ${t('Possible Phishing from <span class="notranslate">%s</span>', from)}`)
14 changes: 7 additions & 7 deletions helpers/is-arbitrary.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,18 +227,18 @@ function isArbitrary(session, headers, bodyStr) {
parseRootDomain(parseHostFromDomainOrAddress(checkSRS(to.address))) ===
session.originalFromAddressRootDomain
);
if (hasSameRcptToAsFrom && session.spfFromHeader.status.result !== 'pass')
session.isPotentialPhishing = true; // used after email is delivered to imap/webhook/forwarding to send a one-time email
if (
hasSameRcptToAsFrom &&
session.spfFromHeader.status.result !== 'pass' &&
// NOTE: a lot of sysadmins have improperly configured SPF/DKIM
// on their servers and send wordpress/php script alerts
!headers.hasHeader('x-php-script') &&
!(
headers.hasHeader('x-mailer') &&
// Drupal
(headers.getFirst('x-mailer').toLowerCase().includes('drupal') ||
// PHPMailer (modern)
headers
.getFirst('x-mailer')
.toLowerCase()
.includes('https://github.com/phpmailer/phpmailer'))
// PHP/PHPMailer/Drupal
['php', 'drupal'].includes(headers.getFirst('x-mailer').toLowerCase())
) &&
!(subject && REGEX_SYSADMIN_SUBJECT.test(subject))
) {
Expand Down
16 changes: 8 additions & 8 deletions helpers/on-connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,14 +187,14 @@ async function onConnect(session, fn) {

// do not allow more than 10 concurrent connections using constructor
if (count > 10) {
const err = new TypeError(
`${HOSTNAME} detected 10+ connections from ${
session.resolvedClientHostname || session.remoteAddress
}`
);
err.isCodeBug = true;
err.session = session;
logger.fatal(err);
// const err = new TypeError(
// `${HOSTNAME} detected 10+ connections from ${
// session.resolvedClientHostname || session.remoteAddress
// }`
// );
// err.isCodeBug = true;
// err.session = session;
// logger.fatal(err);
throw new SMTPError(
`Too many concurrent connections from ${session.remoteAddress}`,
{ responseCode: 421, ignoreHook: true }
Expand Down
33 changes: 33 additions & 0 deletions helpers/on-data-mx.js
Original file line number Diff line number Diff line change
Expand Up @@ -1422,6 +1422,39 @@ async function onDataMX(raw, session, headers, body) {
}
}

// if at least one was accepted and potential phishing
// was detected from `helpers/is-arbitrary.js` then
// send a one-time email to each of the accepted recipients
session.isPotentialPhishing = true;
if (accepted.length > 0 && session.isPotentialPhishing) {
pMapSeries(accepted, async (to) => {
try {
const key = `phishing_check:${
session.originalFromAddressRootDomain
}:${to.toLowerCase()}`;
const cache = await this.client.get(key);
if (cache) return;
await this.client.set(key, true, 'PX', ms('30d'));
await emailHelper({
template: 'phishing',
message: { to, bcc: config.email.message.from },
locals: {
from: session.originalFromAddress,
domain: session.originalFromAddressRootDomain,
subject: headers.getFirst('subject'),
date: headers.getFirst('date'),
messageId: headers.getFirst('message-id'),
remoteAddress: session.remoteAddress
}
});
} catch (err) {
logger.fatal(err);
}
})
.then()
.catch((err) => logger.fatal(err));
}

// return early if no bounces (complete successful delivery)
if (bounces.length === 0) return;

Expand Down
11 changes: 0 additions & 11 deletions helpers/send-email.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
const { Buffer } = require('node:buffer');

const _ = require('lodash');
const isHTML = require('is-html');
const isSANB = require('is-string-and-not-blank');
const previewEmail = require('preview-email');
const { dkimSign } = require('mailauth/lib/dkim/sign');
Expand Down Expand Up @@ -82,16 +81,6 @@ async function sendEmail(
email: envelope.to
});

//
// TODO: we may not want to do isHTML check (?) see comment in GH discussion
//

// TODO: this is a temporary fix until the PR noted in `helpers/wkd.js` is merged
// <https://github.com/sindresorhus/is-html/blob/bc57478683406b11aac25c4a7df78b66c42cc27c/index.js#L1-L11>
const str = new TextDecoder().decode(binaryKey);
if (str && isHTML(str))
throw new Error('Invalid WKD lookup HTML result');

logger.info('binaryKey', { binaryKey });

publicKey = await readKey({
Expand Down
23 changes: 17 additions & 6 deletions helpers/wkd.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
* SPDX-License-Identifier: BUSL-1.1
*/

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

const Boom = require('@hapi/boom');
const WKDClient = require('@openpgp/wkd-client');
const isHTML = require('is-html');
const ms = require('ms');
const undici = require('undici');

Expand Down Expand Up @@ -89,26 +91,35 @@ function WKD(resolver, client) {
const key = `wkd:${options.email}`;

// check cache value
let cache = await client.get(key);
let cache = await client.getBuffer(key);

// if cache is `"false"` it indicates none
// if cache is a string then we can decode it
if (typeof cache === 'string') {
if (cache === 'false')
if (cache) {
if (cache.equals(Buffer.from('false')))
throw Boom.notFound('WKD key not found, try again in 30m');
const decoded = decoder.unpack(cache);
return decoded;
return decoder.unpack(cache);
}

try {
cache = await lookup.call(this, options);

//
// TODO: we may not want to do isHTML check (?) see comment in GH discussion
//

// TODO: this is a temporary fix until the PR noted in `helpers/wkd.js` is merged
// <https://github.com/sindresorhus/is-html/blob/bc57478683406b11aac25c4a7df78b66c42cc27c/index.js#L1-L11>
const str = new TextDecoder().decode(cache);
if (str && isHTML(str)) throw new Error('Invalid WKD lookup HTML result');

client
.set(key, encoder.pack(cache), 'PX', ms('1h'))
.then()
.catch((err) => logger.fatal(err));
return cache;
} catch (err) {
await client.set(key, 'false', 'PX', ms('1h'));
await client.set(key, Buffer.from('false'), 'PX', ms('1h'));
throw err;
}
};
Expand Down
12 changes: 11 additions & 1 deletion locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -10384,5 +10384,15 @@
"Cox Communications includes": "تتضمن شركة Cox Communications",
"in SMTP error messages, but they are merging with Yahoo mail and this will be deprecated": "في رسائل خطأ SMTP، ولكنها تندمج مع بريد Yahoo وسيتم إيقاف هذا",
"t-online.de (German/T-Mobile) includes": "يتضمن t-online.de (الألمانية/T-Mobile)",
"in SMTP error messages, but they have not been reliable per our removal requests in the past": "في رسائل خطأ SMTP، لكنها لم تكن موثوقة وفقًا لطلبات الإزالة التي قدمناها في الماضي"
"in SMTP error messages, but they have not been reliable per our removal requests in the past": "في رسائل خطأ SMTP، لكنها لم تكن موثوقة وفقًا لطلبات الإزالة التي قدمناها في الماضي",
"We detected a possible phishing email": "لقد اكتشفنا وجود رسالة تصيد محتملة",
"We detected that an email sent to you from <strong class=\"notranslate\">%s</strong> may be a phishing email. If you received this message, then please move it to your junk/spam folder if it is not a false positive.": "لقد اكتشفنا أن رسالة البريد الإلكتروني المرسلة إليك من <strong class=\"notranslate\">%s</strong> قد تكون رسالة تصيد. إذا تلقيت هذه الرسالة، فيرجى نقلها إلى مجلد البريد العشوائي/غير المرغوب فيه إذا لم تكن رسالة إيجابية خاطئة.",
"Message-ID": "معرف الرسالة",
"IP": "الملكية الفكرية",
"No, we only send it once a month per sender's root domain.": "لا، نحن نرسلها مرة واحدة شهريًا فقط لكل نطاق جذر المرسل.",
"This email is sent for your protection and in the event a developer or IT specialist did not configure SPF/DKIM correctly.": "تم إرسال هذا البريد الإلكتروني لحمايتك وفي حالة عدم قيام أحد المطورين أو المتخصصين في تكنولوجيا المعلومات بتكوين SPF/DKIM بشكل صحيح.",
"The reason we sent this email is because we detected that it did not have passing SPF nor DKIM and the sender's root domain did not match the From address root domain.": "السبب الذي جعلنا نرسل هذا البريد الإلكتروني هو أننا اكتشفنا أنه لا يحتوي على SPF أو DKIM ناجحين وأن نطاق الجذر الخاص بالمرسل لا يتطابق مع نطاق الجذر الخاص بعنوان المرسل.",
"Possible Phishing from <span class=\"notranslate\">%s</span>": "احتمالية حدوث تصيد احتيالي من <span class=\"notranslate\">%s</span>",
"Webhook request missing X-Signature-Header": "طلب Webhook مفقود X-Signature-Header",
"Invalid signature in webhook request": "توقيع غير صالح في طلب الويب هوك"
}
12 changes: 11 additions & 1 deletion locales/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -10384,5 +10384,15 @@
"Cox Communications includes": "Cox Communications zahrnuje",
"in SMTP error messages, but they are merging with Yahoo mail and this will be deprecated": "v chybových zprávách SMTP, ale slučují se s poštou Yahoo a tato podpora bude ukončena",
"t-online.de (German/T-Mobile) includes": "t-online.de (německy/T-Mobile) zahrnuje",
"in SMTP error messages, but they have not been reliable per our removal requests in the past": "v chybových zprávách SMTP, ale podle našich požadavků na odstranění v minulosti nebyly spolehlivé"
"in SMTP error messages, but they have not been reliable per our removal requests in the past": "v chybových zprávách SMTP, ale podle našich požadavků na odstranění v minulosti nebyly spolehlivé",
"We detected a possible phishing email": "Zjistili jsme možný phishingový e-mail",
"We detected that an email sent to you from <strong class=\"notranslate\">%s</strong> may be a phishing email. If you received this message, then please move it to your junk/spam folder if it is not a false positive.": "Zjistili jsme, že e-mail zaslaný z <strong class=\"notranslate\">%s</strong> může být phishingový e-mail. Pokud jste obdrželi tuto zprávu, přesuňte ji prosím do složky nevyžádaná pošta/spam, pokud není falešně pozitivní.",
"Message-ID": "ID zprávy",
"IP": "IP",
"No, we only send it once a month per sender's root domain.": "Ne, posíláme je pouze jednou měsíčně na kořenovou doménu odesílatele.",
"This email is sent for your protection and in the event a developer or IT specialist did not configure SPF/DKIM correctly.": "Tento e-mail je zasílán pro vaši ochranu a v případě, že vývojář nebo IT specialista správně nenakonfiguroval SPF/DKIM.",
"The reason we sent this email is because we detected that it did not have passing SPF nor DKIM and the sender's root domain did not match the From address root domain.": "Důvod, proč jsme poslali tento e-mail, je ten, že jsme zjistili, že neprošel SPF ani DKIM a kořenová doména odesílatele neodpovídá kořenové doméně adresy odesílatele.",
"Possible Phishing from <span class=\"notranslate\">%s</span>": "Možný phishing od <span class=\"notranslate\">%s</span>",
"Webhook request missing X-Signature-Header": "V požadavku webhooku chybí X-Signature-Header",
"Invalid signature in webhook request": "Neplatný podpis v požadavku webhooku"
}
Loading

0 comments on commit 1f7418f

Please sign in to comment.