Skip to content

Commit

Permalink
feat: added Apple Push Notifications for IMAP iOS PUSH via XAPPLEPUSH…
Browse files Browse the repository at this point in the history
…SERVICE (per <nodemailer/wildduck#711>)
  • Loading branch information
titanism committed Jul 27, 2024
1 parent 3ec9024 commit 6398425
Show file tree
Hide file tree
Showing 26 changed files with 172 additions and 30 deletions.
1 change: 1 addition & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ licensed under the [Mozilla Public License 2.0](mozilla-public-license-20) below
* [helpers/get-query-response.js](https://github.com/forwardemail/forwardemail.net/blob/master/helpers/get-query-response.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).
* [helpers/indexer.js](https://github.com/forwardemail/forwardemail.net/blob/master/helpers/indexer.js)
* [helpers/is-message-encrypted.js](https://github.com/forwardemail/forwardemail.net/blob/master/helpers/is-message-encrypted.js)
* [helpers/pop3/\*\*/\*](https://github.com/forwardemail/forwardemail.net/blob/master/helpers/pop3)
Expand Down
17 changes: 16 additions & 1 deletion app/models/aliases.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ Token.plugin(mongooseCommonPlugin, {
});

const Aliases = new mongoose.Schema({
// apple push notification support
// <https://github.com/nodemailer/wildduck/issues/711>
aps_account_id: String,
aps_device_token: String,
aps_subtopic: String,
aps_mailboxes: String,

// pgp encryption support
has_pgp: {
type: Boolean,
Expand Down Expand Up @@ -367,7 +374,15 @@ Aliases.pre('validate', function (next) {
// it populates "id" String automatically for comparisons
Aliases.plugin(mongooseCommonPlugin, {
object: 'alias',
omitExtraFields: ['is_api', 'tokens', 'pgp_error_sent_at'],
omitExtraFields: [
'is_api',
'tokens',
'pgp_error_sent_at',
'aps_account_id',
'aps_device_token',
'aps_subtopic',
'aps_mailboxes'
],
defaultLocale: i18n.getLocale()
});

Expand Down
2 changes: 1 addition & 1 deletion app/models/threads.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
const crypto = require('node:crypto');

const Database = require('better-sqlite3-multiple-ciphers');
const MessageHandler = require('wildduck/lib/message-handler');
const MessageHandler = require('@forwardemail/wildduck/lib/message-handler');
const _ = require('lodash');
const mongoose = require('mongoose');
const safeStringify = require('fast-safe-stringify');
Expand Down
2 changes: 1 addition & 1 deletion helpers/attachment-storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const revHash = require('rev-hash');
const _ = require('lodash');
const { Builder } = require('json-sql');

const WildDuckAttachmentStorage = require('wildduck/lib/attachment-storage');
const WildDuckAttachmentStorage = require('@forwardemail/wildduck/lib/attachment-storage');

//
// we don't use base64 decoding/encoding of attachments (unlike wildduck)
Expand Down
2 changes: 1 addition & 1 deletion helpers/encrypt-message.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const crypto = require('node:crypto');
const { Buffer } = require('node:buffer');

const openpgp = require('openpgp');
const tools = require('wildduck/lib/tools');
const tools = require('@forwardemail/wildduck/lib/tools');

const config = require('#config');

Expand Down
2 changes: 1 addition & 1 deletion helpers/get-key-info.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

const Boom = require('@hapi/boom');
const openpgp = require('openpgp');
const tools = require('wildduck/lib/tools');
const tools = require('@forwardemail/wildduck/lib/tools');

const i18n = require('#helpers/i18n');

Expand Down
4 changes: 3 additions & 1 deletion helpers/imap/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const onStatus = require('./on-status');
const onStore = require('./on-store');
const onSubscribe = require('./on-subscribe');
const onUnsubscribe = require('./on-unsubscribe');
const onXAPPLEPUSHSERVICE = require('./on-xapplepushservice');

module.exports = {
onAppend,
Expand All @@ -50,5 +51,6 @@ module.exports = {
onStatus,
onStore,
onSubscribe,
onUnsubscribe
onUnsubscribe,
onXAPPLEPUSHSERVICE
};
11 changes: 10 additions & 1 deletion helpers/imap/on-append.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ const dayjs = require('dayjs-with-plugins');
const mongoose = require('mongoose');
const parseErr = require('parse-err');
const splitLines = require('split-lines');
const { IMAPConnection } = require('wildduck/imap-core/lib/imap-connection');
const {
IMAPConnection
} = require('@forwardemail/wildduck/imap-core/lib/imap-connection');
const { convert } = require('html-to-text');

const Aliases = require('#models/aliases');
Expand All @@ -38,6 +40,7 @@ const isCodeBug = require('#helpers/is-code-bug');
const isRetryableError = require('#helpers/is-retryable-error');
const refineAndLogError = require('#helpers/refine-and-log-error');
const updateStorageUsed = require('#helpers/update-storage-used');
const sendApn = require('#helpers/send-apn');

const { formatResponse } = IMAPConnection.prototype;

Expand Down Expand Up @@ -73,6 +76,12 @@ async function onAppend(path, flags, date, raw, session, fn) {
});
this.server.notifier.fire(session.user.alias_id);

// send apple push notification if the path was INBOX
if (path === 'INBOX')
sendApn(session.user.alias_id)
.then()
.catch((err) => this.logger.fatal(err, { session }));

fn(null, bool, response);
} catch (err) {
if (err.imapResponse) return fn(null, err.imapResponse);
Expand Down
2 changes: 1 addition & 1 deletion helpers/imap/on-copy.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

const mongoose = require('mongoose');
const ms = require('ms');
const tools = require('wildduck/lib/tools');
const tools = require('@forwardemail/wildduck/lib/tools');
const { Builder } = require('json-sql');
// const { boolean } = require('boolean');

Expand Down
2 changes: 1 addition & 1 deletion helpers/imap/on-delete.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* https://github.com/nodemailer/wildduck
*/

// const imapTools = require('wildduck/imap-core/lib/imap-tools');
// const imapTools = require('@forwardemail/wildduck/imap-core/lib/imap-tools');
const pify = require('pify');

const onMove = require('./on-move');
Expand Down
6 changes: 4 additions & 2 deletions helpers/imap/on-expunge.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
*/

const pMapSeries = require('p-map-series');
const tools = require('wildduck/lib/tools');
const tools = require('@forwardemail/wildduck/lib/tools');
const { Builder } = require('json-sql');
const { IMAPConnection } = require('wildduck/imap-core/lib/imap-connection');
const {
IMAPConnection
} = require('@forwardemail/wildduck/imap-core/lib/imap-connection');

const IMAPError = require('#helpers/imap-error');
const Mailboxes = require('#models/mailboxes');
Expand Down
8 changes: 5 additions & 3 deletions helpers/imap/on-fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
*/

const getStream = require('get-stream');
const tools = require('wildduck/lib/tools');
const tools = require('@forwardemail/wildduck/lib/tools');
const { Builder } = require('json-sql');
const { IMAPConnection } = require('wildduck/imap-core/lib/imap-connection');
const { imapHandler } = require('wildduck/imap-core');
const {
IMAPConnection
} = require('@forwardemail/wildduck/imap-core/lib/imap-connection');
const { imapHandler } = require('@forwardemail/wildduck/imap-core');
const _ = require('lodash');

const IMAPError = require('#helpers/imap-error');
Expand Down
4 changes: 3 additions & 1 deletion helpers/imap/on-move.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
const mongoose = require('mongoose');
const { Builder } = require('json-sql');
const { boolean } = require('boolean');
const { IMAPConnection } = require('wildduck/imap-core/lib/imap-connection');
const {
IMAPConnection
} = require('@forwardemail/wildduck/imap-core/lib/imap-connection');

const IMAPError = require('#helpers/imap-error');
const Mailboxes = require('#models/mailboxes');
Expand Down
2 changes: 1 addition & 1 deletion helpers/imap/on-search.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const { Buffer } = require('node:buffer');

const _ = require('lodash');
const ms = require('ms');
const tools = require('wildduck/lib/tools');
const tools = require('@forwardemail/wildduck/lib/tools');
const { Builder } = require('json-sql');

const IMAPError = require('#helpers/imap-error');
Expand Down
8 changes: 5 additions & 3 deletions helpers/imap/on-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
* https://github.com/nodemailer/wildduck
*/

const imapTools = require('wildduck/imap-core/lib/imap-tools');
const tools = require('wildduck/lib/tools');
const imapTools = require('@forwardemail/wildduck/imap-core/lib/imap-tools');
const tools = require('@forwardemail/wildduck/lib/tools');
const { Builder } = require('json-sql');
const { IMAPConnection } = require('wildduck/imap-core/lib/imap-connection');
const {
IMAPConnection
} = require('@forwardemail/wildduck/imap-core/lib/imap-connection');
const _ = require('lodash');

const IMAPError = require('#helpers/imap-error');
Expand Down
44 changes: 44 additions & 0 deletions helpers/imap/on-xapplepushservice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Copyright (c) Forward Email LLC
* SPDX-License-Identifier: BUSL-1.1
*/

const Aliases = require('#models/aliases');

const refineAndLogError = require('#helpers/refine-and-log-error');

// <https://github.com/nodemailer/wildduck/issues/711>
// eslint-disable-next-line max-params
async function onXAPPLEPUSHSERVICE(
accountID,
deviceToken,
subTopic,
mailboxes,
session,
fn
) {
this.logger.debug('XAPPLEPUSHSERVICE', {
accountID,
deviceToken,
subTopic,
mailboxes,
session
});
try {
await this.refreshSession(session, 'XAPPLEPUSHSERVICE');
// update the alias with the provided data
await Aliases.findByIdAndUpdate(session.user.alias_id, {
$set: {
aps_account_id: accountID,
aps_device_token: deviceToken,
aps_subtopic: subTopic,
aps_mailboxes: mailboxes
}
});
fn();
} catch (err) {
fn(refineAndLogError(err, session, true, this));
}
}

module.exports = onXAPPLEPUSHSERVICE;
4 changes: 3 additions & 1 deletion helpers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ const isRedisError = require('./is-redis-error');
const isMongoError = require('./is-mongo-error');
const syncUbuntuUser = require('./sync-ubuntu-user');
const getUbuntuMembersMap = require('./get-ubuntu-members-map');
const sendApn = require('./send-apn');

module.exports = {
decrypt,
Expand Down Expand Up @@ -190,5 +191,6 @@ module.exports = {
isRedisError,
isMongoError,
syncUbuntuUser,
getUbuntuMembersMap
getUbuntuMembersMap,
sendApn
};
2 changes: 1 addition & 1 deletion helpers/indexer.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
const { Buffer } = require('node:buffer');
const { PassThrough } = require('node:stream');

const WildDuckIndexer = require('wildduck/imap-core/lib/indexer/indexer');
const WildDuckIndexer = require('@forwardemail/wildduck/imap-core/lib/indexer/indexer');

const storeNodeBodies = require('#helpers/store-node-bodies');

Expand Down
4 changes: 2 additions & 2 deletions helpers/on-auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@

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

const POP3Server = require('wildduck/lib/pop3/server');
const POP3Server = require('@forwardemail/wildduck/lib/pop3/server');
const isSANB = require('is-string-and-not-blank');
const ms = require('ms');
const pify = require('pify');
const revHash = require('rev-hash');
const safeStringify = require('fast-safe-stringify');
const { IMAPServer } = require('wildduck/imap-core');
const { IMAPServer } = require('@forwardemail/wildduck/imap-core');
const { isEmail } = require('validator');
const _ = require('lodash');

Expand Down
6 changes: 6 additions & 0 deletions helpers/parse-payload.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const isRetryableError = require('#helpers/is-retryable-error');
const logger = require('#helpers/logger');
const parseRootDomain = require('#helpers/parse-root-domain');
const recursivelyParse = require('#helpers/recursively-parse');
const sendApn = require('#helpers/send-apn');
const syncTemporaryMailbox = require('#helpers/sync-temporary-mailbox');
const updateStorageUsed = require('#helpers/update-storage-used');
const { encoder, decoder } = require('#helpers/encoder-decoder');
Expand Down Expand Up @@ -1148,6 +1149,11 @@ async function parsePayload(data, ws) {
logger.fatal(err);
}

// send user push notification if applicable
sendApn(alias.id)
.then()
.catch((err) => logger.fatal(err, { session }));

//
// increase rate limiting size and count
//
Expand Down
3 changes: 2 additions & 1 deletion helpers/refresh-session.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ const IMAP_COMMANDS = new Set([
'STATUS',
'STORE',
'SUBSCRIBE',
'UNSUBSCRIBE'
'UNSUBSCRIBE',
'XAPPLEPUSHSERVICE'
]);

// eslint-disable-next-line complexity
Expand Down
52 changes: 52 additions & 0 deletions helpers/send-apn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Copyright (c) Forward Email LLC
* SPDX-License-Identifier: BUSL-1.1
*/

const apn = require('@parse/node-apn');
const dayjs = require('dayjs-with-plugins');

const Aliases = require('#models/aliases');
const config = require('#config');
const env = require('#config/env');
const logger = require('#helpers/logger');

const apnProvider = new apn.Provider({
token: {
key: env.APPLE_KEY_PATH,
keyId: env.APPLE_KEY_ID,
teamId: env.APPLE_TEAM_ID
},
production: config.env === 'production'
});

// <https://github.com/nodemailer/wildduck/issues/711>
async function sendApn(id) {
const alias = await Aliases.findOne({
id,
aps_account_id: { $exists: true },
aps_device_token: { $exists: true }
})
.lean()
.select('+aps_account_id +aps_device_doken')
.exec();

if (!alias) return;

const note = new apn.Notification();
note.topic = 'com.apple.mobilemail';
note.expiry = dayjs().add(1, 'day').toDate().getTime();
note.payload = {
'account-id': alias.aps_account_id
};

logger.debug('note', note);

// note they have commented out code at this below link for setting priority in note
// <https://github.com/freswa/dovecot-xaps-daemon/blob/abce2f14cf1b5afa56329ebb4d923c9c2aebdfe3/internal/apns.go#L162-L163>

const result = await apnProvider.send(note, alias.aps_device_token);
logger.debug('apnProvider.send', { result });
}

module.exports = sendApn;
Loading

0 comments on commit 6398425

Please sign in to comment.