Skip to content

Commit 4ef3eaa

Browse files
committed
Updated XAPPLEPUSHSERVICE schema handling
1 parent 4e96db2 commit 4ef3eaa

File tree

5 files changed

+190
-140
lines changed

5 files changed

+190
-140
lines changed

config/imap.toml

+5
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ ignoredHosts = []
5454
#secure=false
5555
#ignoreSTARTTLS=true
5656

57+
# Apple push notificiations
58+
# TODO: missing actual implementation for Apple Push Service
59+
[aps]
60+
enabled = false
61+
5762
[setup]
5863
# Public configuration for IMAP
5964
hostname = "localhost"

imap-core/lib/commands/xapplepushservice.js

+170-135
Original file line numberDiff line numberDiff line change
@@ -7,143 +7,178 @@
77
// 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)
88
//
99

10+
const requiredKeys = ['aps-version', 'aps-account-id', 'aps-device-token', 'aps-subtopic', 'mailboxes'];
11+
1012
module.exports = {
1113
state: ['Authenticated', 'Selected'],
1214

13-
/*
14-
Schema: [
15-
{
16-
name: 'aps-version',
17-
type: 'number' // always 2
18-
},
19-
{
20-
name: 'aps-account-id',
21-
type: 'string'
22-
},
23-
{
24-
name: 'aps-device-token',
25-
type: 'string'
26-
},
27-
{
28-
name: 'aps-subtopic',
29-
type: 'string' // always "com.apple.mobilemail"
30-
},
31-
// NOTE: this is irrelevant as it won't be used until we figure out how to notify for other than INBOX
32-
// <https://github.com/nodemailer/wildduck/issues/711#issuecomment-2251643672>
33-
{
34-
name: 'mailboxes',
35-
type: 'string' // e.g. (INBOX Notes)
15+
// the input is a key-value set which is not supported by the default schema handler
16+
schema: false,
17+
18+
// [
19+
// { type: 'ATOM', value: 'aps-version' },
20+
// { type: 'ATOM', value: '2' },
21+
// { type: 'ATOM', value: 'aps-account-id' },
22+
// { type: 'ATOM', value: 'xxxxxxx' },
23+
// { type: 'ATOM', value: 'aps-device-token' },
24+
// {
25+
// type: 'ATOM',
26+
// value: 'xxxxxx'
27+
// },
28+
// { type: 'ATOM', value: 'aps-subtopic' },
29+
// { type: 'ATOM', value: 'com.apple.mobilemail' },
30+
// { type: 'ATOM', value: 'mailboxes' },
31+
// [
32+
// { type: 'STRING', value: 'Sent Mail' },
33+
// { type: 'STRING', value: 'INBOX' }
34+
// ]
35+
// ]
36+
37+
handler(command, callback) {
38+
// Command = {
39+
// tag: 'I5',
40+
// command: 'XAPPLEPUSHSERVICE',
41+
// attributes: [
42+
// { type: 'ATOM', value: 'aps-version' }, // 0
43+
// { type: 'ATOM', value: '2' }, // 1
44+
// { type: 'ATOM', value: 'aps-account-id' }, // 2
45+
// { type: 'ATOM', value: 'xxxxxx' }, // 3
46+
// { type: 'ATOM', value: 'aps-device-token' }, // 4
47+
// { // 5
48+
// type: 'ATOM',
49+
// value: 'xxxxxx'
50+
// },
51+
// { type: 'ATOM', value: 'aps-subtopic' }, // 6
52+
// { type: 'ATOM', value: 'com.apple.mobilemail' }, // 7
53+
// { type: 'ATOM', value: 'mailboxes' }, // 8
54+
// [ // 9
55+
// { type: 'STRING', value: 'Sent Mail' },
56+
// { type: 'STRING', value: 'INBOX' }
57+
// ]
58+
// ]
59+
// }
60+
61+
const apsConfig = this._server.options.aps || {};
62+
63+
// Reject if not enabled
64+
if (!apsConfig.enabled) {
65+
return callback(null, {
66+
response: 'BAD',
67+
message: `Unknown command: ${command.command}`
68+
});
69+
}
70+
71+
// Parse input arguments into a structured object:
72+
73+
// {
74+
// "aps-version": "2",
75+
// "aps-account-id": "0715A26B-CA09-4730-A419-793000CA982E",
76+
// "aps-device-token": "2918390218931890821908309283098109381029309829018310983092892829",
77+
// "aps-subtopic": "com.apple.mobilemail",
78+
// "mailboxes": [
79+
// "INBOX",
80+
// "Notes"
81+
// ]
82+
// }
83+
84+
let data = {};
85+
let keyName;
86+
for (let i = 0, len = (command.attributes || []).length; i < len; i++) {
87+
let isKey = i % 2 === 0;
88+
let attr = command.attributes[i];
89+
if (isKey && !['ATOM', 'STRING'].includes(attr.type)) {
90+
return callback(null, {
91+
response: 'BAD',
92+
message: `Invalid argument for ${command.command}`
93+
});
94+
}
95+
if (isKey) {
96+
keyName = (attr.value || '').toString().toLowerCase();
97+
continue;
98+
}
99+
100+
if (!requiredKeys.includes(keyName)) {
101+
// skip unknown keys
102+
}
103+
104+
if (['ATOM', 'STRING'].includes(attr.type)) {
105+
data[keyName] = (attr.value || '').toString();
106+
} else if (Array.isArray(attr) && keyName === 'mailboxes') {
107+
let mailboxes = attr
108+
.map(entry => {
109+
if (['ATOM', 'STRING'].includes(entry.type)) {
110+
return (entry.value || '').toString();
111+
}
112+
return false;
113+
})
114+
.filter(name => name);
115+
data[keyName] = mailboxes;
116+
}
36117
}
37-
],
38-
*/
39-
40-
// it's actually something like this in production
41-
// [
42-
// { type: 'ATOM', value: 'aps-version' },
43-
// { type: 'ATOM', value: '2' },
44-
// { type: 'ATOM', value: 'aps-account-id' },
45-
// { type: 'ATOM', value: 'xxxxxxx' },
46-
// { type: 'ATOM', value: 'aps-device-token' },
47-
// {
48-
// type: 'ATOM',
49-
// value: 'xxxxxx'
50-
// },
51-
// { type: 'ATOM', value: 'aps-subtopic' },
52-
// { type: 'ATOM', value: 'com.apple.mobilemail' },
53-
// { type: 'ATOM', value: 'mailboxes' },
54-
// [
55-
// { type: 'STRING', value: 'Sent Mail' },
56-
// { type: 'STRING', value: 'INBOX' }
57-
// ]
58-
// ]
59-
60-
// disabled for now
61-
schema: false,
62-
63-
handler(command, callback) {
64-
// Command = {
65-
// tag: 'I5',
66-
// command: 'XAPPLEPUSHSERVICE',
67-
// attributes: [
68-
// { type: 'ATOM', value: 'aps-version' }, // 0
69-
// { type: 'ATOM', value: '2' }, // 1
70-
// { type: 'ATOM', value: 'aps-account-id' }, // 2
71-
// { type: 'ATOM', value: 'xxxxxx' }, // 3
72-
// { type: 'ATOM', value: 'aps-device-token' }, // 4
73-
// { // 5
74-
// type: 'ATOM',
75-
// value: 'xxxxxx'
76-
// },
77-
// { type: 'ATOM', value: 'aps-subtopic' }, // 6
78-
// { type: 'ATOM', value: 'com.apple.mobilemail' }, // 7
79-
// { type: 'ATOM', value: 'mailboxes' }, // 8
80-
// [ // 9
81-
// { type: 'STRING', value: 'Sent Mail' },
82-
// { type: 'STRING', value: 'INBOX' }
83-
// ]
84-
// ]
85-
// }
86-
87-
const version = (command.attributes[1] && command.attributes[1].value) || '';
88-
if (version !== '2') {
89-
return callback(null, {
90-
response: 'NO',
91-
code: 'CLIENTBUG',
92-
});
93-
}
94-
95-
const accountID = (command.attributes[3] && command.attributes[3].value) || '';
96-
const deviceToken = (command.attributes[5] && command.attributes[5].value) || '';
97-
const subTopic = (command.attributes[7] && command.attributes[7].value) || '';
98-
99-
if (subTopic !== 'com.apple.mobilemail') {
100-
return callback(null, {
101-
response: 'NO',
102-
code: 'CLIENTBUG',
103-
});
104-
}
105-
106-
// NOTE: mailboxes param is not used at this time (it's a list anyways too)
107-
const mailboxes = command.attributes[9] && Array.isArray(command.attributes[9]) && command.attributes[9].length > 0 ? command.attributes[9].map(object => object.value) : [];
108-
109-
if (typeof this._server.onXAPPLEPUSHSERVICE !== 'function') {
110-
return callback(null, {
111-
response: 'NO',
112-
message: command.command + ' not implemented',
113-
});
114-
}
115-
116-
const logdata = {
117-
short_message: '[XAPPLEPUSHSERVICE]',
118-
_mail_action: 'xapplepushservice',
119-
_accountId: accountID,
120-
_deviceToken: deviceToken,
121-
_subTopic: subTopic,
122-
_mailboxes: mailboxes,
123-
_user: this.session.user.id.toString(),
124-
_sess: this.id,
125-
};
126-
127-
this._server.onXAPPLEPUSHSERVICE(accountID, deviceToken, subTopic, mailboxes, this.session, error => {
128-
if (error) {
129-
logdata._error = error.message;
130-
logdata._code = error.code;
131-
logdata._response = error.response;
132-
this._server.loggelf(logdata);
133-
134-
return callback(null, {
135-
response: 'NO',
136-
code: 'TEMPFAIL',
137-
});
138-
}
139-
140-
// <https://opensource.apple.com/source/dovecot/dovecot-293/dovecot/src/imap/cmd-x-apple-push-service.c.auto.html>
141-
// <https://github.com/st3fan/dovecot-xaps-plugin/blob/3d1c71e0c78cc35ca6ead21f49a8e0e35e948a7c/xaps-imap-plugin.c#L158-L166>
142-
this.send(`* XAPPLEPUSHSERVICE aps-version "${version}" aps-topic "${subTopic}"`);
143-
callback(null, {
144-
response: 'OK',
145-
message: 'XAPPLEPUSHSERVICE Registration successful.'
146-
});
147-
});
148-
},
118+
119+
// Make sure all required keys (exept mailboxes) are present
120+
for (let requiredKey of requiredKeys) {
121+
if (!data[requiredKey] && requiredKey !== 'mailboxes') {
122+
return callback(null, {
123+
response: 'BAD',
124+
message: `Missing required arguments for ${command.command}`
125+
});
126+
}
127+
}
128+
129+
const version = data['aps-version'];
130+
const accountID = data['aps-account-id'];
131+
const deviceToken = data['aps-device-token'];
132+
const subTopic = data['aps-subtopic'];
133+
const mailboxes = data.mailboxes || [];
134+
135+
if (version !== '2') {
136+
return callback(null, {
137+
response: 'NO',
138+
message: 'Unsupported APS version',
139+
code: 'CLIENTBUG'
140+
});
141+
}
142+
143+
if (subTopic !== 'com.apple.mobilemail') {
144+
return callback(null, {
145+
response: 'NO',
146+
message: `Invalid subtopic for ${command.command}`,
147+
code: 'CLIENTBUG'
148+
});
149+
}
150+
151+
const logdata = {
152+
short_message: '[XAPPLEPUSHSERVICE]',
153+
_mail_action: 'xapplepushservice',
154+
_accountId: accountID,
155+
_deviceToken: deviceToken,
156+
_subTopic: subTopic,
157+
_mailboxes: mailboxes,
158+
_user: this.session.user.id.toString(),
159+
_sess: this.id
160+
};
161+
162+
this._server.onXAPPLEPUSHSERVICE(accountID, deviceToken, subTopic, mailboxes, this.session, error => {
163+
if (error) {
164+
logdata._error = error.message;
165+
logdata._code = error.code;
166+
logdata._response = error.response;
167+
this._server.loggelf(logdata);
168+
169+
return callback(null, {
170+
response: 'NO',
171+
code: 'TEMPFAIL'
172+
});
173+
}
174+
175+
// <https://opensource.apple.com/source/dovecot/dovecot-293/dovecot/src/imap/cmd-x-apple-push-service.c.auto.html>
176+
// <https://github.com/st3fan/dovecot-xaps-plugin/blob/3d1c71e0c78cc35ca6ead21f49a8e0e35e948a7c/xaps-imap-plugin.c#L158-L166>
177+
this.send(`* XAPPLEPUSHSERVICE aps-version "${version}" aps-topic "${subTopic}"`);
178+
callback(null, {
179+
response: 'OK',
180+
message: 'XAPPLEPUSHSERVICE Registration successful.'
181+
});
182+
});
183+
}
149184
};

