imapflow 1.3.6 → 1.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1188,6 +1188,366 @@ module.exports['Tools: formatMessageResponse normalizes non-ASCII mailbox path f
1188
1188
  test.done();
1189
1189
  };
1190
1190
 
1191
+ module.exports['Tools: formatMessageResponse parses the full attribute set'] = async test => {
1192
+ let untagged = await parser(
1193
+ '* 3 FETCH (UID 100 RFC822.SIZE 5000 FLAGS (\\Seen \\Flagged) MODSEQ (12345) ' +
1194
+ 'X-GM-MSGID 999 X-GM-THRID 888 X-GM-LABELS (\\Important Work) ' +
1195
+ 'INTERNALDATE "12-Jan-2020 10:00:00 +0000" BODY[1] {3}\r\n BODY[HEADER] {2}\r\n)',
1196
+ { literals: [Buffer.from('abc'), Buffer.from('hi')] }
1197
+ );
1198
+ let mailbox = { path: 'INBOX', uidValidity: 1n }; // no uidNext/highestModseq -> exercise the bump branches
1199
+ let r = await tools.formatMessageResponse(untagged, mailbox);
1200
+ test.equal(r.uid, 100);
1201
+ test.equal(r.size, 5000);
1202
+ test.ok(r.flags.has('\\Seen'));
1203
+ test.equal(r.modseq, 12345n);
1204
+ test.equal(r.emailId, '999'); // X-GM-MSGID
1205
+ test.equal(r.threadId, '888'); // X-GM-THRID
1206
+ test.ok(r.labels.has('Work'));
1207
+ test.ok(r.internalDate instanceof Date);
1208
+ test.ok(Buffer.isBuffer(r.headers));
1209
+ test.ok(r.bodyParts.get('1'));
1210
+ // mailbox estimates bumped from the FETCH data
1211
+ test.equal(mailbox.uidNext, 101);
1212
+ test.equal(mailbox.highestModseq, 12345n);
1213
+ test.done();
1214
+ };
1215
+
1216
+ module.exports['Tools: formatMessageResponse parses envelope and bodystructure'] = async test => {
1217
+ let untagged = await parser(
1218
+ '* 1 FETCH (ENVELOPE ("Mon, 2 Sep 2013 05:30:13 -0700" "Subject" ((NIL NIL "a" "b.com")) NIL NIL NIL NIL NIL NIL "<id@x>") ' +
1219
+ 'BODYSTRUCTURE ("TEXT" "PLAIN" ("CHARSET" "utf-8") NIL NIL "7BIT" 12 0 NIL NIL NIL))'
1220
+ );
1221
+ let r = await tools.formatMessageResponse(untagged, { path: 'INBOX' });
1222
+ test.ok(r.envelope);
1223
+ test.equal(r.envelope.subject, 'Subject');
1224
+ test.ok(r.bodyStructure);
1225
+ test.equal(r.bodyStructure.type, 'text/plain');
1226
+ test.done();
1227
+ };
1228
+
1229
+ module.exports['Tools: formatMessageResponse source from BODY[] and BINARY[] literals'] = async test => {
1230
+ let untagged = await parser('* 1 FETCH (BODY[] {5}\r\n)', { literals: [Buffer.from('HELLO')] });
1231
+ let r = await tools.formatMessageResponse(untagged, { path: 'INBOX' });
1232
+ test.ok(Buffer.isBuffer(r.source));
1233
+ test.equal(r.source.toString(), 'HELLO');
1234
+ test.done();
1235
+ };
1236
+
1237
+ module.exports['Tools: formatMessageResponse keeps invalid INTERNALDATE as raw string'] = async test => {
1238
+ let untagged = await parser('* 1 FETCH (INTERNALDATE "not a date")');
1239
+ let r = await tools.formatMessageResponse(untagged, { path: 'INBOX' });
1240
+ test.equal(r.internalDate, 'not a date');
1241
+ test.done();
1242
+ };
1243
+
1244
+ module.exports['Tools: parseEnvelope parses every field'] = async test => {
1245
+ let untagged = await parser(
1246
+ '* 1 FETCH (ENVELOPE ("Mon, 2 Sep 2013 05:30:13 -0700" "Hello" ' +
1247
+ '((NIL NIL "from" "x.com")) ((NIL NIL "sender" "x.com")) ((NIL NIL "reply" "x.com")) ' +
1248
+ '((NIL NIL "to" "x.com")) ((NIL NIL "cc" "x.com")) ((NIL NIL "bcc" "x.com")) ' +
1249
+ '"<inreplyto@x>" "<msgid@x>"))'
1250
+ );
1251
+ let env = tools.parseEnvelope(untagged.attributes[1][1]);
1252
+ test.ok(env.date instanceof Date);
1253
+ test.equal(env.subject, 'Hello');
1254
+ test.equal(env.from[0].address, 'from@x.com');
1255
+ test.equal(env.sender[0].address, 'sender@x.com');
1256
+ test.equal(env.replyTo[0].address, 'reply@x.com');
1257
+ test.equal(env.to[0].address, 'to@x.com');
1258
+ test.equal(env.cc[0].address, 'cc@x.com');
1259
+ test.equal(env.bcc[0].address, 'bcc@x.com');
1260
+ test.equal(env.inReplyTo, '<inreplyto@x>');
1261
+ test.equal(env.messageId, '<msgid@x>');
1262
+ test.done();
1263
+ };
1264
+
1265
+ module.exports['Tools: parseEnvelope keeps invalid date as string'] = async test => {
1266
+ let untagged = await parser('* 1 FETCH (ENVELOPE ("not a date" "Subj" NIL NIL NIL NIL NIL NIL NIL NIL))');
1267
+ let env = tools.parseEnvelope(untagged.attributes[1][1]);
1268
+ test.equal(env.date, 'not a date');
1269
+ test.done();
1270
+ };
1271
+
1272
+ module.exports['Tools: parseBodystructure parses all single-part extension fields'] = async test => {
1273
+ let bs =
1274
+ '("TEXT" "PLAIN" ("CHARSET" "utf-8") "<cid@x>" "a description" "BASE64" 100 5 ' +
1275
+ '"d41d8cd9" ("attachment" ("filename" "f.txt")) ("en" "de") "http://loc/")';
1276
+ let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
1277
+ let node = tools.parseBodystructure(untagged.attributes[1][1]);
1278
+ test.equal(node.type, 'text/plain');
1279
+ test.equal(node.id, '<cid@x>');
1280
+ test.equal(node.description, 'a description');
1281
+ test.equal(node.encoding, 'base64');
1282
+ test.equal(node.size, 100);
1283
+ test.equal(node.lineCount, 5);
1284
+ test.equal(node.md5, 'd41d8cd9');
1285
+ test.equal(node.disposition, 'attachment');
1286
+ test.equal(node.dispositionParameters.filename, 'f.txt');
1287
+ test.deepEqual(node.language, ['en', 'de']);
1288
+ test.done();
1289
+ };
1290
+
1291
+ module.exports['Tools: parseBodystructure parses message/rfc822 with envelope and child'] = async test => {
1292
+ let bs =
1293
+ '("MESSAGE" "RFC822" NIL NIL NIL "7BIT" 200 ' +
1294
+ '("date" "subj" NIL NIL NIL NIL NIL NIL "<reply>" "<msgid>") ' +
1295
+ '("TEXT" "PLAIN" NIL NIL NIL "7BIT" 10 1) 3)';
1296
+ let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
1297
+ let node = tools.parseBodystructure(untagged.attributes[1][1]);
1298
+ test.equal(node.type, 'message/rfc822');
1299
+ test.ok(node.envelope);
1300
+ test.equal(node.childNodes.length, 1);
1301
+ test.equal(node.lineCount, 3);
1302
+ test.done();
1303
+ };
1304
+
1305
+ module.exports['Tools: parseBodystructure parses multipart with params and disposition'] = async test => {
1306
+ let bs =
1307
+ '(("TEXT" "PLAIN" NIL NIL NIL "7BIT" 10 1)("TEXT" "HTML" NIL NIL NIL "7BIT" 20 2) ' +
1308
+ '"ALTERNATIVE" ("BOUNDARY" "xyz") ("inline" NIL) ("en"))';
1309
+ let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
1310
+ let node = tools.parseBodystructure(untagged.attributes[1][1]);
1311
+ test.equal(node.type, 'multipart/alternative');
1312
+ test.equal(node.childNodes.length, 2);
1313
+ test.equal(node.parameters.boundary, 'xyz');
1314
+ test.equal(node.disposition, 'inline');
1315
+ test.done();
1316
+ };
1317
+
1318
+ module.exports['Tools: formatMessageResponse handles NIL, Buffer and unknown keys'] = async test => {
1319
+ let untagged = await parser('* 1 FETCH (UID NIL RFC822.SIZE NIL X-GM-MSGID {3}\r\n BODY[] NIL FOOBAR 123)', {
1320
+ literals: [Buffer.from('999')]
1321
+ });
1322
+ let r = await tools.formatMessageResponse(untagged, { path: 'INBOX' });
1323
+ test.equal(r.uid, 0); // NIL value -> getString returns false -> Number(false) = 0
1324
+ test.equal(r.size, 0);
1325
+ test.equal(r.emailId, '999'); // Buffer literal value -> getString stringifies it
1326
+ test.equal(r.source, false); // BODY[] NIL -> getBuffer returns false
1327
+ test.done();
1328
+ };
1329
+
1330
+ module.exports['Tools: getFolderTree merges duplicate folder entries'] = test => {
1331
+ let tree = tools.getFolderTree([
1332
+ { name: 'Parent', path: 'Parent', parent: [], flags: new Set(['\\HasChildren']), delimiter: '/' },
1333
+ { name: 'Parent', path: 'Parent', parent: [], flags: new Set(['\\Noselect', '\\HasChildren']), specialUse: '\\Sent', delimiter: '/' }
1334
+ ]);
1335
+ test.equal(tree.folders.length, 1);
1336
+ test.equal(tree.folders[0].disabled, true);
1337
+ test.equal(tree.folders[0].specialUse, '\\Sent');
1338
+ test.done();
1339
+ };
1340
+
1341
+ module.exports['Tools: parseBodystructure handles minimal NIL fields'] = async test => {
1342
+ let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ("APPLICATION" "OCTET-STREAM" NIL NIL NIL NIL NIL))');
1343
+ let node = tools.parseBodystructure(untagged.attributes[1][1]);
1344
+ test.equal(node.type, 'application/octet-stream');
1345
+ test.equal(node.id, undefined);
1346
+ test.equal(node.encoding, undefined);
1347
+ test.done();
1348
+ };
1349
+
1350
+ module.exports['Tools: parseBodystructure decodes RFC 2231 charset continuation params'] = async test => {
1351
+ let bs = "(\"TEXT\" \"PLAIN\" (\"name*0*\" \"utf-8''%E2%82%AC abc\" \"name*1\" \"def\") NIL NIL \"7BIT\" 10 1)";
1352
+ let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
1353
+ let node = tools.parseBodystructure(untagged.attributes[1][1]);
1354
+ test.equal(node.parameters.name, '€ abcdef');
1355
+ test.done();
1356
+ };
1357
+
1358
+ module.exports['Tools: parseBodystructure handles empty-string extension fields'] = async test => {
1359
+ // Empty-string values exercise the `|| ''` / `|| 0` fallbacks in each field.
1360
+ let bs = '("TEXT" "PLAIN" ("CHARSET" "") "" "" "" 0 0 "" ("" ("" "")) ("") "")';
1361
+ let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
1362
+ let node = tools.parseBodystructure(untagged.attributes[1][1]);
1363
+ test.equal(node.id, '');
1364
+ test.equal(node.description, '');
1365
+ test.equal(node.size, 0);
1366
+ test.equal(node.md5, '');
1367
+ test.deepEqual(node.language, ['']);
1368
+ test.done();
1369
+ };
1370
+
1371
+ module.exports['Tools: formatMessageResponse with no data list returns just seq'] = async test => {
1372
+ let untagged = await parser('* 5 FETCH');
1373
+ let r = await tools.formatMessageResponse(untagged, { path: 'INBOX' });
1374
+ test.equal(r.seq, 5);
1375
+ test.done();
1376
+ };
1377
+
1378
+ module.exports['Tools: getFolderTree adds folders array when existing entry gains HasChildren'] = test => {
1379
+ let tree = tools.getFolderTree([
1380
+ { name: 'P', path: 'P', parent: [], flags: new Set([]), delimiter: '/' },
1381
+ { name: 'P', path: 'P', parent: [], flags: new Set(['\\HasChildren']), delimiter: '/' }
1382
+ ]);
1383
+ test.ok(Array.isArray(tree.folders[0].folders), 'folders array created on merge');
1384
+ test.done();
1385
+ };
1386
+
1387
+ module.exports['Tools: normalizePath joins array with empty delimiter fallback'] = test => {
1388
+ // namespace present but without a delimiter -> the (... || '') fallback joins directly
1389
+ let connection = { namespace: { prefix: '' } };
1390
+ test.equal(tools.normalizePath(connection, ['a', 'b']), 'ab');
1391
+ test.done();
1392
+ };
1393
+
1394
+ module.exports['Tools: updateCapabilities defaults non-numeric APPENDLIMIT to 0'] = test => {
1395
+ let caps = tools.updateCapabilities([{ value: 'APPENDLIMIT=abc' }]);
1396
+ test.equal(caps.get('APPENDLIMIT'), 0);
1397
+ test.done();
1398
+ };
1399
+
1400
+ module.exports['Tools: formatMessageResponse skips non-string label entries'] = async test => {
1401
+ let untagged = await parser('* 1 FETCH (X-GM-LABELS (\\Important NIL))');
1402
+ let r = await tools.formatMessageResponse(untagged, { path: 'INBOX' });
1403
+ test.deepEqual([...r.labels], ['\\Important']); // NIL entry filtered out
1404
+ test.done();
1405
+ };
1406
+
1407
+ module.exports['Tools: formatMessageResponse does not lower highestModseq'] = async test => {
1408
+ let mailbox = { path: 'INBOX', highestModseq: 99999n };
1409
+ let untagged = await parser('* 1 FETCH (MODSEQ (5) FLAGS (\\Seen))');
1410
+ await tools.formatMessageResponse(untagged, mailbox);
1411
+ test.equal(mailbox.highestModseq, 99999n); // unchanged: incoming modseq is lower
1412
+ test.done();
1413
+ };
1414
+
1415
+ module.exports['Tools: parseBodystructure multipart with NIL subtype'] = async test => {
1416
+ let untagged = await parser('* 1 FETCH (BODYSTRUCTURE (("TEXT" "PLAIN" NIL NIL NIL "7BIT" 1 1) NIL))');
1417
+ let node = tools.parseBodystructure(untagged.attributes[1][1]);
1418
+ test.equal(node.type, 'multipart/');
1419
+ test.done();
1420
+ };
1421
+
1422
+ module.exports['Tools: parseBodystructure content type with NIL type/subtype'] = async test => {
1423
+ let untagged = await parser('* 1 FETCH (BODYSTRUCTURE (NIL NIL NIL NIL NIL "7BIT" 1))');
1424
+ let node = tools.parseBodystructure(untagged.attributes[1][1]);
1425
+ test.equal(node.type, '/');
1426
+ test.done();
1427
+ };
1428
+
1429
+ module.exports['Tools: parseBodystructure message/rfc822 with NIL envelope/linecount'] = async test => {
1430
+ let bs = '("MESSAGE" "RFC822" NIL NIL NIL "7BIT" 100 NIL ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 1 1) NIL NIL (NIL) NIL)';
1431
+ let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
1432
+ let node = tools.parseBodystructure(untagged.attributes[1][1]);
1433
+ test.equal(node.type, 'message/rfc822');
1434
+ test.done();
1435
+ };
1436
+
1437
+ module.exports['Tools: parseBodystructure text with NIL language/location entries'] = async test => {
1438
+ let bs = '("TEXT" "PLAIN" NIL NIL NIL "7BIT" 100 NIL "md5" ("inline" NIL) (NIL) NIL)';
1439
+ let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
1440
+ let node = tools.parseBodystructure(untagged.attributes[1][1]);
1441
+ test.deepEqual(node.language, ['']);
1442
+ test.done();
1443
+ };
1444
+
1445
+ module.exports['Tools: parseBodystructure empty-string size/linecount/language/location'] = async test => {
1446
+ let bs = '("TEXT" "PLAIN" NIL NIL NIL "7BIT" "" "" "" ("inline" NIL) ("") "")';
1447
+ let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
1448
+ let node = tools.parseBodystructure(untagged.attributes[1][1]);
1449
+ test.equal(node.size, 0);
1450
+ test.equal(node.lineCount, 0);
1451
+ test.deepEqual(node.language, ['']);
1452
+ test.done();
1453
+ };
1454
+
1455
+ module.exports['Tools: parseBodystructure message/rfc822 empty-string linecount'] = async test => {
1456
+ let bs =
1457
+ '("MESSAGE" "RFC822" NIL NIL NIL "7BIT" 100 ("d" "s" NIL NIL NIL NIL NIL NIL NIL NIL) ' +
1458
+ '("TEXT" "PLAIN" NIL NIL NIL "7BIT" 1 1) "")';
1459
+ let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
1460
+ let node = tools.parseBodystructure(untagged.attributes[1][1]);
1461
+ test.equal(node.type, 'message/rfc822');
1462
+ test.equal(node.lineCount, 0);
1463
+ test.done();
1464
+ };
1465
+
1466
+ module.exports['Tools: parseBodystructure decodes RFC 2231 with empty charset and special chars'] = async test => {
1467
+ // Empty charset before the first quote defaults to utf-8; '=' / '?' in the value
1468
+ // exercise the 2-char hex escape branch; the empty *1* continuation segment too.
1469
+ // value includes '=' / '?' (2-char hex escapes), a space (-> '_') and a TAB
1470
+ // (a <0x10 control char -> single-hex-digit escape needing a '0' pad).
1471
+ let bs = "(\"TEXT\" \"PLAIN\" (\"name*0*\" \"''a=b?c d\te\" \"name*1*\" \"\") NIL NIL \"7BIT\" 1 1)";
1472
+ let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
1473
+ let node = tools.parseBodystructure(untagged.attributes[1][1]);
1474
+ test.ok(node.parameters.name.includes('a=b?c'));
1475
+ test.done();
1476
+ };
1477
+
1478
+ module.exports['Tools: parseBodystructure parses empty-string body location'] = async test => {
1479
+ // A trailing element after location makes the (i < length-1) guard pass so the
1480
+ // empty-string location value goes through the `|| ''` fallback.
1481
+ let bs = '("TEXT" "PLAIN" NIL NIL NIL "7BIT" 1 1 NIL NIL NIL "" NIL)';
1482
+ let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
1483
+ let node = tools.parseBodystructure(untagged.attributes[1][1]);
1484
+ test.equal(node.location, '');
1485
+ test.done();
1486
+ };
1487
+
1488
+ module.exports['Tools: expandRange descending range to non-numeric second bound'] = test => {
1489
+ test.deepEqual(tools.expandRange('5:abc'), [5, 4, 3, 2, 1, 0]);
1490
+ test.done();
1491
+ };
1492
+
1493
+ module.exports['Tools: expandRange tolerates non-numeric entries'] = test => {
1494
+ test.deepEqual(tools.expandRange('abc,:5,1:3'), [0, 0, 1, 2, 3, 4, 5, 1, 2, 3]);
1495
+ test.done();
1496
+ };
1497
+
1498
+ module.exports['Tools: expandRange handles descending ranges'] = test => {
1499
+ test.deepEqual(tools.expandRange('5:3'), [5, 4, 3]);
1500
+ test.done();
1501
+ };
1502
+
1503
+ module.exports['Tools: encodePath keeps name as-is when iconv encode throws'] = test => {
1504
+ // The iconv module object is shared by reference; patch encode to throw so the
1505
+ // defensive catch is exercised, then restore.
1506
+ let original = iconv.encode;
1507
+ iconv.encode = () => {
1508
+ throw new Error('encode boom');
1509
+ };
1510
+ try {
1511
+ let connection = { enabled: new Set() };
1512
+ let result = tools.encodePath(connection, 'Tärä');
1513
+ test.equal(result, 'Tärä', 'falls back to the raw path');
1514
+ } finally {
1515
+ iconv.encode = original;
1516
+ }
1517
+ test.done();
1518
+ };
1519
+
1520
+ module.exports['Tools: decodePath keeps name as-is when iconv decode throws'] = test => {
1521
+ let original = iconv.decode;
1522
+ iconv.decode = () => {
1523
+ throw new Error('decode boom');
1524
+ };
1525
+ try {
1526
+ let connection = { enabled: new Set() };
1527
+ let result = tools.decodePath(connection, 'Inbox&AOk-');
1528
+ test.equal(result, 'Inbox&AOk-');
1529
+ } finally {
1530
+ iconv.decode = original;
1531
+ }
1532
+ test.done();
1533
+ };
1534
+
1535
+ module.exports['Tools: formatMessageResponse id falls back when iconv encode throws'] = async test => {
1536
+ let original = iconv.encode;
1537
+ iconv.encode = () => {
1538
+ throw new Error('encode boom');
1539
+ };
1540
+ try {
1541
+ let untagged = await parser('* 1 FETCH (UID 5 FLAGS (\\Seen))');
1542
+ // non-ASCII path triggers the encode branch; the throw is swallowed
1543
+ let r = await tools.formatMessageResponse(untagged, { path: 'Tärä', uidValidity: 1n });
1544
+ test.ok(r.id, 'an id is still produced');
1545
+ } finally {
1546
+ iconv.encode = original;
1547
+ }
1548
+ test.done();
1549
+ };
1550
+
1191
1551
  // ============================================
1192
1552
  // packMessageRange
1193
1553
  // ============================================