summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/extensions/parent/ext-mail.js
blob: 31e86fe7b407d0b020951c5fe631c5f666c1525d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

var { AppConstants } = ChromeUtils.importESModule(
  "resource://gre/modules/AppConstants.sys.mjs"
);
var { XPCOMUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/XPCOMUtils.sys.mjs"
);

var { ExtensionError, getInnerWindowID } = ExtensionUtils;
var { defineLazyGetter, makeWidgetId } = ExtensionCommon;

var { ExtensionSupport } = ChromeUtils.import(
  "resource:///modules/ExtensionSupport.jsm"
);

ChromeUtils.defineESModuleGetters(this, {
  ExtensionContent: "resource://gre/modules/ExtensionContent.sys.mjs",
});

XPCOMUtils.defineLazyModuleGetters(this, {
  MailServices: "resource:///modules/MailServices.jsm",
});

XPCOMUtils.defineLazyPreferenceGetter(
  this,
  "gJunkThreshold",
  "mail.adaptivefilters.junk_threshold",
  90
);
XPCOMUtils.defineLazyPreferenceGetter(
  this,
  "gMessagesPerPage",
  "extensions.webextensions.messagesPerPage",
  100
);
XPCOMUtils.defineLazyGlobalGetters(this, [
  "IOUtils",
  "PathUtils",
  "FileReader",
]);

const MAIN_WINDOW_URI = "chrome://messenger/content/messenger.xhtml";
const POPUP_WINDOW_URI = "chrome://messenger/content/extensionPopup.xhtml";
const COMPOSE_WINDOW_URI =
  "chrome://messenger/content/messengercompose/messengercompose.xhtml";
const MESSAGE_WINDOW_URI = "chrome://messenger/content/messageWindow.xhtml";
const MESSAGE_PROTOCOLS = ["imap", "mailbox", "news", "nntp", "snews"];

const NOTIFICATION_COLLAPSE_TIME = 200;

(function () {
  // Monkey-patch all processes to add the "messenger" alias in all contexts.
  Services.ppmm.loadProcessScript(
    "chrome://messenger/content/processScript.js",
    true
  );

  // This allows scripts to run in the compose document or message display
  // document if and only if the extension has permission.
  let { defaultConstructor } = ExtensionContent.contentScripts;
  ExtensionContent.contentScripts.defaultConstructor = function (matcher) {
    let script = defaultConstructor.call(this, matcher);

    let { matchesWindowGlobal } = script;
    script.matchesWindowGlobal = function (windowGlobal) {
      let { browsingContext, windowContext } = windowGlobal;

      if (
        browsingContext.topChromeWindow?.location.href == COMPOSE_WINDOW_URI &&
        windowContext.documentPrincipal.isNullPrincipal &&
        windowContext.documentURI?.spec == "about:blank?compose"
      ) {
        return script.extension.hasPermission("compose");
      }

      if (MESSAGE_PROTOCOLS.includes(windowContext.documentURI?.scheme)) {
        return script.extension.hasPermission("messagesModify");
      }

      return matchesWindowGlobal.apply(script, arguments);
    };

    return script;
  };
})();

let tabTracker;
let spaceTracker;
let windowTracker;

// This function is pretty tightly tied to Extension.jsm.
// Its job is to fill in the |tab| property of the sender.
const getSender = (extension, target, sender) => {
  let tabId = -1;
  if ("tabId" in sender) {
    // The message came from a privileged extension page running in a tab. In
    // that case, it should include a tabId property (which is filled in by the
    // page-open listener below).
    tabId = sender.tabId;
    delete sender.tabId;
  } else if (
    ExtensionCommon.instanceOf(target, "XULFrameElement") ||
    ExtensionCommon.instanceOf(target, "HTMLIFrameElement")
  ) {
    tabId = tabTracker.getBrowserData(target).tabId;
  }

  if (tabId != null && tabId >= 0) {
    let tab = extension.tabManager.get(tabId, null);
    if (tab) {
      sender.tab = tab.convert();
    }
  }
};

// Used by Extension.jsm.
global.tabGetSender = getSender;

global.clickModifiersFromEvent = event => {
  const map = {
    shiftKey: "Shift",
    altKey: "Alt",
    metaKey: "Command",
    ctrlKey: "Ctrl",
  };
  let modifiers = Object.keys(map)
    .filter(key => event[key])
    .map(key => map[key]);

  if (event.ctrlKey && AppConstants.platform === "macosx") {
    modifiers.push("MacCtrl");
  }

  return modifiers;
};

global.openOptionsPage = extension => {
  let window = windowTracker.topNormalWindow;
  if (!window) {
    return Promise.reject({ message: "No mail window available" });
  }

  if (extension.manifest.options_ui.open_in_tab) {
    window.switchToTabHavingURI(extension.manifest.options_ui.page, true, {
      triggeringPrincipal: extension.principal,
    });
    return Promise.resolve();
  }

  let viewId = `addons://detail/${encodeURIComponent(
    extension.id
  )}/preferences`;

  return window.openAddonsMgr(viewId);
};

/**
 * Returns a real file for the given DOM File.
 *
 * @param {File} file - the DOM File
 * @returns {nsIFile}
 */
