diff --git a/config/imap.toml b/config/imap.toml index 7fccc8f5..f7fde20a 100644 --- a/config/imap.toml +++ b/config/imap.toml @@ -54,6 +54,11 @@ ignoredHosts = [] #secure=false #ignoreSTARTTLS=true +# Apple push notificiations +# TODO: missing actual implementation for Apple Push Service +[aps] +enabled = false + [setup] # Public configuration for IMAP hostname = "localhost" diff --git a/imap-core/lib/commands/xapplepushservice.js b/imap-core/lib/commands/xapplepushservice.js index 158db1cf..1c37f4e4 100644 --- a/imap-core/lib/commands/xapplepushservice.js +++ b/imap-core/lib/commands/xapplepushservice.js @@ -7,143 +7,178 @@ // tag XAPPLEPUSHSERVICE aps-version 2 aps-account-id 0715A26B-CA09-4730-A419-793000CA982E aps-device-token 2918390218931890821908309283098109381029309829018310983092892829 aps-subtopic com.apple.mobilemail mailboxes (INBOX Notes) // +const requiredKeys = ['aps-version', 'aps-account-id', 'aps-device-token', 'aps-subtopic', 'mailboxes']; + module.exports = { state: ['Authenticated', 'Selected'], - /* - Schema: [ - { - name: 'aps-version', - type: 'number' // always 2 - }, - { - name: 'aps-account-id', - type: 'string' - }, - { - name: 'aps-device-token', - type: 'string' - }, - { - name: 'aps-subtopic', - type: 'string' // always "com.apple.mobilemail" - }, - // NOTE: this is irrelevant as it won't be used until we figure out how to notify for other than INBOX - // - { - name: 'mailboxes', - type: 'string' // e.g. (INBOX Notes) + // the input is a key-value set which is not supported by the default schema handler + schema: false, + + // [ + // { type: 'ATOM', value: 'aps-version' }, + // { type: 'ATOM', value: '2' }, + // { type: 'ATOM', value: 'aps-account-id' }, + // { type: 'ATOM', value: 'xxxxxxx' }, + // { type: 'ATOM', value: 'aps-device-token' }, + // { + // type: 'ATOM', + // value: 'xxxxxx' + // }, + // { type: 'ATOM', value: 'aps-subtopic' }, + // { type: 'ATOM', value: 'com.apple.mobilemail' }, + // { type: 'ATOM', value: 'mailboxes' }, + // [ + // { type: 'STRING', value: 'Sent Mail' }, + // { type: 'STRING', value: 'INBOX' } + // ] + // ] + + handler(command, callback) { + // Command = { + // tag: 'I5', + // command: 'XAPPLEPUSHSERVICE', + // attributes: [ + // { type: 'ATOM', value: 'aps-version' }, // 0 + // { type: 'ATOM', value: '2' }, // 1 + // { type: 'ATOM', value: 'aps-account-id' }, // 2 + // { type: 'ATOM', value: 'xxxxxx' }, // 3 + // { type: 'ATOM', value: 'aps-device-token' }, // 4 + // { // 5 + // type: 'ATOM', + // value: 'xxxxxx' + // }, + // { type: 'ATOM', value: 'aps-subtopic' }, // 6 + // { type: 'ATOM', value: 'com.apple.mobilemail' }, // 7 + // { type: 'ATOM', value: 'mailboxes' }, // 8 + // [ // 9 + // { type: 'STRING', value: 'Sent Mail' }, + // { type: 'STRING', value: 'INBOX' } + // ] + // ] + // } + + const apsConfig = this._server.options.aps || {}; + + // Reject if not enabled + if (!apsConfig.enabled) { + return callback(null, { + response: 'BAD', + message: `Unknown command: ${command.command}` + }); } - ], - */ - // it's actually something like this in production - // [ - // { type: 'ATOM', value: 'aps-version' }, - // { type: 'ATOM', value: '2' }, - // { type: 'ATOM', value: 'aps-account-id' }, - // { type: 'ATOM', value: 'xxxxxxx' }, - // { type: 'ATOM', value: 'aps-device-token' }, - // { - // type: 'ATOM', - // value: 'xxxxxx' - // }, - // { type: 'ATOM', value: 'aps-subtopic' }, - // { type: 'ATOM', value: 'com.apple.mobilemail' }, - // { type: 'ATOM', value: 'mailboxes' }, - // [ - // { type: 'STRING', value: 'Sent Mail' }, - // { type: 'STRING', value: 'INBOX' } - // ] - // ] + // Parse input arguments into a structured object: - // disabled for now - schema: false, + // { + // "aps-version": "2", + // "aps-account-id": "0715A26B-CA09-4730-A419-793000CA982E", + // "aps-device-token": "2918390218931890821908309283098109381029309829018310983092892829", + // "aps-subtopic": "com.apple.mobilemail", + // "mailboxes": [ + // "INBOX", + // "Notes" + // ] + // } - handler(command, callback) { - // Command = { - // tag: 'I5', - // command: 'XAPPLEPUSHSERVICE', - // attributes: [ - // { type: 'ATOM', value: 'aps-version' }, // 0 - // { type: 'ATOM', value: '2' }, // 1 - // { type: 'ATOM', value: 'aps-account-id' }, // 2 - // { type: 'ATOM', value: 'xxxxxx' }, // 3 - // { type: 'ATOM', value: 'aps-device-token' }, // 4 - // { // 5 - // type: 'ATOM', - // value: 'xxxxxx' - // }, - // { type: 'ATOM', value: 'aps-subtopic' }, // 6 - // { type: 'ATOM', value: 'com.apple.mobilemail' }, // 7 - // { type: 'ATOM', value: 'mailboxes' }, // 8 - // [ // 9 - // { type: 'STRING', value: 'Sent Mail' }, - // { type: 'STRING', value: 'INBOX' } - // ] - // ] - // } + let data = {}; + let keyName; + for (let i = 0, len = (command.attributes || []).length; i < len; i++) { + let isKey = i % 2 === 0; + let attr = command.attributes[i]; + if (isKey && !['ATOM', 'STRING'].includes(attr.type)) { + return callback(null, { + response: 'BAD', + message: `Invalid argument for ${command.command}` + }); + } + if (isKey) { + keyName = (attr.value || '').toString().toLowerCase(); + continue; + } - const version = (command.attributes[1] && command.attributes[1].value) || ''; - if (version !== '2') { - return callback(null, { - response: 'NO', - code: 'CLIENTBUG', - }); - } + if (!requiredKeys.includes(keyName)) { + // skip unknown keys + } - const accountID = (command.attributes[3] && command.attributes[3].value) || ''; - const deviceToken = (command.attributes[5] && command.attributes[5].value) || ''; - const subTopic = (command.attributes[7] && command.attributes[7].value) || ''; + if (['ATOM', 'STRING'].includes(attr.type)) { + data[keyName] = (attr.value || '').toString(); + } else if (Array.isArray(attr) && keyName === 'mailboxes') { + let mailboxes = attr + .map(entry => { + if (['ATOM', 'STRING'].includes(entry.type)) { + return (entry.value || '').toString(); + } + return false; + }) + .filter(name => name); + data[keyName] = mailboxes; + } + } - if (subTopic !== 'com.apple.mobilemail') { - return callback(null, { - response: 'NO', - code: 'CLIENTBUG', - }); - } + // Make sure all required keys (exept mailboxes) are present + for (let requiredKey of requiredKeys) { + if (!data[requiredKey] && requiredKey !== 'mailboxes') { + return callback(null, { + response: 'BAD', + message: `Missing required arguments for ${command.command}` + }); + } + } - // NOTE: mailboxes param is not used at this time (it's a list anyways too) - const mailboxes = command.attributes[9] && Array.isArray(command.attributes[9]) && command.attributes[9].length > 0 ? command.attributes[9].map(object => object.value) : []; + const version = data['aps-version']; + const accountID = data['aps-account-id']; + const deviceToken = data['aps-device-token']; + const subTopic = data['aps-subtopic']; + const mailboxes = data.mailboxes || []; - if (typeof this._server.onXAPPLEPUSHSERVICE !== 'function') { - return callback(null, { - response: 'NO', - message: command.command + ' not implemented', - }); - } + if (version !== '2') { + return callback(null, { + response: 'NO', + message: 'Unsupported APS version', + code: 'CLIENTBUG' + }); + } - const logdata = { - short_message: '[XAPPLEPUSHSERVICE]', - _mail_action: 'xapplepushservice', - _accountId: accountID, - _deviceToken: deviceToken, - _subTopic: subTopic, - _mailboxes: mailboxes, - _user: this.session.user.id.toString(), - _sess: this.id, - }; + if (subTopic !== 'com.apple.mobilemail') { + return callback(null, { + response: 'NO', + message: `Invalid subtopic for ${command.command}`, + code: 'CLIENTBUG' + }); + } - this._server.onXAPPLEPUSHSERVICE(accountID, deviceToken, subTopic, mailboxes, this.session, error => { - if (error) { - logdata._error = error.message; - logdata._code = error.code; - logdata._response = error.response; - this._server.loggelf(logdata); + const logdata = { + short_message: '[XAPPLEPUSHSERVICE]', + _mail_action: 'xapplepushservice', + _accountId: accountID, + _deviceToken: deviceToken, + _subTopic: subTopic, + _mailboxes: mailboxes, + _user: this.session.user.id.toString(), + _sess: this.id + }; - return callback(null, { - response: 'NO', - code: 'TEMPFAIL', - }); - } + this._server.onXAPPLEPUSHSERVICE(accountID, deviceToken, subTopic, mailboxes, this.session, error => { + if (error) { + logdata._error = error.message; + logdata._code = error.code; + logdata._response = error.response; + this._server.loggelf(logdata); - // - // - this.send(`* XAPPLEPUSHSERVICE aps-version "${version}" aps-topic "${subTopic}"`); - callback(null, { - response: 'OK', - message: 'XAPPLEPUSHSERVICE Registration successful.' - }); - }); - }, + return callback(null, { + response: 'NO', + code: 'TEMPFAIL' + }); + } + + // + // + this.send(`* XAPPLEPUSHSERVICE aps-version "${version}" aps-topic "${subTopic}"`); + callback(null, { + response: 'OK', + message: 'XAPPLEPUSHSERVICE Registration successful.' + }); + }); + } }; diff --git a/imap-core/lib/imap-tools.js b/imap-core/lib/imap-tools.js index dfe284a1..daa6e27e 100644 --- a/imap-core/lib/imap-tools.js +++ b/imap-core/lib/imap-tools.js @@ -717,9 +717,6 @@ module.exports.getQueryResponse = function (query, message, options) { module.exports.sendCapabilityResponse = connection => { let capabilities = []; - if (typeof connection._server.onXAPPLEPUSHSERVICE === 'function') - capabilities.push('XAPPLEPUSHSERVICE'); - if (!connection.secure) { if (!connection._server.options.disableSTARTTLS) { capabilities.push('STARTTLS'); @@ -766,6 +763,10 @@ module.exports.sendCapabilityResponse = connection => { if (connection._server.options.maxMessage) { capabilities.push('APPENDLIMIT=' + connection._server.options.maxMessage); } + + if (connection._server.options.aps?.enabled) { + capabilities.push('XAPPLEPUSHSERVICE'); + } } capabilities.sort((a, b) => a.localeCompare(b)); diff --git a/imap.js b/imap.js index 9a862290..065e27d8 100644 --- a/imap.js +++ b/imap.js @@ -36,7 +36,7 @@ const onMove = require('./lib/handlers/on-move'); const onSearch = require('./lib/handlers/on-search'); const onGetQuotaRoot = require('./lib/handlers/on-get-quota-root'); const onGetQuota = require('./lib/handlers/on-get-quota'); -// const onXAPPLEPUSHSERVICE = require('./lib/handlers/on-xapplepushservice'); +const onXAPPLEPUSHSERVICE = require('./lib/handlers/on-xapplepushservice'); let logger = { info(...args) { @@ -78,6 +78,8 @@ let createInterface = (ifaceOptions, callback) => { vendor: config.imap.vendor || 'Kreata' }, + aps: config.imap.aps, + logger, maxMessage: config.imap.maxMB * 1024 * 1024, @@ -157,7 +159,7 @@ let createInterface = (ifaceOptions, callback) => { server.onSearch = onSearch(server); server.onGetQuotaRoot = onGetQuotaRoot(server); server.onGetQuota = onGetQuota(server); - // server.onXAPPLEPUSHSERVICE = onXAPPLEPUSHSERVICE(server); + server.onXAPPLEPUSHSERVICE = onXAPPLEPUSHSERVICE(server); if (loggelf) { server.loggelf = loggelf; diff --git a/lib/handlers/on-xapplepushservice.js b/lib/handlers/on-xapplepushservice.js index 80d88832..13799874 100644 --- a/lib/handlers/on-xapplepushservice.js +++ b/lib/handlers/on-xapplepushservice.js @@ -6,6 +6,12 @@ // // tag XAPPLEPUSHSERVICE aps-version 2 aps-account-id 0715A26B-CA09-4730-A419-793000CA982E aps-device-token 2918390218931890821908309283098109381029309829018310983092892829 aps-subtopic com.apple.mobilemail mailboxes (INBOX Notes) // + +// TODO: +// 1. store APS information in DB, each deviceToken separately +// 2. on new email use the stored information to push to apple (use mathcing deviceTokens as an array of recipients) +// 3. if pushing to a specific deviceToken yields in 410, remove that token + module.exports = server => (accountID, deviceToken, subTopic, mailboxes, session, callback) => { server.logger.debug( { @@ -19,5 +25,6 @@ module.exports = server => (accountID, deviceToken, subTopic, mailboxes, session subTopic, mailboxes ); + return callback(new Error('Not implemented, see ')); };