Skip to content

Commit c8033d0

Browse files
committed
fix: restore TTI to original but with added NOOP for yahoo
1 parent 3a1fe04 commit c8033d0

File tree

1 file changed

+53
-262
lines changed

1 file changed

+53
-262
lines changed

helpers/get-message.js

Lines changed: 53 additions & 262 deletions
Original file line numberDiff line numberDiff line change
@@ -6,279 +6,70 @@
66
const ms = require('ms');
77
const pWaitFor = require('p-wait-for');
88

9-
const logger = require('#helpers/logger');
10-
11-
//
12-
// NOTE: We use polling instead of IMAP IDLE for all providers because:
13-
// 1. Gmail's IDLE EXISTS notifications are delayed 30+ seconds
14-
// 2. Yahoo does not support IMAP IDLE at all
15-
// - https://stackoverflow.com/a/71254393
16-
// - https://github.com/ikvk/imap_tools/blob/master/tests/test_idle.py
17-
// 3. Polling provides consistent, predictable behavior across all providers
18-
//
19-
const POLLING_INTERVAL = 0; // ms('1s');
20-
21-
/**
22-
* Wait for a message to arrive using continuous polling.
23-
* Checks the mailbox at regular intervals until the message is found or timeout.
24-
*
25-
* @param {Object} imapClient - ImapFlow client instance (should have mailbox selected)
26-
* @param {Object} info - Message info containing messageId
27-
* @param {Object} provider - Provider configuration
28-
* @returns {Promise<{provider: Object, received: Date|null, err: Error|null}>}
29-
*/
309
async function getMessage(imapClient, info, provider) {
3110
let received;
3211
let err;
33-
const startTime = Date.now();
34-
const timeout = ms('1m');
35-
36-
// Extract the unique part of message ID for matching
37-
const messageIdPart = info.messageId
38-
.replace('<', '')
39-
.replace('>', '')
40-
.split('@')[0];
41-
42-
logger.debug('getMessage started', {
43-
provider: provider.name,
44-
messageId: info.messageId,
45-
messageIdPart,
46-
timeout,
47-
pollingInterval: POLLING_INTERVAL,
48-
mailboxPath: imapClient.mailbox?.path,
49-
mailboxExists: imapClient.mailbox?.exists,
50-
clientUsable: imapClient.usable
51-
});
52-
53-
try {
54-
// First check if message already exists (in case it arrived before we started waiting)
55-
logger.debug('Checking if message already exists', {
56-
provider: provider.name,
57-
messageIdPart
58-
});
59-
60-
const checkStartTime = Date.now();
61-
const alreadyExists = await checkForMessage(imapClient, messageIdPart);
62-
const checkDuration = Date.now() - checkStartTime;
63-
64-
logger.debug('Initial message check completed', {
65-
provider: provider.name,
66-
alreadyExists,
67-
checkDurationMs: checkDuration
68-
});
69-
70-
if (alreadyExists) {
71-
received = new Date();
72-
logger.info('Message already exists (no wait needed)', {
73-
provider: provider.name,
74-
messageId: info.messageId,
75-
timeMs: Date.now() - startTime
76-
});
77-
return { provider, received, err };
78-
}
79-
80-
// Use polling to wait for message
81-
logger.debug('Message not found, starting polling', {
82-
provider: provider.name,
83-
messageIdPart,
84-
timeoutMs: timeout,
85-
pollingIntervalMs: POLLING_INTERVAL
86-
});
87-
88-
received = await waitForMessageWithPolling(imapClient, messageIdPart, {
89-
timeout,
90-
provider,
91-
info,
92-
startTime
93-
});
94-
} catch (_err) {
95-
err = _err;
96-
logger.debug('getMessage caught error', {
97-
provider: provider.name,
98-
error: _err.message,
99-
stack: _err.stack
100-
});
101-
}
102-
103-
const totalTime = Date.now() - startTime;
104-
105-
// Log timing for debugging
106-
if (received) {
107-
logger.info('getMessage completed successfully', {
108-
provider: provider.name,
109-
totalTimeMs: totalTime,
110-
messageId: info.messageId
111-
});
112-
} else {
113-
logger.warn('getMessage completed without finding message', {
114-
provider: provider.name,
115-
totalTimeMs: totalTime,
116-
messageId: info.messageId,
117-
error: err ? err.message : 'no error'
118-
});
119-
}
120-
121-
return { provider, received, err };
122-
}
123-
124-
/**
125-
* Check if a message with the given ID part already exists in the mailbox
126-
*/
127-
async function checkForMessage(imapClient, messageIdPart) {
128-
const checkStartTime = Date.now();
129-
130-
try {
131-
// Issue NOOP to refresh mailbox state before checking
132-
// This is required for Yahoo which doesn't push EXISTS updates
133-
// Without NOOP, Yahoo returns stale mailbox data
134-
try {
135-
await imapClient.noop();
136-
} catch (noopErr) {
137-
logger.debug('NOOP failed, continuing anyway', {
138-
error: noopErr.message
139-
});
140-
}
141-
142-
// Get current mailbox status (now refreshed after NOOP)
143-
const status = imapClient.mailbox;
144-
145-
logger.debug('checkForMessage: mailbox status', {
146-
path: status?.path,
147-
exists: status?.exists,
148-
uidNext: status?.uidNext,
149-
uidValidity: status?.uidValidity
150-
});
151-
152-
if (!status || !status.exists || status.exists === 0) {
153-
logger.debug('checkForMessage: mailbox empty or not selected', {
154-
hasStatus: Boolean(status),
155-
exists: status?.exists
156-
});
157-
return false;
158-
}
159-
160-
// Only check the last 10 messages for efficiency
161-
const startSeq = Math.max(1, status.exists - 9);
162-
const range = `${startSeq}:*`;
163-
164-
logger.debug('checkForMessage: fetching messages', {
165-
range,
166-
totalMessages: status.exists,
167-
checkingCount: status.exists - startSeq + 1
168-
});
169-
170-
let messagesChecked = 0;
171-
for await (const message of imapClient.fetch(range, {
172-
envelope: true
173-
})) {
174-
messagesChecked++;
175-
const msgId = message.envelope?.messageId;
176-
177-
logger.debug('checkForMessage: checking message', {
178-
seq: message.seq,
179-
uid: message.uid,
180-
messageId: msgId,
181-
lookingFor: messageIdPart,
182-
matches: msgId ? msgId.includes(messageIdPart) : false
183-
});
184-
185-
if (msgId && msgId.includes(messageIdPart)) {
186-
logger.debug('checkForMessage: FOUND matching message', {
187-
seq: message.seq,
188-
uid: message.uid,
189-
messageId: msgId,
190-
durationMs: Date.now() - checkStartTime
191-
});
192-
return true;
193-
}
194-
}
195-
196-
logger.debug('checkForMessage: no match found', {
197-
messagesChecked,
198-
durationMs: Date.now() - checkStartTime
199-
});
200-
} catch (err) {
201-
// Log but don't throw - we'll retry on next poll
202-
logger.warn('checkForMessage: error during check', {
203-
error: err.message,
204-
stack: err.stack,
205-
durationMs: Date.now() - checkStartTime
206-
});
207-
}
208-
209-
return false;
210-
}
211-
212-
/**
213-
* Wait for a message using continuous polling.
214-
*
215-
* We use polling instead of IMAP IDLE because:
216-
* 1. Gmail's IDLE EXISTS notifications are delayed 30+ seconds
217-
* 2. Yahoo does not support IMAP IDLE at all
218-
* - https://stackoverflow.com/a/71254393
219-
* - https://github.com/ikvk/imap_tools/blob/master/tests/test_idle.py
220-
* 3. Polling provides consistent, predictable behavior across all providers
221-
*/
222-
async function waitForMessageWithPolling(imapClient, messageIdPart, options) {
223-
const { timeout, provider, info, startTime } = options;
224-
225-
logger.debug('waitForMessageWithPolling: starting', {
226-
provider: provider.name,
227-
messageIdPart,
228-
timeoutMs: timeout,
229-
pollingIntervalMs: POLLING_INTERVAL
230-
});
231-
232-
let pollCount = 0;
233-
23412
try {
23513
await pWaitFor(
23614
async () => {
237-
pollCount++;
238-
const pollStartTime = Date.now();
239-
240-
logger.debug('Polling: checking for message', {
241-
provider: provider.name,
242-
pollCount,
243-
timeSinceStartMs: Date.now() - startTime
244-
});
245-
246-
const found = await checkForMessage(imapClient, messageIdPart);
247-
248-
logger.debug('Polling: check completed', {
249-
provider: provider.name,
250-
pollCount,
251-
found,
252-
checkDurationMs: Date.now() - pollStartTime
253-
});
254-
255-
return found;
15+
// TODO: IMAP Protocol Extension Support
16+
// TODO: render a page with each provider's capabilities
17+
// <https://gist.github.com/nevans/8ef449da0786f9d1cc7c8324a288dd9b>
18+
// /blog/smtp-capability-command-by-provider
19+
// /blog/smtp-jmap-capability-imaprev
20+
// console.log('capabilities', imapClient.capabilities);
21+
22+
try {
23+
//
24+
// NOTE: We issue NOOP before each fetch to refresh mailbox state.
25+
// This is required for Yahoo which doesn't push EXISTS updates.
26+
// Without NOOP, Yahoo returns stale mailbox data.
27+
// - https://stackoverflow.com/a/71254393
28+
// - https://github.com/ikvk/imap_tools/blob/master/tests/test_idle.py
29+
//
30+
try {
31+
await imapClient.noop();
32+
} catch {}
33+
34+
for await (const message of imapClient.fetch('*', {
35+
headers: ['Message-ID']
36+
})) {
37+
if (received) break;
38+
if (
39+
message.headers &&
40+
message.headers
41+
.toString()
42+
.includes(
43+
info.messageId.replace('<', '').replace('>', '').split('@')[1]
44+
)
45+
) {
46+
//
47+
// NOTE: due to NTP time differences we cannot rely on
48+
// a message's internal date from a given provider
49+
// nor can we rely on Recieved headers
50+
// nor can we rely on message envelope date
51+
//
52+
received = new Date();
53+
}
54+
}
55+
} catch (_err) {
56+
err = _err;
57+
}
58+
59+
if (err) throw err;
60+
61+
return Boolean(received);
25662
},
25763
{
258-
interval: POLLING_INTERVAL,
259-
timeout
64+
interval: 0,
65+
timeout: ms('1m')
26066
}
26167
);
262-
263-
const receivedTime = new Date();
264-
logger.info('Polling: message FOUND', {
265-
provider: provider.name,
266-
messageId: info.messageId,
267-
totalTimeMs: Date.now() - startTime,
268-
pollCount
269-
});
270-
271-
return receivedTime;
272-
} catch (err) {
273-
logger.warn('Polling: timeout or error', {
274-
provider: provider.name,
275-
messageId: info.messageId,
276-
error: err.message,
277-
totalTimeMs: Date.now() - startTime,
278-
pollCount
279-
});
280-
throw new Error('Polling timeout waiting for message');
68+
} catch (_err) {
69+
err = _err;
28170
}
71+
72+
return { provider, received, err };
28273
}
28374

28475
module.exports = getMessage;

0 commit comments

Comments
 (0)