imap-core/lib/imap-tools.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -717,9 +717,6 @@ module.exports.getQueryResponse = function (query, message, options) {
717717
module.exports.sendCapabilityResponse = connection => {
718718
let capabilities = [];
719719

720-
if (typeof connection._server.onXAPPLEPUSHSERVICE === 'function')
721-
capabilities.push('XAPPLEPUSHSERVICE');
722-
723720
if (!connection.secure) {
724721
if (!connection._server.options.disableSTARTTLS) {
725722
capabilities.push('STARTTLS');
@@ -766,6 +763,10 @@ module.exports.sendCapabilityResponse = connection => {
766763
if (connection._server.options.maxMessage) {
767764
capabilities.push('APPENDLIMIT=' + connection._server.options.maxMessage);
768765
}
766+
767+
if (connection._server.options.aps?.enabled) {
768+
capabilities.push('XAPPLEPUSHSERVICE');
769+
}
769770
}
770771

771772
capabilities.sort((a, b) => a.localeCompare(b));

imap.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const onMove = require('./lib/handlers/on-move');
3636
const onSearch = require('./lib/handlers/on-search');
3737
const onGetQuotaRoot = require('./lib/handlers/on-get-quota-root');
3838
const onGetQuota = require('./lib/handlers/on-get-quota');
39-
// const onXAPPLEPUSHSERVICE = require('./lib/handlers/on-xapplepushservice');
39+
const onXAPPLEPUSHSERVICE = require('./lib/handlers/on-xapplepushservice');
4040

4141
let logger = {
4242
info(...args) {
@@ -78,6 +78,8 @@ let createInterface = (ifaceOptions, callback) => {
7878
vendor: config.imap.vendor || 'Kreata'
7979
},
8080

81+
aps: config.imap.aps,
82+
8183
logger,
8284

8385
maxMessage: config.imap.maxMB * 1024 * 1024,
@@ -157,7 +159,7 @@ let createInterface = (ifaceOptions, callback) => {
157159
server.onSearch = onSearch(server);
158160
server.onGetQuotaRoot = onGetQuotaRoot(server);
159161
server.onGetQuota = onGetQuota(server);
160-
// server.onXAPPLEPUSHSERVICE = onXAPPLEPUSHSERVICE(server);
162+
server.onXAPPLEPUSHSERVICE = onXAPPLEPUSHSERVICE(server);
161163

162164
if (loggelf) {
163165
server.loggelf = loggelf;

lib/handlers/on-xapplepushservice.js

+7
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66
// <https://github.com/nodemailer/wildduck/issues/711>
77
// 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)
88
//
9+
10+
// TODO:
11+
// 1. store APS information in DB, each deviceToken separately
12+
// 2. on new email use the stored information to push to apple (use mathcing deviceTokens as an array of recipients)
13+
// 3. if pushing to a specific deviceToken yields in 410, remove that token
14+
915
module.exports = server => (accountID, deviceToken, subTopic, mailboxes, session, callback) => {
1016
server.logger.debug(
1117
{
@@ -19,5 +25,6 @@ module.exports = server => (accountID, deviceToken, subTopic, mailboxes, session
1925
subTopic,
2026
mailboxes
2127
);
28+
2229
return callback(new Error('Not implemented, see <https://github.com/nodemailer/wildduck/issues/711>'));
2330
};

0 commit comments

Comments
 (0)