mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 08:06:36 +00:00
fix: say the mail server refuses push relays that are not on a public domain
This commit is contained in:
@@ -1058,6 +1058,7 @@
|
|||||||
"err_relay_rejected": "The push relay rejected the registration: {detail}",
|
"err_relay_rejected": "The push relay rejected the registration: {detail}",
|
||||||
"err_relay_unreachable": "Could not reach the push relay at {url} ({detail}).",
|
"err_relay_unreachable": "Could not reach the push relay at {url} ({detail}).",
|
||||||
"err_up_android_only": "UnifiedPush is only available on Android.",
|
"err_up_android_only": "UnifiedPush is only available on Android.",
|
||||||
|
"err_url_refused": "The mail server refuses to send notifications to the relay at {url}. It only accepts https addresses on a public domain name, not a private IP address or a local name such as localhost, .local or .lan. Use the hosted relay, or give your relay a public domain name.",
|
||||||
"err_verify_rejected": "The mail server rejected the verification code: {detail}",
|
"err_verify_rejected": "The mail server rejected the verification code: {detail}",
|
||||||
"err_verify_relay": "The relay never received the verification code from the mail server (last relay response: {detail}).",
|
"err_verify_relay": "The relay never received the verification code from the mail server (last relay response: {detail}).",
|
||||||
"err_verify_timeout": "The mail server did not send a verification code to the relay within 75 s. Check that the server can reach the relay URL.",
|
"err_verify_timeout": "The mail server did not send a verification code to the relay within 75 s. Check that the server can reach the relay URL.",
|
||||||
|
|||||||
@@ -197,7 +197,8 @@ export function NotificationSettings() {
|
|||||||
message: t('settings.notifications.push.status_busy', 'Working…'),
|
message: t('settings.notifications.push.status_busy', 'Working…'),
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await setStoredRelayBaseUrl(trimmed);
|
// The setup stores the relay itself, and keeps the one in use when the
|
||||||
|
// mail server refuses the new one's address.
|
||||||
await setupPushNotifications({
|
await setupPushNotifications({
|
||||||
relayBaseUrl: trimmed,
|
relayBaseUrl: trimmed,
|
||||||
accountLabel: username ?? undefined,
|
accountLabel: username ?? undefined,
|
||||||
|
|||||||
@@ -371,6 +371,45 @@ describe('setupPushNotifications subscription shape', () => {
|
|||||||
expect(result.subscriptionId).toBe('new-server-id');
|
expect(result.subscriptionId).toBe('new-server-id');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('says the mail server refuses a relay that is not on a public domain (Stalwart 1.0)', async () => {
|
||||||
|
const relay = 'https://relay.home.lan';
|
||||||
|
createMock.mockRejectedValueOnce(new JMAPSetError(
|
||||||
|
'invalidProperties',
|
||||||
|
'Failed to create push subscription new: invalidProperties – properties=[url] – Host is not a public domain name',
|
||||||
|
['url'],
|
||||||
|
));
|
||||||
|
const err = await setupPushNotifications({ relayBaseUrl: relay }).catch((e) => e);
|
||||||
|
expect(err).toBeInstanceOf(PushSetupError);
|
||||||
|
expect(err.phase).toBe('jmap');
|
||||||
|
expect(err.message).toContain(relay);
|
||||||
|
expect(err.message).toContain('public domain name');
|
||||||
|
expect(err.message).not.toContain('properties=[url]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the relay a working registration goes through when the server refuses the new one', async () => {
|
||||||
|
await AsyncStorage.setItem('push:relayBaseUrl:v1', RELAY);
|
||||||
|
await AsyncStorage.setItem(SUB_KEY, 'existing');
|
||||||
|
listMock.mockResolvedValue([{ id: 'existing', deviceClientId: OUR_DCID, expires: new Date(Date.now() + 5 * 86400000).toISOString(), types: ['EmailDelivery'] }]);
|
||||||
|
createMock.mockRejectedValueOnce(new JMAPSetError('invalidProperties', 'Host is not a public domain name', ['url']));
|
||||||
|
const fetchMock = global.fetch as ReturnType<typeof vi.fn>;
|
||||||
|
|
||||||
|
const err = await setupPushNotifications({ relayBaseUrl: 'https://relay.home.lan', forceRecreate: true }).catch((e) => e);
|
||||||
|
|
||||||
|
expect(err).toBeInstanceOf(PushSetupError);
|
||||||
|
expect(await AsyncStorage.getItem('push:relayBaseUrl:v1')).toBe(RELAY);
|
||||||
|
expect(await AsyncStorage.getItem(SUB_KEY)).toBe('existing');
|
||||||
|
expect(destroyMock).not.toHaveBeenCalledWith('existing');
|
||||||
|
const deleted = fetchMock.mock.calls.filter((c) => (c[1] as RequestInit | undefined)?.method === 'DELETE').map((c) => String(c[0]));
|
||||||
|
expect(deleted).toEqual([`https://relay.home.lan/api/push/register/${OUR_DCID}`]);
|
||||||
|
|
||||||
|
// The same relay refused (a server that tightened its rules) stays as it is.
|
||||||
|
fetchMock.mockClear();
|
||||||
|
createMock.mockRejectedValueOnce(new JMAPSetError('invalidProperties', 'Host is not a public domain name', ['url']));
|
||||||
|
await setupPushNotifications({ relayBaseUrl: RELAY, forceRecreate: true }).catch(() => undefined);
|
||||||
|
expect(await AsyncStorage.getItem('push:relayBaseUrl:v1')).toBe(RELAY);
|
||||||
|
expect(fetchMock.mock.calls.some((c) => (c[1] as RequestInit | undefined)?.method === 'DELETE')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('rejects plain-http relay URLs', async () => {
|
it('rejects plain-http relay URLs', async () => {
|
||||||
await expect(setupPushNotifications({ relayBaseUrl: 'http://relay.example.com' })).rejects.toMatchObject({ phase: 'relay' });
|
await expect(setupPushNotifications({ relayBaseUrl: 'http://relay.example.com' })).rejects.toMatchObject({ phase: 'relay' });
|
||||||
expect(createMock).not.toHaveBeenCalled();
|
expect(createMock).not.toHaveBeenCalled();
|
||||||
|
|||||||
@@ -684,10 +684,30 @@ export class PushSetupError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the server refused the push address itself. Stalwart checks the
|
||||||
|
* URL when a subscription is created: 0.16.27 and 1.0 refuse plain http and
|
||||||
|
* private or reserved IP addresses, and 1.0 also refuses any host that is not
|
||||||
|
* a public domain name (localhost, .local, .lan, .internal, a single label).
|
||||||
|
*/
|
||||||
|
function isRefusedPushUrl(err: unknown): boolean {
|
||||||
|
return isJMAPSetError(err) && err.type === 'invalidProperties' && !!err.properties?.includes('url');
|
||||||
|
}
|
||||||
|
|
||||||
// A subscription write the mail server refused or could not take. The
|
// A subscription write the mail server refused or could not take. The
|
||||||
// refusals a user can do something about read as plain text; anything else
|
// refusals a user can do something about read as plain text; anything else
|
||||||
// keeps the error's own message.
|
// keeps the error's own message.
|
||||||
function subscriptionError(err: unknown): PushSetupError {
|
function subscriptionError(err: unknown, relayBaseUrl: string): PushSetupError {
|
||||||
|
if (isRefusedPushUrl(err)) {
|
||||||
|
return new PushSetupError(
|
||||||
|
'jmap',
|
||||||
|
t(
|
||||||
|
'settings.notifications.push.err_url_refused',
|
||||||
|
'The mail server refuses to send notifications to the relay at {url}. It only accepts https addresses on a public domain name, not a private IP address or a local name such as localhost, .local or .lan. Use the hosted relay, or give your relay a public domain name.',
|
||||||
|
{ url: relayBaseUrl },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
const type = err instanceof JMAPMethodError ? err.type : undefined;
|
const type = err instanceof JMAPMethodError ? err.type : undefined;
|
||||||
if (type === 'forbidden') {
|
if (type === 'forbidden') {
|
||||||
return new PushSetupError(
|
return new PushSetupError(
|
||||||
@@ -1204,6 +1224,9 @@ async function setupPushNotificationsInner(
|
|||||||
await migrateLegacyPushKeys();
|
await migrateLegacyPushKeys();
|
||||||
|
|
||||||
const deviceClientId = await getOrCreateDeviceClientId(accountId);
|
const deviceClientId = await getOrCreateDeviceClientId(accountId);
|
||||||
|
// The relay a working registration goes through, put back should the mail
|
||||||
|
// server refuse the new one's address.
|
||||||
|
const previousRelayBaseUrl = await getStoredRelayBaseUrl();
|
||||||
await setStoredRelayBaseUrl(relayBaseUrl);
|
await setStoredRelayBaseUrl(relayBaseUrl);
|
||||||
|
|
||||||
// Register this account's device-client-id with the relay. Multiple
|
// Register this account's device-client-id with the relay. Multiple
|
||||||
@@ -1291,7 +1314,7 @@ async function setupPushNotificationsInner(
|
|||||||
// Nothing the server said about the subscription (a failed request,
|
// Nothing the server said about the subscription (a failed request,
|
||||||
// rate limiting, a refused method call): it keeps delivering, and
|
// rate limiting, a refused method call): it keeps delivering, and
|
||||||
// the next run tries again.
|
// the next run tries again.
|
||||||
throw subscriptionError(err);
|
throw subscriptionError(err, relayBaseUrl);
|
||||||
}
|
}
|
||||||
if (refreshed) {
|
if (refreshed) {
|
||||||
await rememberRefusedEmailPushAccounts(accountId, refusedBefore, emailPush, refreshed.emailPush);
|
await rememberRefusedEmailPushAccounts(accountId, refusedBefore, emailPush, refreshed.emailPush);
|
||||||
@@ -1377,7 +1400,15 @@ async function setupPushNotificationsInner(
|
|||||||
serverExpires = created.result.expires;
|
serverExpires = created.result.expires;
|
||||||
await rememberRefusedEmailPushAccounts(accountId, refusedBefore, emailPush, created.emailPush);
|
await rememberRefusedEmailPushAccounts(accountId, refusedBefore, emailPush, created.emailPush);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw subscriptionError(err);
|
// The server will never take this relay's address. Whatever registration
|
||||||
|
// still works goes through the relay used before: keep that one stored
|
||||||
|
// (token refreshes and the next launch register with it) and drop the
|
||||||
|
// record this run left on the refused relay.
|
||||||
|
if (isRefusedPushUrl(err) && previousRelayBaseUrl !== relayBaseUrl) {
|
||||||
|
await setStoredRelayBaseUrl(previousRelayBaseUrl);
|
||||||
|
await deregisterFromRelay(relayBaseUrl, deviceClientId);
|
||||||
|
}
|
||||||
|
throw subscriptionError(err, relayBaseUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
logPhase('verify');
|
logPhase('verify');
|
||||||
|
|||||||
Reference in New Issue
Block a user