async function getRealFileForFile(file) {
  if (file.mozFullPath) {
    let realFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
    realFile.initWithPath(file.mozFullPath);
    return realFile;
  }

  let pathTempFile = await IOUtils.createUniqueFile(
    PathUtils.tempDir,
    file.name.replaceAll(/[/:*?\"<>|]/g, "_"),
    0o600
  );

  let tempFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
  tempFile.initWithPath(pathTempFile);
  let extAppLauncher = Cc[
    "@mozilla.org/uriloader/external-helper-app-service;1"
  ].getService(Ci.nsPIExternalAppLauncher);
  extAppLauncher.deleteTemporaryFileOnExit(tempFile);

  let bytes = await new Promise(function (resolve) {
    let reader = new FileReader();
    reader.onloadend = function () {
      resolve(new Uint8Array(reader.result));
    };
    reader.readAsArrayBuffer(file);
  });

  await IOUtils.write(pathTempFile, bytes);
  return tempFile;
}

/**
 * Gets the window for a tabmail tabInfo.
 *
 * @param {NativeTabInfo} nativeTabInfo - The tabInfo object to get the browser for
 * @returns {Window} - The browser element for the tab
 */
function getTabWindow(nativeTabInfo) {
  return Cu.getGlobalForObject(nativeTabInfo);
}
global.getTabWindow = getTabWindow;

/**
 * Gets the tabmail for a tabmail tabInfo.
 *
 * @param {NativeTabInfo} nativeTabInfo - The tabInfo object to get the browser for
 * @returns {?XULElement} - The browser element for the tab
 */
function getTabTabmail(nativeTabInfo) {
  return getTabWindow(nativeTabInfo).document.getElementById("tabmail");
}
global.getTabTabmail = getTabTabmail;

/**
 * Gets the tab browser for the tabmail tabInfo.
 *
 * @param {NativeTabInfo} nativeTabInfo - The tabInfo object to get the browser for
 * @returns {?XULElement} The browser element for the tab
 */
function getTabBrowser(nativeTabInfo) {
  if (!nativeTabInfo) {
    return null;
  }

  if (nativeTabInfo.mode) {
    if (nativeTabInfo.mode.getBrowser) {
      return nativeTabInfo.mode.getBrowser(nativeTabInfo);
    }

    if (nativeTabInfo.mode.tabType.getBrowser) {
      return nativeTabInfo.mode.tabType.getBrowser(nativeTabInfo);
    }
  }

  if (nativeTabInfo.ownerGlobal && nativeTabInfo.ownerGlobal.getBrowser) {
    return nativeTabInfo.ownerGlobal.getBrowser();
  }

  return null;
}
global.getTabBrowser = getTabBrowser;

/**
 * Manages tab-specific and window-specific context data, and dispatches
 * tab select events across all windows.
 */
global.TabContext = class extends EventEmitter {
  /**
   * @param {Function} getDefaultPrototype
   *        Provides the prototype of the context value for a tab or window when there is none.
   *        Called with a XULElement or ChromeWindow argument.
   *        Should return an object or null.
   */
  constructor(getDefaultPrototype) {
    super();
    this.getDefaultPrototype = getDefaultPrototype;
    this.tabData = new WeakMap();
  }

  /**
   * Returns the context data associated with `keyObject`.
   *
   * @param {XULElement|ChromeWindow} keyObject
   *        Browser tab or browser chrome window.
   * @returns {object}
   */
  get(keyObject) {
    if (!this.tabData.has(keyObject)) {
      let data = Object.create(this.getDefaultPrototype(keyObject));
      this.tabData.set(keyObject, data);
    }

    return this.tabData.get(keyObject);
  }

  /**
   * Clears the context data associated with `keyObject`.
   *
   * @param {XULElement|ChromeWindow} keyObject
   *        Browser tab or browser chrome window.
   */
  clear(keyObject) {
    this.tabData.delete(keyObject);
  }
};

/* global searchInitialized */
// This promise is used to wait for the search service to be initialized.
// None of the code in the WebExtension modules requests that initialization.
// It is assumed that it is started at some point. That might never happen,
// e.g. if the application shuts down before the search service initializes.
XPCOMUtils.defineLazyGetter(global, "searchInitialized", () => {
  if (Services.search.isInitialized) {
    return Promise.resolve();
  }
  return ExtensionUtils.promiseObserved(
    "browser-search-service",
    (_, data) => data == "init-complete"
  );
});

/**
 * Class for dummy message Headers.
 */
class nsDummyMsgHeader {
  constructor(msgHdr) {
    this.mProperties = [];
    this.messageSize = 0;
    this.author = null;
    this.subject = "";
    this.recipients = null;
    this.ccList = null;
    this.listPost = null;
    this.messageId = null;
    this.date = 0;
    this.accountKey = "";
    this.flags = 0;
    // If you change us to return a fake folder, please update
    // folderDisplay.js's FolderDisplayWidget's selectedMessageIsExternal getter.
    this.folder = null;

    if (msgHdr) {
      for (let member of [
        "accountKey",
        "ccList",
        "date",
        "flags",
        "listPost",
        "messageId",
        "messageSize",
      ]) {
        // Members are either (associative) arrays or primitives.
        if (typeof msgHdr[member] == "object") {
          this[member] = [];
          for (let property in msgHdr[member]) {
            this[member][property] = msgHdr[member][property];
          }
        } else {
          this[member] = msgHdr[member];
        }
      }
      this.author = msgHdr.mime2DecodedAuthor;
      this.recipients = msgHdr.mime2DecodedRecipients;
      this.subject = msgHdr.mime2DecodedSubject;
      this.mProperties.dummyMsgUrl = msgHdr.getStringProperty("dummyMsgUrl");
      this.mProperties.dummyMsgLastModifiedTime = msgHdr.getUint32Property(
        "dummyMsgLastModifiedTime"
      );
    }
  }
  getProperty(aProperty) {
    return this.getStringProperty(aProperty);
  }
  setProperty(aProperty, aVal) {
    return this.setStringProperty(aProperty, aVal);
  }
  getStringProperty(aProperty) {
    if (aProperty in this.mProperties) {
      return this.mProperties[aProperty];
    }
    return "";
  }
  setStringProperty(aProperty, aVal) {
    this.mProperties[aProperty] = aVal;
  }
  getUint32Property(aProperty) {
    if (aProperty in this.mProperties) {
      return parseInt(this.mProperties[aProperty]);
    }
    return 0;
  }
  setUint32Property(aProperty, aVal) {
    this.mProperties[aProperty] = aVal.toString();
  }
  markHasAttachments(hasAttachments) {}
  get mime2DecodedAuthor() {
    return this.author;
  }
  get mime2DecodedSubject() {
    return this.subject;
  }
  get mime2DecodedRecipients() {
    return this.recipients;
  }
}

/**
 * Returns the WebExtension window type for the given window, or null, if it is
 * not supported.
 *
 * @param {DOMWindow} window - The window to check
 * @returns {[string]} - The WebExtension type of the window
 */
function getWebExtensionWindowType(window) {
  let { documentElement } = window.document;
  if (!documentElement) {
    return null;
  }
  switch (documentElement.getAttribute("windowtype")) {
    case "msgcompose":
      return "messageCompose";
    case "mail:messageWindow":
      return "messageDisplay";
    case "mail:extensionPopup":
      return "popup";
    case "mail:3pane":
      return "normal";
    default:
      return "unknown";
  }
}

/**
 * The window tracker tracks opening and closing Thunderbird windows. Each window has an id, which
 * is mapped to native window objects.
 */
class WindowTracker extends WindowTrackerBase {
  /**
   * Adds a tab progress listener to the given mail window.
   *
   * @param {DOMWindow} window - The mail window to which to add the listener.
   * @param {object} listener - The listener to add
   */
  addProgressListener(window, listener) {
    if (window.contentProgress) {
      window.contentProgress.addListener(listener);
    }
  }

  /**
   * Removes a tab progress listener from the given mail window.
   *
   * @param {DOMWindow} window - The mail window from which to remove the listener.
   * @param {object} listener - The listener to remove
   */
  removeProgressListener(window, listener) {
    if (window.contentProgress) {
      window.contentProgress.removeListener(listener);
    }
  }

  /**
   * Determines if the passed window object is supported by the windows API. The
   * function name is for base class compatibility with toolkit.
   *
   * @param {DOMWindow} window - The window to check
   * @returns {boolean} True, if the window is supported by the windows API
   */
  isBrowserWindow(window) {
    let type = getWebExtensionWindowType(window);
    return !!type && type != "unknown";
  }

  /**
   * Determines if the passed window object is a mail window but not the main
   * window. This is useful to find windows where the window itself is the
   * "nativeTab" object in API terms.
   *
   * @param {DOMWindow} window - The window to check
   * @returns {boolean} True, if the window is a mail window but not the main window
   */
  isSecondaryWindow(window) {
    let { documentElement } = window.document;
    if (!documentElement) {
      return false;
    }

    return ["msgcompose", "mail:messageWindow", "mail:extensionPopup"].includes(
      documentElement.getAttribute("windowtype")
    );
  }

  /**
   * The currently active, or topmost window supported by the API, or null if no
   * supported window is currently open.
   *
   * @property {?DOMWindow} topWindow
   * @readonly
   */
  get topWindow() {
    let win = Services.wm.getMostRecentWindow(null);
    // If we're lucky, this is a window supported by the API and we can return it
    // directly.
    if (win && !this.isBrowserWindow(win)) {
      win = null;
      // This is oldest to newest, so this gets a bit ugly.
      for (let nextWin of Services.wm.getEnumerator(null)) {
        if (this.isBrowserWindow(nextWin)) {
          win = nextWin;
        }
      }
    }
    return win;
  }

  /**
   * The currently active, or topmost window, or null if no window is currently open, that
   * is not private browsing.
   *
   * @property {DOMWindow|null} topWindow
   * @readonly
   */
  get topNonPBWindow() {
    // Thunderbird does not support private browsing, return topWindow.
    return this.topWindow;
  }

  /**
   * The currently active, or topmost, mail window, or null if no mail window is currently open.
   * Will only return the topmost "normal" (i.e., not popup) window.
   *
   * @property {?DOMWindow} topNormalWindow
   * @readonly
   */
  get topNormalWindow() {
    return Services.wm.getMostRecentWindow("mail:3pane");
  }
}

/**
 * Convenience class to keep track of and manage spaces.
 */
class SpaceTracker {
  /**
   * @typedef SpaceData
   * @property {string} name - name of the space as used by the extension
   * @property {integer} spaceId - id of the space as used by the tabs API
   * @property {string} spaceButtonId - id of the button of this space in the
   *   spaces toolbar
   * @property {string} defaultUrl - the url for the default space tab
   * @property {ButtonProperties} buttonProperties
   *   @see mail/components/extensions/schemas/spaces.json
   * @property {ExtensionData} extension - the extension the space belongs to
   */

  constructor() {
    this._nextId = 1;
    this._spaceData = new Map();
    this._spaceIds = new Map();

    // Keep this in sync with the default spaces in gSpacesToolbar.
    let builtInSpaces = [
      {
        name: "mail",
        spaceButtonId: "mailButton",
        tabInSpace: tabInfo =>
          ["folder", "mail3PaneTab", "mailMessageTab"].includes(
            tabInfo.mode.name
          )
            ? 1
            : 0,
      },
      {
        name: "addressbook",
        spaceButtonId: "addressBookButton",
        tabInSpace: tabInfo => (tabInfo.mode.name == "addressBookTab" ? 1 : 0),
      },
      {
        name: "calendar",
        spaceButtonId: "calendarButton",
        tabInSpace: tabInfo => (tabInfo.mode.name == "calendar" ? 1 : 0),
      },
      {
        name: "tasks",
        spaceButtonId: "tasksButton",
        tabInSpace: tabInfo => (tabInfo.mode.name == "tasks" ? 1 : 0),
      },
      {
        name: "chat",
        spaceButtonId: "chatButton",
        tabInSpace: tabInfo => (tabInfo.mode.name == "chat" ? 1 : 0),
      },
      {
        name: "settings",
        spaceButtonId: "settingsButton",
        tabInSpace: tabInfo => {
          switch (tabInfo.mode.name) {
            case "preferencesTab":
              // A primary tab that the open method creates.
              return 1;
            case "contentTab":
              let url = tabInfo.urlbar?.value;
              if (url == "about:accountsettings" || url == "about:addons") {
                // A secondary tab, that is related to this space.
                return 2;
              }
          }
          return 0;
        },
      },
    ];
    for (let builtInSpace of builtInSpaces) {
      this._add(builtInSpace);
    }
  }

  findSpaceForTab(tabInfo) {
    for (let spaceData of this._spaceData.values()) {
      if (spaceData.tabInSpace(tabInfo)) {
        return spaceData;
      }
    }
    return undefined;
  }

  _add(spaceData) {
    let spaceId = this._nextId++;
    let { spaceButtonId } = spaceData;
    this._spaceData.set(spaceButtonId, { ...spaceData, spaceId });
    this._spaceIds.set(spaceId, spaceButtonId);
    return { ...spaceData, spaceId };
  }

  /**
   * Generate an id of the form <add-on-id>-spacesButton-<spaceId>.
   *
   * @param {string} name - name of the space as used by the extension
   * @param {ExtensionData} extension
   * @returns {string} id of the html element of the spaces toolbar button of
   *   this space
   */
  _getSpaceButtonId(name, extension) {
    return `${makeWidgetId(extension.id)}-spacesButton-${name}`;
  }

  /**
   * Get the SpaceData for the space with the given name for the given extension.
   *
   * @param {string} name - name of the space as used by the extension
   * @param {ExtensionData} extension
   * @returns {SpaceData}
   */
  fromSpaceName(name, extension) {
    let spaceButtonId = this._getSpaceButtonId(name, extension);
    return this.fromSpaceButtonId(spaceButtonId);
  }

  /**
   * Get the SpaceData for the space with the given spaceId.
   *
   * @param {integer} spaceId - id of the space as used by the tabs API
   * @returns {SpaceData}
   */
  fromSpaceId(spaceId) {
    let spaceButtonId = this._spaceIds.get(spaceId);
    return this.fromSpaceButtonId(spaceButtonId);
  }

  /**
   * Get the SpaceData for the space with the given spaceButtonId.
   *
   * @param {string} spaceButtonId - id of the html element of a spaces toolbar
   *   button
   * @returns {SpaceData}
   */
  fromSpaceButtonId(spaceButtonId) {
    if (!spaceButtonId || !this._spaceData.has(spaceButtonId)) {
      return null;
    }
    return this._spaceData.get(spaceButtonId);
  }

  /**
   * Create a new space and return its SpaceData.
   *
   * @param {string} name - name of the space as used by the extension
   * @param {string} defaultUrl - the url for the default space tab
   * @param {ButtonProperties} buttonProperties
   *   @see mail/components/extensions/schemas/spaces.json
   * @param {ExtensionData} extension - the extension the space belongs to
   * @returns {SpaceData}
   */
  async create(name, defaultUrl, buttonProperties, extension) {
    let spaceButtonId = this._getSpaceButtonId(name, extension);
    if (this._spaceData.has(spaceButtonId)) {
      return false;
    }
    return this._add({
      name,
      spaceButtonId,
      tabInSpace: tabInfo => (tabInfo.spaceButtonId == spaceButtonId ? 1 : 0),
      defaultUrl,
      buttonProperties,
      extension,
    });
  }

  /**
   * Return a WebExtension Space object, representing the given spaceData.
   *
   * @param {SpaceData} spaceData
   * @returns {Space} - @see mail/components/extensions/schemas/spaces.json
   */
  convert(spaceData, extension) {
    let space = {
      id: spaceData.spaceId,
      name: spaceData.name,
      isBuiltIn: !spaceData.extension,
      isSelfOwned: spaceData.extension?.id == extension.id,
    };
    if (spaceData.extension && extension.hasPermission("management")) {
      space.extensionId = spaceData.extension.id;
    }
    return space;
  }

  /**
   * Remove a space and its SpaceData from the tracker.
   *
   * @param {SpaceData} spaceData
   */
  remove(spaceData) {
    if (!this._spaceData.has(spaceData.spaceButtonId)) {
      return;
    }
    this._spaceData.delete(spaceData.spaceButtonId);
  }

  /**
   * Update spaceData for a space in the tracker.
   *
   * @param {SpaceData} spaceData
   */
  update(spaceData) {
    if (!this._spaceData.has(spaceData.spaceButtonId)) {
      return;
    }
    this._spaceData.set(spaceData.spaceButtonId, spaceData);
  }

  /**
   * Return the SpaceData of all spaces known to the tracker.
   *
   * @returns {SpaceData[]}
   */
  getAll() {
    return this._spaceData.values();
  }
}

/**
 * Tracks the opening and closing of tabs and maps them between their numeric WebExtension ID and
 * the native tab info objects.
 */
class TabTracker extends TabTrackerBase {
  constructor() {
    super();

    this._tabs = new WeakMap();
    this._browsers = new Map();
    this._tabIds = new Map();
    this._nextId = 1;
    this._movingTabs = new Map();

    this._handleTabDestroyed = this._handleTabDestroyed.bind(this);

    ExtensionSupport.registerWindowListener("ext-sessions", {
      chromeURLs: [MAIN_WINDOW_URI],
      onLoadWindow(window) {
        window.gTabmail.registerTabMonitor({
          monitorName: "extensionSession",
          onTabTitleChanged(aTab) {},
          onTabClosing(aTab) {},
          onTabPersist(aTab) {
            return aTab._ext.extensionSession;
          },
          onTabRestored(aTab, aState) {
            aTab._ext.extensionSession = aState;
          },
          onTabSwitched(aNewTab, aOldTab) {},
          onTabOpened(aTab) {},
        });
      },
    });
  }

  /**
   * Initialize tab tracking listeners the first time that an event listener is added.
   */
  init() {
    if (this.initialized) {
      return;
    }
    this.initialized = true;

    this._handleWindowOpen = this._handleWindowOpen.bind(this);
    this._handleWindowClose = this._handleWindowClose.bind(this);

    windowTracker.addListener("TabClose", this);
    windowTracker.addListener("TabOpen", this);
    windowTracker.addListener("TabSelect", this);
    windowTracker.addOpenListener(this._handleWindowOpen);
    windowTracker.addCloseListener(this._handleWindowClose);

    /* eslint-disable mozilla/balanced-listeners */
    this.on("tab-detached", this._handleTabDestroyed);
    this.on("tab-removed", this._handleTabDestroyed);
    /* eslint-enable mozilla/balanced-listeners */
  }

  /**
   * Returns the numeric ID for the given native tab.
   *
   * @param {NativeTabInfo} nativeTabInfo - The tabmail tabInfo for which to return an ID
   * @returns {Integer} The tab's numeric ID
   */
  getId(nativeTabInfo) {
    let id = this._tabs.get(nativeTabInfo);
    if (id) {
      return id;
    }

    this.init();

    id = this._nextId++;
    this.setId(nativeTabInfo, id);
    return id;
  }

  /**
   * Returns the tab id corresponding to the given browser element.
   *
   * @param {XULElement} browser - The <browser> element to retrieve for
   * @returns {Integer} The tab's numeric ID
   */
  getBrowserTabId(browser) {
    let id = this._browsers.get(browser.browserId);
    if (id) {
      return id;
    }

    let window = browser.browsingContext.topChromeWindow;
    let tabmail = window.document.getElementById("tabmail");
    let tab = tabmail && tabmail.getTabForBrowser(browser);

    if (tab) {
      id = this.getId(tab);
      this._browsers.set(browser.browserId, id);
      return id;
    }
    if (windowTracker.isSecondaryWindow(window)) {
      return this.getId(window);
    }
    return -1;
  }

  /**
   * Records the tab information for the given tabInfo object.
   *
   * @param {NativeTabInfo} nativeTabInfo - The tab info to record for
   * @param {Integer} id - The tab id to record
   */
  setId(nativeTabInfo, id) {
    this._tabs.set(nativeTabInfo, id);
    let browser = getTabBrowser(nativeTabInfo);
    if (browser) {
      this._browsers.set(browser.browserId, id);
    }
    this._tabIds.set(id, nativeTabInfo);
  }

  /**
   * Function to call when a tab was close, deletes tab information for the tab.
   *
   * @param {Event} event - The event triggering the detroyal
   * @param {{ nativeTabInfo:NativeTabInfo}} - The object containing tab info
   */
  _handleTabDestroyed(event, { nativeTabInfo }) {
    let id = this._tabs.get(nativeTabInfo);
    if (id) {
      this._tabs.delete(nativeTabInfo);
      if (nativeTabInfo.browser) {
        this._browsers.delete(nativeTabInfo.browser.browserId);
      }
      if (this._tabIds.get(id) === nativeTabInfo) {
        this._tabIds.delete(id);
      }
    }
  }

  /**
   * Returns the native tab with the given numeric ID.
   *
   * @param {Integer} tabId - The numeric ID of the tab to return.
   * @param {*} default_ - The value to return if no tab exists with the given ID.
   * @returns {NativeTabInfo} The tab information for the given id.
   */
  getTab(tabId, default_ = undefined) {
    let nativeTabInfo = this._tabIds.get(tabId);
    if (nativeTabInfo) {
      return nativeTabInfo;
    }
    if (default_ !== undefined) {
      return default_;
    }
    throw new ExtensionError(`Invalid tab ID: ${tabId}`);
  }

  /**
   * Handles load events for recently-opened windows, and adds additional
   * listeners which may only be safely added when the window is fully loaded.
   *
   * @param {Event} event - A DOM event to handle.
   */
  handleEvent(event) {
    let nativeTabInfo = event.detail.tabInfo;

    switch (event.type) {
      case "TabOpen": {
        // Save the current tab, since the newly-created tab will likely be
        // active by the time the promise below resolves and the event is
        // dispatched.
        let tabmail = event.target.ownerDocument.getElementById("tabmail");
        let currentTab = tabmail.selectedTab;
        // We need to delay sending this event until the next tick, since the
        // tab does not have its final index when the TabOpen event is dispatched.
        Promise.resolve().then(() => {
          if (event.detail.moving) {
            let srcTabId = this._movingTabs.get(event.detail.moving);
            this.setId(nativeTabInfo, srcTabId);
            this._movingTabs.delete(event.detail.moving);

            this.emitAttached(nativeTabInfo);
          } else {
            this.emitCreated(nativeTabInfo, currentTab);
          }
        });
        break;
      }

      case "TabClose": {
        if (event.detail.moving) {
          this._movingTabs.set(event.detail.moving, this.getId(nativeTabInfo));
          this.emitDetached(nativeTabInfo);
        } else {
          this.emitRemoved(nativeTabInfo, false);
        }
        break;
      }

      case "TabSelect":
        // Because we are delaying calling emitCreated above, we also need to
        // delay sending this event because it shouldn't fire before onCreated.
        Promise.resolve().then(() => {
          this.emitActivated(nativeTabInfo, event.detail.previousTabInfo);
        });
        break;
    }
  }

  /**
   * A private method which is called whenever a new mail window is opened, and dispatches the
   * necessary events for it.
   *
   * @param {DOMWindow} window - The window being opened.
   */
  _handleWindowOpen(window) {
    if (windowTracker.isSecondaryWindow(window)) {
      this.emit("tab-created", {
        nativeTabInfo: window,
        currentTab: window,
      });
      return;
    }

    let tabmail = window.document.getElementById("tabmail");
    if (!tabmail) {
      return;
    }

    for (let nativeTabInfo of tabmail.tabInfo) {
      this.emitCreated(nativeTabInfo);
    }
  }

  /**
   * A private method which is called whenever a mail window is closed, and dispatches the necessary
   * events for it.
   *
   * @param {DOMWindow} window - The window being closed.
   */
  _handleWindowClose(window) {
    if (windowTracker.isSecondaryWindow(window)) {
      this.emit("tab-removed", {
        nativeTabInfo: window,
        tabId: this.getId(window),
        windowId: windowTracker.getId(getTabWindow(window)),
        isWindowClosing: true,
      });
      return;
    }

    let tabmail = window.document.getElementById("tabmail");
    if (!tabmail) {
      return;
    }

    for (let nativeTabInfo of tabmail.tabInfo) {
      this.emitRemoved(nativeTabInfo, true);
    }
  }

  /**
   * Emits a "tab-activated" event for the given tab info.
   *
   * @param {NativeTabInfo} nativeTabInfo - The tab info which has been activated.
   * @param {NativeTab} previousTabInfo - The previously active tab element.
   */
  emitActivated(nativeTabInfo, previousTabInfo) {
    let previousTabId;
    if (previousTabInfo && !previousTabInfo.closed) {
      previousTabId = this.getId(previousTabInfo);
    }
    this.emit("tab-activated", {
      tabId: this.getId(nativeTabInfo),
      previousTabId,
      windowId: windowTracker.getId(getTabWindow(nativeTabInfo)),
    });
  }

  /**
   * Emits a "tab-attached" event for the given tab info.
   *
   * @param {NativeTabInfo} nativeTabInfo - The tab info which is being attached.
   */
  emitAttached(nativeTabInfo) {
    let tabId = this.getId(nativeTabInfo);
    let browser = getTabBrowser(nativeTabInfo);
    let tabmail = browser.ownerDocument.getElementById("tabmail");
    let tabIndex = tabmail._getTabContextForTabbyThing(nativeTabInfo)[0];
    let newWindowId = windowTracker.getId(browser.ownerGlobal);

    this.emit("tab-attached", {
      nativeTabInfo,
      tabId,
      newWindowId,
      newPosition: tabIndex,
    });
  }

  /**
   * Emits a "tab-detached" event for the given tab info.
   *
   * @param {NativeTabInfo} nativeTabInfo - The tab info which is being detached.
   */
  emitDetached(nativeTabInfo) {
    let tabId = this.getId(nativeTabInfo);
    let browser = getTabBrowser(nativeTabInfo);
    let tabmail = browser.ownerDocument.getElementById("tabmail");
    let tabIndex = tabmail._getTabContextForTabbyThing(nativeTabInfo)[0];
    let oldWindowId = windowTracker.getId(browser.ownerGlobal);

    this.emit("tab-detached", {
      nativeTabInfo,
      tabId,
      oldWindowId,
      oldPosition: tabIndex,
    });
  }

  /**
   * Emits a "tab-created" event for the given tab info.
   *
   * @param {NativeTabInfo} nativeTabInfo - The tab info which is being created.
   * @param {?NativeTab} currentTab - The tab info for the currently active tab.
   */
  emitCreated(nativeTabInfo, currentTab) {
    this.emit("tab-created", { nativeTabInfo, currentTab });
  }

  /**
   * Emits a "tab-removed" event for the given tab info.
   *
   * @param {NativeTabInfo} nativeTabInfo - The tab info in the window to which the tab is being
   *                                          removed
   * @param {boolean} isWindowClosing - If true, the window with these tabs is closing
   */
  emitRemoved(nativeTabInfo, isWindowClosing) {
    this.emit("tab-removed", {
      nativeTabInfo,
      tabId: this.getId(nativeTabInfo),
      windowId: windowTracker.getId(getTabWindow(nativeTabInfo)),
      isWindowClosing,
    });
  }

  /**
   * Returns tab id and window id for the given browser element.
   *
   * @param {Element} browser - The browser element to check
   * @returns {{ tabId:Integer, windowId:Integer }} The browsing data for the element
   */
  getBrowserData(browser) {
    return {
      tabId: this.getBrowserTabId(browser),
      windowId: windowTracker.getId(browser.ownerGlobal),
    };
  }

  /**
   * Returns the active tab info for the given window
   *
   * @property {?NativeTabInfo} activeTab       The active tab
   * @readonly
   */
  get activeTab() {
    let window = windowTracker.topWindow;
    let tabmail = window && window.document.getElementById("tabmail");
    return tabmail ? tabmail.selectedTab : window;
  }
}

tabTracker = new TabTracker();
spaceTracker = new SpaceTracker();
windowTracker = new WindowTracker();
Object.assign(global, { tabTracker, spaceTracker, windowTracker });

/**
 * Extension-specific wrapper around a Thunderbird tab. Note that for actual
 * tabs in the main window, some of these methods are overridden by the
 * TabmailTab subclass.
 */
class Tab extends TabBase {
  get spaceId() {
    let tabWindow = getTabWindow(this.nativeTab);
    if (getWebExtensionWindowType(tabWindow) != "normal") {
      return undefined;
    }

    let spaceData = spaceTracker.findSpaceForTab(this.nativeTab);
    return spaceData?.spaceId ?? undefined;
  }

  /** What sort of tab is this? */
  get type() {
    switch (this.nativeTab.location?.href) {
      case COMPOSE_WINDOW_URI:
        return "messageCompose";
      case MESSAGE_WINDOW_URI:
        return "messageDisplay";
      case POPUP_WINDOW_URI:
        return "content";
      default:
        return null;
    }
  }

  /** Overrides the matches function to enable querying for tab types. */
  matches(queryInfo, context) {
    // If the query includes url or title, but this is a non-browser tab, return
    // false directly.
    if ((queryInfo.url || queryInfo.title) && !this.browser) {
      return false;
    }
    let result = super.matches(queryInfo, context);

    let type = queryInfo.mailTab ? "mail" : queryInfo.type;
    if (result && type && this.type != type) {
      return false;
    }

    if (result && queryInfo.spaceId && this.spaceId != queryInfo.spaceId) {
      return false;
    }

    return result;
  }

  /** Adds the mailTab property and removes some useless properties from a tab object. */
  convert(fallback) {
    let result = super.convert(fallback);
    result.spaceId = this.spaceId;
    result.type = this.type;
    result.mailTab = result.type == "mail";

    // These properties are not useful to Thunderbird extensions and are not returned.
    for (let key of [
      "attention",
      "audible",
      "discarded",
      "hidden",
      "incognito",
      "isArticle",
      "isInReaderMode",
      "lastAccessed",
      "mutedInfo",
      "pinned",
      "sharingState",
      "successorTabId",
    ]) {
      delete result[key];
    }

    return result;
  }

  /** Always returns false. This feature doesn't exist in Thunderbird. */
  get _incognito() {
    return false;
  }

  /** Returns the XUL browser for the tab. */
  get browser() {
    if (this.type == "messageCompose") {
      return this.nativeTab.GetCurrentEditorElement();
    }
    if (this.nativeTab.getBrowser) {
      return this.nativeTab.getBrowser();
    }
    return null;
  }

  get innerWindowID() {
    if (!this.browser) {
      return null;
    }
    if (this.type == "messageCompose") {
      return this.browser.contentWindow.windowUtils.currentInnerWindowID;
    }
    return super.innerWindowID;
  }

  /** Returns the frame loader for the tab. */
  get frameLoader() {
    // If we don't have a frameLoader yet, just return a dummy with no width and
    // height.
    return super.frameLoader || { lazyWidth: 0, lazyHeight: 0 };
  }

  /** Returns false if the current tab does not have a url associated. */
  get matchesHostPermission() {
    if (!this._url) {
      return false;
    }
    return super.matchesHostPermission;
  }

  /** Returns the current URL of this tab, without permission checks. */
  get _url() {
    if (this.type == "messageCompose") {
      return undefined;
    }
    return this.browser?.currentURI?.spec;
  }

  /** Returns the current title of this tab, without permission checks. */
  get _title() {
    if (this.browser && this.browser.contentTitle) {
      return this.browser.contentTitle;
    }
    return this.nativeTab.label;
  }

  /** Returns the favIcon, without permission checks. */
  get _favIconUrl() {
    return null;
  }

  /** Returns the last accessed time. */
  get lastAccessed() {
    return 0;
  }

  /** Returns the audible state. */
  get audible() {
    return false;
  }

  /** Returns the cookie store id. */
  get cookieStoreId() {
    if (this.browser && this.browser.contentPrincipal) {
      return getCookieStoreIdForOriginAttributes(
        this.browser.contentPrincipal.originAttributes
      );
    }

    return DEFAULT_STORE;
  }

  /** Returns the discarded state. */
  get discarded() {
    return false;
  }

  /** Returns the tab height. */
  get height() {
    return this.frameLoader.lazyHeight;
  }

  /** Returns hidden status. */
  get hidden() {
    return false;
  }

  /** Returns the tab index. */
  get index() {
    return 0;
  }

  /** Returns information about the muted state of the tab. */
  get mutedInfo() {
    return { muted: false };
  }

  /** Returns information about the sharing state of the tab. */
  get sharingState() {
    return { camera: false, microphone: false, screen: false };
  }

  /** Returns the pinned state of the tab. */
  get pinned() {
    return false;
  }

  /** Returns the active state of the tab. */
  get active() {
    return true;
  }

  /** Returns the highlighted state of the tab. */
  get highlighted() {
    return this.active;
  }

  /** Returns the selected state of the tab. */
  get selected() {
    return this.active;
  }

  /** Returns the loading status of the tab. */
  get status() {
    let isComplete;
    switch (this.type) {
      case "messageDisplay":
      case "addressBook":
        isComplete = this.browser?.contentDocument?.readyState == "complete";
        break;
      case "mail":
        {
          // If the messagePane is hidden or all browsers are hidden, there is
          // nothing to be loaded and we should return complete.
          let about3Pane = this.nativeTab.chromeBrowser.contentWindow;
          isComplete =
            !about3Pane.paneLayout?.messagePaneVisible ||
            this.browser?.webProgress?.isLoadingDocument === false ||
            (about3Pane.webBrowser?.hidden &&
              about3Pane.messageBrowser?.hidden &&
              about3Pane.multiMessageBrowser?.hidden);
        }
        break;
      case "content":
      case "special":
        isComplete = this.browser?.webProgress?.isLoadingDocument === false;
        break;
      default:
        // All other tabs (chat, task, calendar, messageCompose) do not fire the
        // tabs.onUpdated event (Bug 1827929). Let them always be complete.
        isComplete = true;
    }
    return isComplete ? "complete" : "loading";
  }

  /** Returns the width of the tab. */
  get width() {
    return this.frameLoader.lazyWidth;
  }

  /** Returns the native window object of the tab. */
  get window() {
    return this.nativeTab;
  }

  /** Returns the window id of the tab. */
  get windowId() {
    return windowTracker.getId(this.window);
  }

  /** Returns the attention state of the tab. */
  get attention() {
    return false;
  }

  /** Returns the article state of the tab. */
  get isArticle() {
    return false;
  }

  /** Returns the reader mode state of the tab. */
  get isInReaderMode() {
    return false;
  }

  /** Returns the id of the successor tab of the tab. */
  get successorTabId() {
    return -1;
  }
}

class TabmailTab extends Tab {
  constructor(extension, nativeTab, id) {
    if (nativeTab.localName == "tab") {
      let tabmail = nativeTab.ownerDocument.getElementById("tabmail");
      nativeTab = tabmail._getTabContextForTabbyThing(nativeTab)[1];
    }
    super(extension, nativeTab, id);
  }

  /** What sort of tab is this? */
  get type() {
    switch (this.nativeTab.mode.name) {
      case "mail3PaneTab":
        return "mail";
      case "addressBookTab":
        return "addressBook";
      case "mailMessageTab":
        return "messageDisplay";
      case "contentTab": {
        let currentURI = this.nativeTab.browser.currentURI;
        if (currentURI?.schemeIs("about")) {
          switch (currentURI.filePath) {
            case "accountprovisioner":
              return "accountProvisioner";
            case "blank":
              return "content";
            default:
              return "special";
          }
        }
        if (currentURI?.schemeIs("chrome")) {
          return "special";
        }
        return "content";
      }
      case "calendar":
      case "calendarEvent":
      case "calendarTask":
      case "tasks":
      case "chat":
        return this.nativeTab.mode.name;
      case "provisionerCheckoutTab":
      case "glodaFacet":
      case "preferencesTab":
        return "special";
      default:
        // We should not get here, unless a new type is registered with tabmail.
        return null;
    }
  }

  /** Returns the XUL browser for the tab. */
  get browser() {
    return getTabBrowser(this.nativeTab);
  }

  /** Returns the favIcon, without permission checks. */
  get _favIconUrl() {
    return this.nativeTab.favIconUrl;
  }

  /** Returns the tabmail element for the tab. */
  get tabmail() {
    return getTabTabmail(this.nativeTab);
  }

  /** Returns the tab index. */
  get index() {
    return this.tabmail.tabInfo.indexOf(this.nativeTab);
  }

  /** Returns the active state of the tab. */
  get active() {
    return this.nativeTab == this.tabmail.selectedTab;
  }

  /** Returns the title of the tab, without permission checks. */
  get _title() {
    if (this.browser && this.browser.contentTitle) {
      return this.browser.contentTitle;
    }
    // Do we want to be using this.nativeTab.title instead? The difference is
    // that the tabNode label may use defaultTabTitle instead, but do we want to
    // send this out?
    return this.nativeTab.tabNode.getAttribute("label");
  }

  /** Returns the native window object of the tab. */
  get window() {
    return this.tabmail.ownerGlobal;
  }
}

/**
 * Extension-specific wrapper around a Thunderbird window.
 */
class Window extends WindowBase {
  /**
   * @property {string} type - The type of the window, as defined by the
   *   WebExtension API.
   * @see mail/components/extensions/schemas/windows.json
   * @readonly
   */
  get type() {
    let type = getWebExtensionWindowType(this.window);
    if (!type) {
      throw new ExtensionError(
        "Windows API encountered an invalid window type."
      );
    }
    return type;
  }

  /** Returns the title of the tab, without permission checks. */
  get _title() {
    return this.window.document.title;
  }

  /** Returns the title of the tab, checking tab permissions. */
  get title() {
    // Thunderbird can have an empty active tab while a window is loading
    if (this.activeTab && this.activeTab.hasTabPermission) {
      return this._title;
    }
    return null;
  }

  /**
   * Sets the title preface of the window.
   *
   * @param {string} titlePreface - The title preface to set
   */
  setTitlePreface(titlePreface) {
    this.window.document.documentElement.setAttribute(
      "titlepreface",
      titlePreface
    );
  }

  /** Gets the foucsed state of the window. */
  get focused() {
    return this.window.document.hasFocus();
  }

  /** Gets the top position of the window. */
  get top() {
    return this.window.screenY;
  }

  /** Gets the left position of the window. */
  get left() {
    return this.window.screenX;
  }

  /** Gets the width of the window. */
  get width() {
    return this.window.outerWidth;
  }

  /** Gets the height of the window. */
  get height() {
    return this.window.outerHeight;
  }

  /** Gets the private browsing status of the window. */
  get incognito() {
    return false;
  }

  /** Checks if the window is considered always on top. */
  get alwaysOnTop() {
    return this.appWindow.zLevel >= Ci.nsIAppWindow.raisedZ;
  }

  /** Checks if the window was the last one focused. */
  get isLastFocused() {
    return this.window === windowTracker.topWindow;
  }

  /**
   * Returns the window state for the given window.
   *
   * @param {DOMWindow} window - The window to check
   * @returns {string} "maximized", "minimized", "normal" or "fullscreen"
   */
  static getState(window) {
    const STATES = {
      [window.STATE_MAXIMIZED]: "maximized",
      [window.STATE_MINIMIZED]: "minimized",
      [window.STATE_NORMAL]: "normal",
    };
    let state = STATES[window.windowState];
    if (window.fullScreen) {
      state = "fullscreen";
    }
    return state;
  }

  /** Returns the window state for this specific window. */
  get state() {
    return Window.getState(this.window);
  }

  /**
   * Sets the window state for this specific window.
   *
   * @param {string} state - "maximized", "minimized", "normal" or "fullscreen"
   */
  async setState(state) {
    let { window } = this;
    const expectedState = (function () {
      switch (state) {
        case "maximized":
          return window.STATE_MAXIMIZED;
        case "minimized":
        case "docked":
          return window.STATE_MINIMIZED;
        case "normal":
          return window.STATE_NORMAL;
        case "fullscreen":
          return window.STATE_FULLSCREEN;
      }
      throw new ExtensionError(`Unexpected window state: ${state}`);
    })();

    const initialState = window.windowState;
    if (expectedState == initialState) {
      return;
    }

    // We check for window.fullScreen here to make sure to exit fullscreen even
    // if DOM and widget disagree on what the state is. This is a speculative
    // fix for bug 1780876, ideally it should not be needed.
    if (initialState == window.STATE_FULLSCREEN || window.fullScreen) {
      window.fullScreen = false;
    }

    switch (expectedState) {
      case window.STATE_MAXIMIZED:
        window.maximize();
        break;
      case window.STATE_MINIMIZED:
        window.minimize();
        break;

      case window.STATE_NORMAL:
        // Restore sometimes returns the window to its previous state, rather
        // than to the "normal" state, so it may need to be called anywhere from
        // zero to two times.
        window.restore();
        if (window.windowState !== window.STATE_NORMAL) {
          window.restore();
        }
        if (window.windowState !== window.STATE_NORMAL) {
          // And on OS-X, where normal vs. maximized is basically a heuristic,
          // we need to cheat.
          window.sizeToContent();
        }
        break;

      case window.STATE_FULLSCREEN:
        window.fullScreen = true;
        break;

      default:
        throw new ExtensionError(`Unexpected window state: ${state}`);
    }

    if (window.windowState != expectedState) {
      // On Linux, sizemode changes are asynchronous. Some of them might not
      // even happen if the window manager doesn't want to, so wait for a bit
      // instead of forever for a sizemode change that might not ever happen.
      const noWindowManagerTimeout = 2000;

      let onSizeModeChange;
      const promiseExpectedSizeMode = new Promise(resolve => {
        onSizeModeChange = function () {
          if (window.windowState == expectedState) {
            resolve();
          }
        };
        window.addEventListener("sizemodechange", onSizeModeChange);
      });

      await Promise.any([
        promiseExpectedSizeMode,
        new Promise(resolve =>
          window.setTimeout(resolve, noWindowManagerTimeout)
        ),
      ]);
      window.removeEventListener("sizemodechange", onSizeModeChange);
    }

    if (window.windowState != expectedState) {
      console.warn(
        `Window manager refused to set window to state ${expectedState}.`
      );
    }
  }

  /**
   * Retrieves the (relevant) tabs in this window.
   *
   * @yields {Tab}      The wrapped Tab in this window
   */
  *getTabs() {
    let { tabManager } = this.extension;
    yield tabManager.getWrapper(this.window);
  }

  /**
   * Returns an iterator of TabBase objects for the highlighted tab in this
   * window. This is an alias for the active tab.
   *
   * @returns {Iterator<TabBase>}
   */
  *getHighlightedTabs() {
    yield this.activeTab;
  }

  /** Retrieves the active tab in this window */
  get activeTab() {
    let { tabManager } = this.extension;
    return tabManager.getWrapper(this.window);
  }

  /**
   * Retrieves the tab at the given index.
   *
   * @param {number} index - The index to look at
   * @returns {Tab} The wrapped tab at the index
   */
  getTabAtIndex(index) {
    let { tabManager } = this.extension;
    if (index == 0) {
      return tabManager.getWrapper(this.window);
    }
    return null;
  }
}

class TabmailWindow extends Window {
  /** Returns the tabmail element for the tab. */
  get tabmail() {
    return this.window.document.getElementById("tabmail");
  }

  /**
   * Retrieves the (relevant) tabs in this window.
   *
   * @yields {Tab}      The wrapped Tab in this window
   */
  *getTabs() {
    let { tabManager } = this.extension;

    for (let nativeTabInfo of this.tabmail.tabInfo) {
      // Only tabs that have a browser element.
      yield tabManager.getWrapper(nativeTabInfo);
    }
  }

  /** Retrieves the active tab in this window */
  get activeTab() {
    let { tabManager } = this.extension;
    let selectedTab = this.tabmail.selectedTab;
    if (selectedTab) {
      return tabManager.getWrapper(selectedTab);
    }
    return null;
  }

  /**
   * Retrieves the tab at the given index.
   *
   * @param {number} index - The index to look at
   * @returns {Tab} The wrapped tab at the index
   */
  getTabAtIndex(index) {
    let { tabManager } = this.extension;
    let nativeTabInfo = this.tabmail.tabInfo[index];
    if (nativeTabInfo) {
      return tabManager.getWrapper(nativeTabInfo);
    }
    return null;
  }
}

Object.assign(global, { Tab, Window });

/**
 * Manages native tabs, their wrappers, and their dynamic permissions for a particular extension.
 */
class TabManager extends TabManagerBase {
  /**
   * Returns a Tab wrapper for the tab with the given ID.
   *
   * @param {integer} tabId - The ID of the tab for which to return a wrapper.
   * @param {*} default_ - The value to return if no tab exists with the given ID.
   * @returns {Tab|*} The wrapped tab, or the default value
   */
  get(tabId, default_ = undefined) {
    let nativeTabInfo = tabTracker.getTab(tabId, default_);

    if (nativeTabInfo) {
      return this.getWrapper(nativeTabInfo);
    }
    return default_;
  }

  /**
   * If the extension has requested activeTab permission, grant it those permissions for the current
   * inner window in the given native tab.
   *
   * @param {NativeTabInfo} nativeTabInfo - The native tab for which to grant permissions.
   */
  addActiveTabPermission(nativeTabInfo = tabTracker.activeTab) {
    if (nativeTabInfo.browser) {
      super.addActiveTabPermission(nativeTabInfo);
    }
  }

  /**
   * Revoke the extension's activeTab permissions for the current inner window of the given native
   * tab.
   *
   * @param {NativeTabInfo} nativeTabInfo - The native tab for which to revoke permissions.
   */
  revokeActiveTabPermission(nativeTabInfo = tabTracker.activeTab) {
    super.revokeActiveTabPermission(nativeTabInfo);
  }

  /**
   * Determines access using extension context.
   *
   * @param {NativeTab} nativeTab
   *        The tab to check access on.
   * @returns {boolean}
   *        True if the extension has permissions for this tab.
   */
  canAccessTab(nativeTab) {
    return true;
  }

  /**
   * Returns a new Tab instance wrapping the given native tab info.
   *
   * @param {NativeTabInfo} nativeTabInfo - The native tab for which to return a wrapper.
   * @returns {Tab} The wrapped native tab
   */
  wrapTab(nativeTabInfo) {
    let tabClass = TabmailTab;
    if (nativeTabInfo instanceof Ci.nsIDOMWindow) {
      tabClass = Tab;
    }
    return new tabClass(
      this.extension,
      nativeTabInfo,
      tabTracker.getId(nativeTabInfo)
    );
  }
}

/**
 * Manages native browser windows and their wrappers for a particular extension.
 */
class WindowManager extends WindowManagerBase {
  /**
   * Returns a Window wrapper for the mail window with the given ID.
   *
   * @param {Integer} windowId - The ID of the browser window for which to return a wrapper.
   * @param {BaseContext} context - The extension context for which the matching is being performed.
   *                                  Used to determine the current window for relevant properties.
   * @returns {Window} The wrapped window
   */
  get(windowId, context) {
    let window = windowTracker.getWindow(windowId, context);
    return this.getWrapper(window);
  }

  /**
   * Yields an iterator of WindowBase wrappers for each currently existing browser window.
   *
   * @yields {Window}
   */
  *getAll() {
    for (let window of windowTracker.browserWindows()) {
      yield this.getWrapper(window);
    }
  }

  /**
   * Returns a new Window instance wrapping the given mail window.
   *
   * @param {DOMWindow} window - The mail window for which to return a wrapper.
   * @returns {Window} The wrapped window
   */
  wrapWindow(window) {
    let windowClass = Window;
    if (
      window.document.documentElement.getAttribute("windowtype") == "mail:3pane"
    ) {
      windowClass = TabmailWindow;
    }
    return new windowClass(this.extension, window, windowTracker.getId(window));
  }
}

/**
 * Wait until the normal window identified by the given windowId has finished its
 * delayed startup. Returns its DOMWindow when done. Waits for the top normal
 * window, if no window is specified.
 *
 * @param {*} [context] - a WebExtension context
 * @param {*} [windowId] - a WebExtension window id
 * @returns {DOMWindow}
 */
async function getNormalWindowReady(context, windowId) {
  let window;
  if (windowId) {
    let win = context.extension.windowManager.get(windowId, context);
    if (win.type != "normal") {
      throw new ExtensionError(
        `Window with ID ${windowId} is not a normal window`
      );
    }
    window = win.window;
  } else {
    window = windowTracker.topNormalWindow;
  }

  // Wait for session restore.
  await new Promise(resolve => {
    if (!window.SessionStoreManager._restored) {
      let obs = (observedWindow, topic, data) => {
        if (observedWindow != window) {
          return;
        }
        Services.obs.removeObserver(obs, "mail-tabs-session-restored");
        resolve();
      };
      Services.obs.addObserver(obs, "mail-tabs-session-restored");
    } else {
      resolve();
    }
  });

  // Wait for all mail3PaneTab's to have been fully restored and loaded.
  for (let tabInfo of window.gTabmail.tabInfo) {
    let { chromeBrowser, mode, closed } = tabInfo;
    if (!closed && mode.name == "mail3PaneTab") {
      await new Promise(resolve => {
        if (
          chromeBrowser.contentDocument.readyState == "complete" &&
          chromeBrowser.currentURI.spec == "about:3pane"
        ) {
          resolve();
        } else {
          chromeBrowser.contentWindow.addEventListener(
            "load",
            () => resolve(),
            {
              once: true,
            }
          );
        }
      });
    }
  }

  return window;
}

/**
 * Converts an nsIMsgAccount to a simple object
 *
 * @param {nsIMsgAccount} account
 * @returns {object}
 */
function convertAccount(account, includeFolders = true) {
  if (!account) {
    return null;
  }

  account = account.QueryInterface(Ci.nsIMsgAccount);
  let server = account.incomingServer;
  if (server.type == "im") {
    return null;
  }

  let folders = null;
  if (includeFolders) {
    folders = traverseSubfolders(
      account.incomingServer.rootFolder,
      account.key
    ).subFolders;
  }

  return {
    id: account.key,
    name: account.incomingServer.prettyName,
    type: account.incomingServer.type,
    folders,
    identities: account.identities.map(identity =>
      convertMailIdentity(account, identity)
    ),
  };
}

/**
 * Converts an nsIMsgIdentity to a simple object for use in messages.
 *
 * @param {nsIMsgAccount} account
 * @param {nsIMsgIdentity} identity
 * @returns {object}
 */
function convertMailIdentity(account, identity) {
  if (!account || !identity) {
    return null;
  }
  identity = identity.QueryInterface(Ci.nsIMsgIdentity);
  return {
    accountId: account.key,
    id: identity.key,
    label: identity.label || "",
    name: identity.fullName || "",
    email: identity.email || "",
    replyTo: identity.replyTo || "",
    organization: identity.organization || "",
    composeHtml: identity.composeHtml,
    signature: identity.htmlSigText || "",
    signatureIsPlainText: !identity.htmlSigFormat,
  };
}

/**
 * The following functions turn nsIMsgFolder references into more human-friendly forms.
 * A folder can be referenced with the account key, and the path to the folder in that account.
 */

/**
 * Convert a folder URI to a human-friendly path.
 *
 * @returns {string}
 */
function folderURIToPath(accountId, uri) {
  let server = MailServices.accounts.getAccount(accountId).incomingServer;
  let rootURI = server.rootFolder.URI;
  if (rootURI == uri) {
    return "/";
  }
  // The .URI property of an IMAP folder doesn't have %-encoded characters, but
  // may include literal % chars. Services.io.newURI(uri) applies encodeURI to
  // the returned filePath, but will not encode any literal % chars, which will
  // cause decodeURIComponent to fail (bug 1707408).
  if (server.type == "imap") {
    return uri.substring(rootURI.length);
  }
  let path = Services.io.newURI(uri).filePath;
  return path.split("/").map(decodeURIComponent).join("/");
}

/**
 * Convert a human-friendly path to a folder URI. This function does not assume
 * that the folder referenced exists.
 *
 * @returns {string}
 */
function folderPathToURI(accountId, path) {
  let server = MailServices.accounts.getAccount(accountId).incomingServer;
  let rootURI = server.rootFolder.URI;
  if (path == "/") {
    return rootURI;
  }
  // The .URI property of an IMAP folder doesn't have %-encoded characters.
  // If encoded here, the folder lookup service won't find the folder.
  if (server.type == "imap") {
    return rootURI + path;
  }
  return (
    rootURI +
    path
      .split("/")
      .map(p =>
        encodeURIComponent(p)
          .replace(/[!'()*]/g, c => "%" + c.charCodeAt(0).toString(16))
          // We do not encode "+" chars in folder URIs. Manually convert them
          // back to literal + chars, otherwise folder lookup will fail.
          .replaceAll("%2B", "+")
      )
      .join("/")
  );
}

const folderTypeMap = new Map([
  [Ci.nsMsgFolderFlags.Inbox, "inbox"],
  [Ci.nsMsgFolderFlags.Drafts, "drafts"],
  [Ci.nsMsgFolderFlags.SentMail, "sent"],
  [Ci.nsMsgFolderFlags.Trash, "trash"],
  [Ci.nsMsgFolderFlags.Templates, "templates"],
  [Ci.nsMsgFolderFlags.Archive, "archives"],
  [Ci.nsMsgFolderFlags.Junk, "junk"],
  [Ci.nsMsgFolderFlags.Queue, "outbox"],
]);

/**
 * Converts an nsIMsgFolder to a simple object for use in API messages.
 *
 * @param {nsIMsgFolder} folder - The folder to convert.
 * @param {string} [accountId] - An optimization to avoid looking up the
 *     account. The value from nsIMsgHdr.accountKey must not be used here.
 * @returns {MailFolder}
 * @see mail/components/extensions/schemas/folders.json
 */
function convertFolder(folder, accountId) {
  if (!folder) {
    return null;
  }
  if (!accountId) {
    let server = folder.server;
    let account = MailServices.accounts.FindAccountForServer(server);
    accountId = account.key;
  }

  let folderObject = {
    accountId,
    name: folder.prettyName,
    path: folderURIToPath(accountId, folder.URI),
  };

  for (let [flag, typeName] of folderTypeMap.entries()) {
    if (folder.flags & flag) {
      folderObject.type = typeName;
    }
  }

  return folderObject;
}

/**
 * Converts an nsIMsgFolder and all its subfolders to a simple object for use in
 * API messages.
 *
 * @param {nsIMsgFolder} folder - The folder to convert.
 * @param {string} [accountId] - An optimization to avoid looking up the
 *     account. The value from nsIMsgHdr.accountKey must not be used here.
 * @returns {MailFolder}
 * @see mail/components/extensions/schemas/folders.json
 */
function traverseSubfolders(folder, accountId) {
  let f = convertFolder(folder, accountId);
  f.subFolders = [];
  if (folder.hasSubFolders) {
    // Use the same order as used by Thunderbird.
    let subFolders = [...folder.subFolders].sort((a, b) =>
      a.sortOrder == b.sortOrder
        ? a.name.localeCompare(b.name)
        : a.sortOrder - b.sortOrder
    );
    for (let subFolder of subFolders) {
      f.subFolders.push(
        traverseSubfolders(subFolder, accountId || f.accountId)
      );
    }
  }
  return f;
}

class FolderManager {
  constructor(extension) {
    this.extension = extension;
  }

  convert(folder, accountId) {
    return convertFolder(folder, accountId);
  }

  get(accountId, path) {
    return MailServices.folderLookup.getFolderForURL(
      folderPathToURI(accountId, path)
    );
  }
}

/**
 * Checks if the provided nsIMsgHdr is a dummy message header of an attached message.
 */
function isAttachedMessage(msgHdr) {
  try {
    return (
      !msgHdr.folder &&
      new URL(msgHdr.getStringProperty("dummyMsgUrl")).searchParams.has("part")
    );
  } catch (ex) {
    return false;
  }
}

/**
 * Converts an nsIMsgHdr to a simple object for use in messages.
 * This function WILL change as the API develops.
 *
 * @param {nsIMsgHdr} msgHdr
 * @param {ExtensionData} extension
 * @returns {MessageHeader} MessageHeader object
 *
 * @see /mail/components/extensions/schemas/messages.json
 */
function convertMessage(msgHdr, extension) {
  if (!msgHdr) {
    return null;
  }

  let composeFields = Cc[
    "@mozilla.org/messengercompose/composefields;1"
  ].createInstance(Ci.nsIMsgCompFields);

  let junkScore = parseInt(msgHdr.getStringProperty("junkscore"), 10) || 0;
  let tags = (msgHdr.getStringProperty("keywords") || "")
    .split(" ")
    .filter(MailServices.tags.isValidKey);

  let external = !msgHdr.folder;

  // Getting the size of attached messages does not work consistently. For imap://
  // and mailbox:// messages the returned size in msgHdr.messageSize is 0, and for
  // file:// messages the returned size is always the total file size
  // Be consistent here and always return 0. The user can obtain the message size
  // from the size of the associated attachment file.
  let size = isAttachedMessage(msgHdr) ? 0 : msgHdr.messageSize;

  let messageObject = {
    id: messageTracker.getId(msgHdr),
    date: new Date(Math.round(msgHdr.date / 1000)),
    author: msgHdr.mime2DecodedAuthor,
    recipients: composeFields.splitRecipients(
      msgHdr.mime2DecodedRecipients,
      false
    ),
    ccList: composeFields.splitRecipients(msgHdr.ccList, false),
    bccList: composeFields.splitRecipients(msgHdr.bccList, false),
    subject: msgHdr.mime2DecodedSubject,
    read: msgHdr.isRead,
    new: !!(msgHdr.flags & Ci.nsMsgMessageFlags.New),
    headersOnly: !!(msgHdr.flags & Ci.nsMsgMessageFlags.Partial),
    flagged: !!msgHdr.isFlagged,
    junk: junkScore >= gJunkThreshold,
    junkScore,
    headerMessageId: msgHdr.messageId,
    size,
    tags,
    external,
  };
  // convertMessage can be called without providing an extension, if the info is
  // needed for multiple extensions. The caller has to ensure that the folder info
  // is not forwarded to extensions, which do not have the required permission.
  if (
    msgHdr.folder &&
    (!extension || extension.hasPermission("accountsRead"))
  ) {
    messageObject.folder = convertFolder(msgHdr.folder);
  }
  return messageObject;
}

/**
 * A map of numeric identifiers to messages for easy reference.
 *
 * @implements {nsIFolderListener}
 * @implements {nsIMsgFolderListener}
 * @implements {nsIObserver}
 */
var messageTracker = new (class extends EventEmitter {
  constructor() {
    super();
    this._nextId = 1;
    this._messages = new Map();
    this._messageIds = new Map();
    this._listenerCount = 0;
    this._pendingKeyChanges = new Map();
    this._dummyMessageHeaders = new Map();

    // nsIObserver
    Services.obs.addObserver(this, "quit-application-granted");
    Services.obs.addObserver(this, "attachment-delete-msgkey-changed");
    // nsIFolderListener
    MailServices.mailSession.AddFolderListener(
      this,
      Ci.nsIFolderListener.propertyFlagChanged |
        Ci.nsIFolderListener.intPropertyChanged
    );
    // nsIMsgFolderListener
    MailServices.mfn.addListener(
      this,
      MailServices.mfn.msgsJunkStatusChanged |
        MailServices.mfn.msgsDeleted |
        MailServices.mfn.msgsMoveCopyCompleted |
        MailServices.mfn.msgKeyChanged
    );

    this._messageOpenListener = {
      registered: false,
      async handleEvent(event) {
        let msgHdr = event.detail;
        // It is not possible to retrieve the dummyMsgHdr of messages opened
        // from file at a later time, track them manually.
        if (
          msgHdr &&
          !msgHdr.folder &&
          msgHdr.getStringProperty("dummyMsgUrl").startsWith("file://")
        ) {
          messageTracker.getId(msgHdr);
        }
      },
    };
    try {
      windowTracker.addListener("MsgLoaded", this._messageOpenListener);
      this._messageOpenListener.registered = true;
    } catch (ex) {
      // Fails during XPCSHELL tests, which mock the WindowWatcher but do not
      // implement registerNotification.
    }
  }

  cleanup() {
    // nsIObserver
    Services.obs.removeObserver(this, "quit-application-granted");
    Services.obs.removeObserver(this, "attachment-delete-msgkey-changed");
    // nsIFolderListener
    MailServices.mailSession.RemoveFolderListener(this);
    // nsIMsgFolderListener
    MailServices.mfn.removeListener(this);
    if (this._messageOpenListener.registered) {
      windowTracker.removeListener("MsgLoaded", this._messageOpenListener);
      this._messageOpenListener.registered = false;
    }
  }

  /**
   * Maps the provided message identifier to the given messageTracker id.
   */
  _set(id, msgIdentifier, msgHdr) {
    let hash = JSON.stringify(msgIdentifier);
    this._messageIds.set(hash, id);
    this._messages.set(id, msgIdentifier);
    // Keep track of dummy message headers, which do not have a folderURI property
    // and cannot be retrieved later.
    if (msgHdr && !msgHdr.folder) {
      this._dummyMessageHeaders.set(msgIdentifier.dummyMsgUrl, msgHdr);
    }
  }

  /**
   * Lookup the messageTracker id for the given message identifier, return null
   * if not known.
   */
  _get(msgIdentifier) {
    let hash = JSON.stringify(msgIdentifier);
    if (this._messageIds.has(hash)) {
      return this._messageIds.get(hash);
    }
    return null;
  }

  /**
   * Removes the provided message identifier from the messageTracker.
   */
  _remove(msgIdentifier) {
    let hash = JSON.stringify(msgIdentifier);
    let id = this._get(msgIdentifier);
    this._messages.delete(id);
    this._messageIds.delete(hash);
    this._dummyMessageHeaders.delete(msgIdentifier.dummyMsgUrl);
  }

  /**
   * Finds a message in the messageTracker or adds it.
   *
   * @returns {int} The messageTracker id of the message
   */
  getId(msgHdr) {
    let msgIdentifier;
    if (msgHdr.folder) {
      msgIdentifier = {
        folderURI: msgHdr.folder.URI,
        messageKey: msgHdr.messageKey,
      };
    } else {
      // Normalize the dummyMsgUrl by sorting its parameters and striping them
      // to a minimum.
      let url = new URL(msgHdr.getStringProperty("dummyMsgUrl"));
      let parameters = Array.from(url.searchParams, p => p[0]).filter(
        p => !["group", "number", "key", "part"].includes(p)
      );
      for (let parameter of parameters) {
        url.searchParams.delete(parameter);
      }
      url.searchParams.sort();

      msgIdentifier = {
        dummyMsgUrl: url.href,
        dummyMsgLastModifiedTime: msgHdr.getUint32Property(
          "dummyMsgLastModifiedTime"
        ),
      };
    }

    let id = this._get(msgIdentifier);
    if (id) {
      return id;
    }
    id = this._nextId++;

    this._set(id, msgIdentifier, new nsDummyMsgHeader(msgHdr));
    return id;
  }

  /**
   * Check if the provided msgIdentifier belongs to a modified file message.
   *
   * @param {*} msgIdentifier - the msgIdentifier object of the message
   * @returns {boolean}
   */
  isModifiedFileMsg(msgIdentifier) {
    if (!msgIdentifier.dummyMsgUrl?.startsWith("file://")) {
      return false;
    }

    try {
      let file = Services.io
        .newURI(msgIdentifier.dummyMsgUrl)
        .QueryInterface(Ci.nsIFileURL).file;
      if (!file?.exists()) {
        throw new ExtensionError("File does not exist");
      }
      if (
        msgIdentifier.dummyMsgLastModifiedTime &&
        Math.floor(file.lastModifiedTime / 1000000) !=
          msgIdentifier.dummyMsgLastModifiedTime
      ) {
        throw new ExtensionError("File has been modified");
      }
    } catch (ex) {
      console.error(ex);
      return true;
    }
    return false;
  }

  /**
   * Retrieves a message from the messageTracker. If the message no longer,
   * exists it is removed from the messageTracker.
   *
   * @returns {nsIMsgHdr} The identifier of the message
   */
  getMessage(id) {
    let msgIdentifier = this._messages.get(id);
    if (!msgIdentifier) {
      return null;
    }

    if (msgIdentifier.folderURI) {
      let folder = MailServices.folderLookup.getFolderForURL(
        msgIdentifier.folderURI
      );
      if (folder) {
        let msgHdr = folder.msgDatabase.getMsgHdrForKey(
          msgIdentifier.messageKey
        );
        if (msgHdr) {
          return msgHdr;
        }
      }
    } else {
      let msgHdr = this._dummyMessageHeaders.get(msgIdentifier.dummyMsgUrl);
      if (msgHdr && !this.isModifiedFileMsg(msgIdentifier)) {
        return msgHdr;
      }
    }

    this._remove(msgIdentifier);
    return null;
  }

  // nsIFolderListener

  onFolderPropertyFlagChanged(item, property, oldFlag, newFlag) {
    let changes = {};
    switch (property) {
      case "Status":
        if ((oldFlag ^ newFlag) & Ci.nsMsgMessageFlags.Read) {
          changes.read = item.isRead;
        }
        if ((oldFlag ^ newFlag) & Ci.nsMsgMessageFlags.New) {
          changes.new = !!(newFlag & Ci.nsMsgMessageFlags.New);
        }
        break;
      case "Flagged":
        changes.flagged = item.isFlagged;
        break;
      case "Keywords":
        {
          let tags = item.getStringProperty("keywords");
          tags = tags ? tags.split(" ") : [];
          changes.tags = tags.filter(MailServices.tags.isValidKey);
        }
        break;
    }
    if (Object.keys(changes).length) {
      this.emit("message-updated", item, changes);
    }
  }

  onFolderIntPropertyChanged(folder, property, oldValue, newValue) {
    switch (property) {
      case "BiffState":
        if (newValue == Ci.nsIMsgFolder.nsMsgBiffState_NewMail) {
          // The folder argument is a root folder.
          this.findNewMessages(folder);
        }
        break;
      case "NewMailReceived":
        // The folder argument is a real folder.
        this.findNewMessages(folder);
        break;
    }
  }

  /**
   * Finds all folders with new messages in the specified changedFolder and
   * returns those.
   *
   * @see MailNotificationManager._getFirstRealFolderWithNewMail()
   */
  findNewMessages(changedFolder) {
    let folders = changedFolder.descendants;
    folders.unshift(changedFolder);
    for (let folder of folders) {
      let flags = folder.flags;
      if (
        !(flags & Ci.nsMsgFolderFlags.Inbox) &&
        flags & (Ci.nsMsgFolderFlags.SpecialUse | Ci.nsMsgFolderFlags.Virtual)
      ) {
        // Do not notify if the folder is not Inbox but one of
        // Drafts|Trash|SentMail|Templates|Junk|Archive|Queue or Virtual.
        continue;
      }
      let numNewMessages = folder.getNumNewMessages(false);
      if (!numNewMessages) {
        continue;
      }
      let msgDb = folder.msgDatabase;
      let newMsgKeys = msgDb.getNewList().slice(-numNewMessages);
      if (newMsgKeys.length == 0) {
        continue;
      }
      this.emit(
        "messages-received",
        folder,
        newMsgKeys.map(key => msgDb.getMsgHdrForKey(key))
      );
    }
  }

  // nsIMsgFolderListener

  msgsJunkStatusChanged(messages) {
    for (let msgHdr of messages) {
      let junkScore = parseInt(msgHdr.getStringProperty("junkscore"), 10) || 0;
      this.emit("message-updated", msgHdr, {
        junk: junkScore >= gJunkThreshold,
      });
    }
  }

  msgsDeleted(deletedMsgs) {
    if (deletedMsgs.length > 0) {
      this.emit("messages-deleted", deletedMsgs);
    }
  }

  msgsMoveCopyCompleted(move, srcMsgs, dstFolder, dstMsgs) {
    if (srcMsgs.length > 0 && dstMsgs.length > 0) {
      let emitMsg = move ? "messages-moved" : "messages-copied";
      this.emit(emitMsg, srcMsgs, dstMsgs);
    }
  }

  msgKeyChanged(oldKey, newMsgHdr) {
    // For IMAP messages there is a delayed update of database keys and if those
    // keys change, the messageTracker needs to update its maps, otherwise wrong
    // messages will be returned. Key changes are replayed in multi-step swaps.
    let newKey = newMsgHdr.messageKey;

    // Replay pending swaps.
    while (this._pendingKeyChanges.has(oldKey)) {
      let next = this._pendingKeyChanges.get(oldKey);
      this._pendingKeyChanges.delete(oldKey);
      oldKey = next;

      // Check if we are left with a no-op swap and exit early.
      if (oldKey == newKey) {
        this._pendingKeyChanges.delete(oldKey);
        return;
      }
    }

    if (oldKey != newKey) {
      // New key swap, log the mirror swap as pending.
      this._pendingKeyChanges.set(newKey, oldKey);

      // Swap tracker entries.
      let oldId = this._get({
        folderURI: newMsgHdr.folder.URI,
        messageKey: oldKey,
      });
      let newId = this._get({
        folderURI: newMsgHdr.folder.URI,
        messageKey: newKey,
      });
      this._set(oldId, { folderURI: newMsgHdr.folder.URI, messageKey: newKey });
      this._set(newId, { folderURI: newMsgHdr.folder.URI, messageKey: oldKey });
    }
  }

  // nsIObserver

  /**
   * Observer to update message tracker if a message has received a new key due
   * to attachments being removed, which we do not consider to be a new message.
   */
  observe(subject, topic, data) {
    if (topic == "attachment-delete-msgkey-changed") {
      data = JSON.parse(data);

      if (data && data.folderURI && data.oldMessageKey && data.newMessageKey) {
        let id = this._get({
          folderURI: data.folderURI,
          messageKey: data.oldMessageKey,
        });
        if (id) {
          // Replace tracker entries.
          this._set(id, {
            folderURI: data.folderURI,
            messageKey: data.newMessageKey,
          });
        }
      }
    } else if (topic == "quit-application-granted") {
      this.cleanup();
    }
  }
})();

/**
 * Tracks lists of messages so that an extension can consume them in chunks.
 * Any WebExtensions method that could return multiple messages should instead call
 * messageListTracker.startList and return the results, which contain the first
 * chunk. Further chunks can be fetched by the extension calling
 * browser.messages.continueList. Chunk size is controlled by a pref.
 */
var messageListTracker = {
  _contextLists: new WeakMap(),

  /**
   * Takes an array or enumerator of messages and returns the first chunk.
   *
   * @returns {object}
   */
  startList(messages, extension) {
    let messageList = this.createList(extension);
    if (Array.isArray(messages)) {
      messages = this._createEnumerator(messages);
    }
    while (messages.hasMoreElements()) {
      let next = messages.getNext();
      messageList.add(next.QueryInterface(Ci.nsIMsgDBHdr));
    }
    messageList.done();
    return this.getNextPage(messageList);
  },

  _createEnumerator(array) {
    let current = 0;
    return {
      hasMoreElements() {
        return current < array.length;
      },
      getNext() {
        return array[current++];
      },
    };
  },

  /**
   * Creates and returns a new messageList object.
   *
   * @returns {object}
   */
  createList(extension) {
    let messageListId = Services.uuid.generateUUID().number.substring(1, 37);
    let messageList = this._createListObject(messageListId, extension);
    let lists = this._contextLists.get(extension);
    if (!lists) {
      lists = new Map();
      this._contextLists.set(extension, lists);
    }
    lists.set(messageListId, messageList);
    return messageList;
  },

  /**
   * Returns the messageList object for a given id.
   *
   * @returns {object}
   */
  getList(messageListId, extension) {
    let lists = this._contextLists.get(extension);
    let messageList = lists ? lists.get(messageListId, null) : null;
    if (!messageList) {
      throw new ExtensionError(
        `No message list for id ${messageListId}. Have you reached the end of a list?`
      );
    }
    return messageList;
  },

  /**
   * Returns the first/next message page of the given messageList.
   *
   * @returns {object}
   */
  async getNextPage(messageList) {
    let messageListId = messageList.id;
    let messages = await messageList.getNextPage();
    if (!messageList.hasMorePages()) {
      let lists = this._contextLists.get(messageList.extension);
      if (lists && lists.has(messageListId)) {
        lists.delete(messageListId);
      }
      messageListId = null;
    }
    return {
      id: messageListId,
      messages,
    };
  },

  _createListObject(messageListId, extension) {
    function getCurrentPage() {
      return pages.length > 0 ? pages[pages.length - 1] : null;
    }

    function addPage() {
      let contents = getCurrentPage();
      let resolvePage = currentPageResolveCallback;

      pages.push([]);
      pagePromises.push(
        new Promise(resolve => {
          currentPageResolveCallback = resolve;
        })
      );

      if (contents && resolvePage) {
        resolvePage(contents);
      }
    }

    let _messageListId = messageListId;
    let _extension = extension;
    let isDone = false;
    let pages = [];
    let pagePromises = [];
    let currentPageResolveCallback = null;
    let readIndex = 0;

    // Add first page.
    addPage();

    return {
      get id() {
        return _messageListId;
      },
      get extension() {
        return _extension;
      },
      add(message) {
        if (isDone) {
          return;
        }
        if (getCurrentPage().length >= gMessagesPerPage) {
          addPage();
        }
        getCurrentPage().push(convertMessage(message, _extension));
      },
      done() {
        if (isDone) {
          return;
        }
        isDone = true;
        currentPageResolveCallback(getCurrentPage());
      },
      hasMorePages() {
        return readIndex < pages.length;
      },
      async getNextPage() {
        if (readIndex >= pages.length) {
          return null;
        }
        const pageContent = await pagePromises[readIndex];
        // Increment readIndex only after pagePromise has resolved, so multiple
        // calls to getNextPage get the same page.
        readIndex++;
        return pageContent;
      },
    };
  },
};

class MessageManager {
  constructor(extension) {
    this.extension = extension;
  }

  convert(msgHdr) {
    return convertMessage(msgHdr, this.extension);
  }

  get(id) {
    return messageTracker.getMessage(id);
  }

  startMessageList(messageList) {
    return messageListTracker.startList(messageList, this.extension);
  }
}

extensions.on("startup", (type, extension) => {
  // eslint-disable-line mozilla/balanced-listeners
  if (extension.hasPermission("accountsRead")) {
    defineLazyGetter(
      extension,
      "folderManager",
      () => new FolderManager(extension)
    );
  }
  if (extension.hasPermission("addressBooks")) {
    defineLazyGetter(extension, "addressBookManager", () => {
      if (!("addressBookCache" in this)) {
        extensions.loadModule("addressBook");
      }
      return {
        findAddressBookById: this.addressBookCache.findAddressBookById.bind(
          this.addressBookCache
        ),
        findContactById: this.addressBookCache.findContactById.bind(
          this.addressBookCache
        ),
        findMailingListById: this.addressBookCache.findMailingListById.bind(
          this.addressBookCache
        ),
        convert: this.addressBookCache.convert.bind(this.addressBookCache),
      };
    });
  }
  if (extension.hasPermission("messagesRead")) {
    defineLazyGetter(
      extension,
      "messageManager",
      () => new MessageManager(extension)
    );
  }
  defineLazyGetter(extension, "tabManager", () => new TabManager(extension));
  defineLazyGetter(
    extension,
    "windowManager",
    () => new WindowManager(extension)
  );
});