summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/test/resources/MessageGenerator.jsm
blob: 4e3f8b75f1d7519c65c535300d46a60565c13f97 (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
/* 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/. */

const EXPORTED_SYMBOLS = [
  "MessageGenerator",
  "addMessagesToFolder",
  "MessageScenarioFactory",
  "SyntheticPartLeaf",
  "SyntheticDegeneratePartEmpty",
  "SyntheticPartMulti",
  "SyntheticPartMultiMixed",
  "SyntheticPartMultiParallel",
  "SyntheticPartMultiDigest",
  "SyntheticPartMultiAlternative",
  "SyntheticPartMultiRelated",
  "SyntheticPartMultiSignedSMIME",
  "SyntheticPartMultiSignedPGP",
  "SyntheticMessage",
  "SyntheticMessageSet",
];

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

/**
 * A list of first names for use by MessageGenerator to create deterministic,
 *  reversible names.  To keep things easily reversible, if you add names, make
 *  sure they have no spaces in them!
 */
var FIRST_NAMES = [
  "Andy",
  "Bob",
  "Chris",
  "David",
  "Emily",
  "Felix",
  "Gillian",
  "Helen",
  "Idina",
  "Johnny",
  "Kate",
  "Lilia",
  "Martin",
  "Neil",
  "Olof",
  "Pete",
  "Quinn",
  "Rasmus",
  "Sarah",
  "Troels",
  "Ulf",
  "Vince",
  "Will",
  "Xavier",
  "Yoko",
  "Zig",
];

/**
 * A list of last names for use by MessageGenerator to create deterministic,
 *  reversible names.  To keep things easily reversible, if you add names, make
 *  sure they have no spaces in them!
 */
var LAST_NAMES = [
  "Anway",
  "Bell",
  "Clarke",
  "Davol",
  "Ekberg",
  "Flowers",
  "Gilbert",
  "Hook",
  "Ivarsson",
  "Jones",
  "Kurtz",
  "Lowe",
  "Morris",
  "Nagel",
  "Orzabal",
  "Price",
  "Quinn",
  "Rolinski",
  "Stanley",
  "Tennant",
  "Ulvaeus",
  "Vannucci",
  "Wiggs",
  "Xavier",
  "Young",
  "Zig",
];

/**
 * A list of adjectives used to construct a deterministic, reversible subject
 *  by MessageGenerator.  To keep things easily reversible, if you add more,
 *  make sure they have no spaces in them!  Also, make sure your additions
 *  don't break the secret Monty Python reference!
 */
var SUBJECT_ADJECTIVES = [
  "Big",
  "Small",
  "Huge",
  "Tiny",
  "Red",
  "Green",
  "Blue",
  "My",
  "Happy",
  "Sad",
  "Grumpy",
  "Angry",
  "Awesome",
  "Fun",
  "Lame",
  "Funky",
];

/**
 * A list of nouns used to construct a deterministic, reversible subject
 *  by MessageGenerator.  To keep things easily reversible, if you add more,
 *  make sure they have no spaces in them!  Also, make sure your additions
 *  don't break the secret Monty Python reference!
 */
var SUBJECT_NOUNS = [
  "Meeting",
  "Party",
  "Shindig",
  "Wedding",
  "Document",
  "Report",
  "Spreadsheet",
  "Hovercraft",
  "Aardvark",
  "Giraffe",
  "Llama",
  "Velociraptor",
  "Laser",
  "Ray-Gun",
  "Pen",
  "Sword",
];

/**
 * A list of suffixes used to construct a deterministic, reversible subject
 *  by MessageGenerator.  These can (clearly) have spaces in them.  Make sure
 *  your additions don't break the secret Monty Python reference!
 */
var SUBJECT_SUFFIXES = [
  "Today",
  "Tomorrow",
  "Yesterday",
  "In a Fortnight",
  "Needs Attention",
  "Very Important",
  "Highest Priority",
  "Full Of Eels",
  "In The Lobby",
  "On Your Desk",
  "In Your Car",
  "Hiding Behind The Door",
];

/**
 * Base class for MIME Part representation.
 */
function SyntheticPart(aProperties) {
  if (aProperties) {
    if ("contentType" in aProperties) {
      this._contentType = aProperties.contentType;
    }
    if ("charset" in aProperties) {
      this._charset = aProperties.charset;
    }
    if ("format" in aProperties) {
      this._format = aProperties.format;
    }
    if ("filename" in aProperties) {
      this._filename = aProperties.filename;
    }
    if ("boundary" in aProperties) {
      this._boundary = aProperties.boundary;
    }
    if ("encoding" in aProperties) {
      this._encoding = aProperties.encoding;
    }
    if ("contentId" in aProperties) {
      this._contentId = aProperties.contentId;
    }
    if ("disposition" in aProperties) {
      this._forceDisposition = aProperties.disposition;
    }
    if ("extraHeaders" in aProperties) {
      this._extraHeaders = aProperties.extraHeaders;
    }
  }
}
SyntheticPart.prototype = {
  _forceDisposition: null,
  _encoding: null,

  get contentTypeHeaderValue() {
    let s = this._contentType;
    if (this._charset) {
      s += "; charset=" + this._charset;
    }
    if (this._format) {
      s += "; format=" + this._format;
    }
    if (this._filename) {
      s += ';\r\n name="' + this._filename + '"';
    }
    if (this._contentTypeExtra) {
      for (let [key, value] of Object.entries(this._contentTypeExtra)) {
        s += ";\r\n " + key + '="' + value + '"';
      }
    }
    if (this._boundary) {
      s += ';\r\n boundary="' + this._boundary + '"';
    }
    return s;
  },
  get hasTransferEncoding() {
    return this._encoding;
  },
  get contentTransferEncodingHeaderValue() {
    return this._encoding;
  },
  get hasDisposition() {
    return this._forceDisposition || this._filename || false;
  },
  get contentDispositionHeaderValue() {
    let s = "";
    if (this._forceDisposition) {
      s += this._forceDisposition;
    } else if (this._filename) {
      s += 'attachment;\r\n filename="' + this._filename + '"';
    }
    return s;
  },
  get hasContentId() {
    return this._contentId || false;
  },
  get contentIdHeaderValue() {
    return "<" + this._contentId + ">";
  },
  get hasExtraHeaders() {
    return this._extraHeaders || false;
  },
  get extraHeaders() {
    return this._extraHeaders || false;
  },
};

/**
 * Leaf MIME part, defaulting to text/plain.
 */
function SyntheticPartLeaf(aBody, aProperties) {
  SyntheticPart.call(this, aProperties);
  this.body = aBody;
}
SyntheticPartLeaf.prototype = {
  __proto__: SyntheticPart.prototype,
  _contentType: "text/plain",
  _charset: "ISO-8859-1",
  _format: "flowed",
  _encoding: "7bit",
  toMessageString() {
    return this.body;
  },
  prettyString(aIndent) {
    return "Leaf: " + this._contentType;
  },
};

/**
 * A part that tells us to produce NO output in a multipart section.  So if our
 *  separator is "--BOB", we might produce "--BOB\n--BOB--\n" instead of having
 *  some headers and actual content in there.
 * This is not a good idea and probably not legal either, but it happens and
 *  we need to test for it.
 */
function SyntheticDegeneratePartEmpty() {}
SyntheticDegeneratePartEmpty.prototype = {
  prettyString(aIndent) {
    return "Degenerate Empty Part";
  },
};

/**
 * Multipart (multipart/*) MIME part base class.
 */
function SyntheticPartMulti(aParts, aProperties) {
  SyntheticPart.call(this, aProperties);

  this._boundary = "--------------CHOPCHOP" + this.BOUNDARY_COUNTER;
  this.BOUNDARY_COUNTER_HOME.BOUNDARY_COUNTER += 1;
  this.parts = aParts != null ? aParts : [];
}
SyntheticPartMulti.prototype = {
  __proto__: SyntheticPart.prototype,
  BOUNDARY_COUNTER: 0,
  toMessageString() {
    let s = "This is a multi-part message in MIME format.\r\n";
    for (let part of this.parts) {
      s += "--" + this._boundary + "\r\n";
      if (part instanceof SyntheticDegeneratePartEmpty) {
        continue;
      }
      s += "Content-Type: " + part.contentTypeHeaderValue + "\r\n";
      if (part.hasTransferEncoding) {
        s +=
          "Content-Transfer-Encoding: " +
          part.contentTransferEncodingHeaderValue +
          "\r\n";
      }
      if (part.hasDisposition) {
        s +=
          "Content-Disposition: " + part.contentDispositionHeaderValue + "\r\n";
      }
      if (part.hasContentId) {
        s += "Content-ID: " + part.contentIdHeaderValue + "\r\n";
      }
      if (part.hasExtraHeaders) {
        for (let k in part.extraHeaders) {
          let v = part.extraHeaders[k];
          s += k + ": " + v + "\r\n";
        }
      }
      s += "\r\n";
      s += part.toMessageString() + "\r\n";
    }
    s += "--" + this._boundary + "--";
    return s;
  },
  prettyString(aIndent) {
    let nextIndent = aIndent != null ? aIndent + "  " : "";

    let s = "Container: " + this._contentType;

    for (let iPart = 0; iPart < this.parts.length; iPart++) {
      let part = this.parts[iPart];
      s +=
        "\n" + nextIndent + (iPart + 1) + " " + part.prettyString(nextIndent);
    }

    return s;
  },
};
SyntheticPartMulti.prototype.BOUNDARY_COUNTER_HOME =
  SyntheticPartMulti.prototype;

/**
 * Multipart mixed (multipart/mixed) MIME part.
 */
function SyntheticPartMultiMixed(...aArgs) {
  SyntheticPartMulti.apply(this, aArgs);
}
SyntheticPartMultiMixed.prototype = {
  __proto__: SyntheticPartMulti.prototype,
  _contentType: "multipart/mixed",
};

/**
 * Multipart mixed (multipart/mixed) MIME part.
 */
function SyntheticPartMultiParallel(...aArgs) {
  SyntheticPartMulti.apply(this, aArgs);
}
SyntheticPartMultiParallel.prototype = {
  __proto__: SyntheticPartMulti.prototype,
  _contentType: "multipart/parallel",
};

/**
 * Multipart digest (multipart/digest) MIME part.
 */
function SyntheticPartMultiDigest(...aArgs) {
  SyntheticPartMulti.apply(this, aArgs);
}
SyntheticPartMultiDigest.prototype = {
  __proto__: SyntheticPartMulti.prototype,
  _contentType: "multipart/digest",
};

/**
 * Multipart alternative (multipart/alternative) MIME part.
 */
function SyntheticPartMultiAlternative(...aArgs) {
  SyntheticPartMulti.apply(this, aArgs);
}
SyntheticPartMultiAlternative.prototype = {
  __proto__: SyntheticPartMulti.prototype,
  _contentType: "multipart/alternative",
};

/**
 * Multipart related (multipart/related) MIME part.
 */
function SyntheticPartMultiRelated(...aArgs) {
  SyntheticPartMulti.apply(this, aArgs);
}
SyntheticPartMultiRelated.prototype = {
  __proto__: SyntheticPartMulti.prototype,
  _contentType: "multipart/related",
};

var PKCS_SIGNATURE_MIME_TYPE = "application/x-pkcs7-signature";
/**
 * Multipart signed (multipart/signed) SMIME part.  This is helperish and makes
 *  up a gibberish signature.  We wrap the provided parts in the standard
 *  signature idiom
 *
 * @param {string} aPart - The content part to wrap. Only one part!
 *    Use a multipart if you need to cram extra stuff in there.
 * @param {object} aProperties - Properties, propagated to SyntheticPart, see that.
 */
function SyntheticPartMultiSignedSMIME(aPart, aProperties) {
  SyntheticPartMulti.call(this, [aPart], aProperties);
  this.parts.push(
    new SyntheticPartLeaf(
      "I am not really a signature but let's hope no one figures it out.",
      {
        contentType: PKCS_SIGNATURE_MIME_TYPE,
        name: "smime.p7s",
      }
    )
  );
}
SyntheticPartMultiSignedSMIME.prototype = {
  __proto__: SyntheticPartMulti.prototype,
  _contentType: "multipart/signed",
  _contentTypeExtra: {
    protocol: PKCS_SIGNATURE_MIME_TYPE,
    micalg: "SHA1",
  },
};

var PGP_SIGNATURE_MIME_TYPE = "application/pgp-signature";
/**
 * Multipart signed (multipart/signed) PGP part.  This is helperish and makes
 *  up a gibberish signature.  We wrap the provided parts in the standard
 *  signature idiom
 *
 * @param {string} aPart - The content part to wrap. Only one part!
 *    Use a multipart if you need to cram extra stuff in there.
 * @param {object} aProperties - Properties, propagated to SyntheticPart, see that.
 */
function SyntheticPartMultiSignedPGP(aPart, aProperties) {
  SyntheticPartMulti.call(this, [aPart], aProperties);
  this.parts.push(
    new SyntheticPartLeaf(
      "I am not really a signature but let's hope no one figures it out.",
      {
        contentType: PGP_SIGNATURE_MIME_TYPE,
      }
    )
  );
}
SyntheticPartMultiSignedPGP.prototype = {
  __proto__: SyntheticPartMulti.prototype,
  _contentType: "multipart/signed",
  _contentTypeExtra: {
    protocol: PGP_SIGNATURE_MIME_TYPE,
    micalg: "pgp-sha1",
  },
};

var _DEFAULT_META_STATES = {
  junk: false,
  read: false,
};

/**
 * A synthetic message, created by the MessageGenerator.  Captures both the
 *  ingredients that went into the synthetic message as well as the rfc822 form
 *  of the message.
 *
 * @param {object} [aHeaders] A dictionary of rfc822 header payloads.
 *   The key should be capitalized as you want it to appear in the output.
 *   This requires adherence to convention of this class. You are best to just
 *   use the helpers provided by this class.
 * @param {object} [aBodyPart] - An instance of one of the many Synthetic part
 *   types available in this file.
 * @param {object} [aMetaState] - A dictionary of meta-state about the message
 *   that is only relevant to the MessageInjection logic and perhaps some
 *   testing logic.
 * @param {boolean} [aMetaState.junk=false] Is the method junk?
 */
function SyntheticMessage(aHeaders, aBodyPart, aMetaState) {
  // we currently do not need to call SyntheticPart's constructor...
  this.headers = aHeaders || {};
  this.bodyPart = aBodyPart || new SyntheticPartLeaf("");
  this.metaState = aMetaState || {};
  for (let key in _DEFAULT_META_STATES) {
    let value = _DEFAULT_META_STATES[key];
    if (!(key in this.metaState)) {
      this.metaState[key] = value;
    }
  }
}

SyntheticMessage.prototype = {
  __proto__: SyntheticPart.prototype,
  _contentType: "message/rfc822",
  _charset: null,
  _format: null,
  _encoding: null,

  /** @returns {string} The Message-Id header value. */
  get messageId() {
    return this._messageId;
  },
  /**
   * Sets the Message-Id header value.
   *
   * @param {string} aMessageId - A unique string without the greater-than and
   *   less-than, we add those for you.
   */
  set messageId(aMessageId) {
    this._messageId = aMessageId;
    this.headers["Message-Id"] = "<" + aMessageId + ">";
  },

  /** @returns {Date} The message Date header value. */
  get date() {
    return this._date;
  },
  /**
   * Sets the Date header to the given javascript Date object.
   *
   * @param {Date} aDate The date you want the message to claim to be from.
   */
  set date(aDate) {
    this._date = aDate;
    let dateParts = aDate.toString().split(" ");
    this.headers.Date =
      dateParts[0] +
      ", " +
      dateParts[2] +
      " " +
      dateParts[1] +
      " " +
      dateParts[3] +
      " " +
      dateParts[4] +
      " " +
      dateParts[5].substring(3);
  },

  /** @returns {string} The message subject. */
  get subject() {
    return this._subject;
  },
  /**
   * Sets the message subject.
   *
   * @param {string} aSubject - A string sans newlines or other illegal characters.
   */
  set subject(aSubject) {
    this._subject = aSubject;
    this.headers.Subject = aSubject;
  },

  /**
   * Given a tuple containing [a display name, an e-mail address], returns a
   *  string suitable for use in a to/from/cc header line.
   *
   * @param {string[]} aNameAndAddress - A list with two elements. The first
   *   should be the display name (sans wrapping quotes). The second element
   *   should be the e-mail address (sans wrapping greater-than/less-than).
   */
  _formatMailFromNameAndAddress(aNameAndAddress) {
    // if the name is encoded, do not put it in quotes!
    if (aNameAndAddress[0].startsWith("=")) {
      return aNameAndAddress[0] + " <" + aNameAndAddress[1] + ">";
    }
    return '"' + aNameAndAddress[0] + '" <' + aNameAndAddress[1] + ">";
  },

  /**
   * Given a mailbox, parse out name and email. The mailbox
   * can (per rfc 2822) be of two forms:
   *  1) Name <me@example.org>
   *  2) me@example.org
   *
   * @returns {string[]} A tuple of name, email.
   */
  _parseMailbox(mailbox) {
    let matcher = mailbox.match(/(.*)<(.+@.+)>/);
    if (!matcher) {
      // no match -> second form
      return ["", mailbox];
    }

    let name = matcher[1].trim();
    let email = matcher[2].trim();
    return [name, email];
  },

  /** @returns {string[]} The name-and-address tuple used when setting the From header. */
  get from() {
    return this._from;
  },
  /**
   * Sets the From header using the given tuple containing [a display name,
   *  an e-mail address].
   *
   * @param {string[]} aNameAndAddress - A list with two elements. The first
   *   should be the display name (sans wrapping quotes). The second element
   *   should be the e-mail address (sans wrapping greater-than/less-than).
   *   Can also be a string, should then be a valid raw From: header value.
   */
  set from(aNameAndAddress) {
    if (typeof aNameAndAddress === "string") {
      this._from = this._parseMailbox(aNameAndAddress);
      this.headers.From = aNameAndAddress;
      return;
    }
    this._from = aNameAndAddress;
    this.headers.From = this._formatMailFromNameAndAddress(aNameAndAddress);
  },

  /** @returns {string} The display name part of the From header. */
  get fromName() {
    return this._from[0];
  },
  /** @returns {string} The e-mail address part of the From header (no display name). */
  get fromAddress() {
    return this._from[1];
  },

  /**
   * For our header storage, we may need to pre-add commas, this does it.
   *
   * @param {string[]} aList - A list of strings that is mutated so that every
   *   string in the list except the last one has a comma appended to it.
   */
  _commaize(aList) {
    for (let i = 0; i < aList.length - 1; i++) {
      aList[i] = aList[i] + ",";
    }
    return aList;
  },

  /**
   * @returns {string[][]} the comma-ized list of name-and-address tuples used
   *   to set the To header.
   */
  get to() {
    return this._to;
  },
  /**
   * Sets the To header using a list of tuples containing [a display name,
   *  an e-mail address].
   *
   * @param {string[][]} aNameAndAddresses - A list of name-and-address tuples.
   *   Each tuple is alist with two elements. The first should be the
   *   display name (sans wrapping quotes).  The second element should be the
   *   e-mail address (sans wrapping greater-than/less-than).
   *   Can also be a string, should then be a valid raw To: header value.
   */
  set to(aNameAndAddresses) {
    if (typeof aNameAndAddresses === "string") {
      this._to = [];
      let people = aNameAndAddresses.split(",");
      for (let i = 0; i < people.length; i++) {
        this._to.push(this._parseMailbox(people[i]));
      }

      this.headers.To = aNameAndAddresses;
      return;
    }
    this._to = aNameAndAddresses;
    this.headers.To = this._commaize(
      aNameAndAddresses.map(nameAndAddr =>
        this._formatMailFromNameAndAddress(nameAndAddr)
      )
    );
  },
  /** @returns {string} The display name of the first intended recipient. */
  get toName() {
    return this._to[0][0];
  },
  /** @returns {string} The email address (no display name) of the first recipient. */
  get toAddress() {
    return this._to[0][1];
  },

  /**
   * @returns {string[][]} The comma-ized list of name-and-address tuples used
   *   to set the Cc header.
   */
  get cc() {
    return this._cc;
  },
  /**
   * Sets the Cc header using a list of tuples containing [a display name,
   *  an e-mail address].
   *
   * @param {string[][]} aNameAndAddresses - A list of name-and-address tuples.
   *   Each tuple is a list with two elements. The first should be the
   *   display name (sans wrapping quotes). The second element should be the
   *   e-mail address (sans wrapping greater-than/less-than).
   *   Can also be a string, should then be a valid raw Cc: header value.
   */
  set cc(aNameAndAddresses) {
    if (typeof aNameAndAddresses === "string") {
      this._cc = [];
      let people = aNameAndAddresses.split(",");
      for (let i = 0; i < people.length; i++) {
        this._cc.push(this._parseMailbox(people[i]));
      }
      this.headers.Cc = aNameAndAddresses;
      return;
    }
    this._cc = aNameAndAddresses;
    this.headers.Cc = this._commaize(
      aNameAndAddresses.map(nameAndAddr =>
        this._formatMailFromNameAndAddress(nameAndAddr)
      )
    );
  },

  get bodyPart() {
    return this._bodyPart;
  },
  set bodyPart(aBodyPart) {
    this._bodyPart = aBodyPart;
    this.headers["Content-Type"] = this._bodyPart.contentTypeHeaderValue;
  },

  /**
   * Normalizes header values, which may be strings or arrays of strings, into
   *  a suitable string suitable for appending to the header name/key.
   *
   * @returns {string} A normalized string representation of the header
   *   value(s), which may include spanning multiple lines.
   */
  _formatHeaderValues(aHeaderValues) {
    // may not be an array
    if (!(aHeaderValues instanceof Array)) {
      return aHeaderValues;
    }
    // it's an array!
    if (aHeaderValues.length == 1) {
      return aHeaderValues[0];
    }
    return aHeaderValues.join("\r\n\t");
  },

  /**
   * @returns {string} A string uniquely identifying this message, at least
   *   as long as the messageId is set and unique.
   */
  toString() {
    return "msg:" + this._messageId;
  },

  /**
   * Convert the message and its hierarchy into a "pretty string".  The message
   *  and each MIME part get their own line.  The string never ends with a
   *  newline.  For a non-multi-part message, only a single line will be
   *  returned.
   * Messages have their subject displayed, everyone else just shows their
   *  content type.
   */
  prettyString(aIndent) {
    if (aIndent === undefined) {
      aIndent = "";
    }
    let nextIndent = aIndent + "  ";

    let s = "Message: " + this.subject;
    s += "\n" + nextIndent + "1 " + this.bodyPart.prettyString(nextIndent);

    return s;
  },

  /**
   * @returns {string} This messages in rfc822 format, or something close enough.
   */
  toMessageString() {
    let lines = Object.keys(this.headers).map(
      headerKey =>
        headerKey + ": " + this._formatHeaderValues(this.headers[headerKey])
    );

    return lines.join("\r\n") + "\r\n\r\n" + this.bodyPart.toMessageString();
  },

  toMboxString() {
    return "From " + this._from[1] + "\r\n" + this.toMessageString() + "\r\n";
  },

  /**
   * @returns {nsIStringInputStream} This message in rfc822 format in a string stream.
   */
  toStream() {
    let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
      Ci.nsIStringInputStream
    );
    let str = this.toMessageString();
    stream.setData(str, str.length);
    return stream;
  },

  /**
   * Writes this message to an mbox stream.  his means adding a "From " line
   *  and making sure we've got a trailing newline.
   */
  writeToMboxStream(aStream) {
    let str = this.toMboxString();
    aStream.write(str, str.length);
  },
};

/**
 * Write a list of messages to a folder
 *
 * @param {SyntheticMessage[]} aMessages - The list of SyntheticMessages instances to write.
 * @param {nsIMsgFolder} aFolder - The folder to write to.
 */
function addMessagesToFolder(aMessages, aFolder) {
  let localFolder = aFolder.QueryInterface(Ci.nsIMsgLocalMailFolder);
  for (let message of aMessages) {
    localFolder.addMessage(message.toMboxString());
  }
}

/**
 * Represents a set of synthetic messages, also supporting insertion into and
 *  tracking of the message folders to which they belong.  This then allows
 *  mutations of the messages (in their folders) for testing purposes.
 *
 * In general, you would create a synthetic message set by passing in only a
 *  list of synthetic messages, and then add then messages to nsIMsgFolders by
 *  using one of the addMessage* methods.  This will populate the aMsgFolders
 *  and aFolderIndices values.  (They are primarily intended for reasons of
 *  slicing, but people who know what they are doing can also use them.)
 *
 * @param {SyntheticMessage[]} aSynMessages The synthetic messages that should belong to this set.
 * @param {nsIMsgFolder|nsIMsgFolder[]} [aMsgFolders] Optional nsIMsgFolder or list of folders.
 * @param {number[]} [aFolderIndices] Optional list where each value is an index into the
 *     msgFolders attribute, specifying what folder the message can be found
 *     in.  The value may also be null if the message has not yet been
 *     inserted into a folder.
 */
function SyntheticMessageSet(aSynMessages, aMsgFolders, aFolderIndices) {
  this.synMessages = aSynMessages;

  if (Array.isArray(aMsgFolders)) {
    this.msgFolders = aMsgFolders;
  } else if (aMsgFolders) {
    this.msgFolders = [aMsgFolders];
  } else {
    this.msgFolders = [];
  }
  if (aFolderIndices == null) {
    this.folderIndices = aSynMessages.map(_ => null);
  } else {
    this.folderIndices = aFolderIndices;
  }
}
SyntheticMessageSet.prototype = {
  /**
   * Helper method for messageInjection to use to tell us it is injecting a
   *  message in a given folder.  As a convenience, we also return the
   *  synthetic message.
   *
   * @protected
   */
  _trackMessageAddition(aFolder, aMessageIndex) {
    let aFolderIndex = this.msgFolders.indexOf(aFolder);
    if (aFolderIndex == -1) {
      aFolderIndex = this.msgFolders.push(aFolder) - 1;
    }
    this.folderIndices[aMessageIndex] = aFolderIndex;
    return this.synMessages[aMessageIndex];
  },
  /**
   * Helper method for use by |MessageInjection.async_move_messages| to tell us that it moved
   *  all the messages from aOldFolder to aNewFolder.
   */
  _folderSwap(aOldFolder, aNewFolder) {
    let folderIndex = this.msgFolders.indexOf(aOldFolder);
    this.msgFolders[folderIndex] = aNewFolder;
  },

  /**
   * Union this set with another set and return the (new) result.
   *
   * @param {SyntheticMessageSet} aOtherSet - The other synthetic message set.
   * @returns {SyntheticMessageSet} A new SyntheticMessageSet containing the
   *   union of this set and the other set.
   */
  union(aOtherSet) {
    let messages = this.synMessages.concat(aOtherSet.synMessages);
    let folders = this.msgFolders.concat();
    let indices = this.folderIndices.concat();

    let folderUrisToIndices = {};
    for (let [iFolder, folder] of this.msgFolders.entries()) {
      folderUrisToIndices[folder.URI] = iFolder;
    }

    for (let iOther = 0; iOther < aOtherSet.synMessages.length; iOther++) {
      let folderIndex = aOtherSet.folderIndices[iOther];
      if (folderIndex == null) {
        indices.push(folderIndex);
      } else {
        let folder = aOtherSet.msgFolders[folderIndex];
        if (!(folder.URI in folderUrisToIndices)) {
          folderUrisToIndices[folder.URI] = folders.length;
          folders.push(folder);
        }
        indices.push(folderUrisToIndices[folder.URI]);
      }
    }

    return new SyntheticMessageSet(messages, folders, indices);
  },

  /**
   * Get the single message header of the message at the given index; use
   *  |msgHdrs| if you want to get all the headers at once.
   *
   * @param {integer} aIndex
   */
  getMsgHdr(aIndex) {
    let folder = this.msgFolders[this.folderIndices[aIndex]];
    let synMsg = this.synMessages[aIndex];
    return folder.msgDatabase.getMsgHdrForMessageID(synMsg.messageId);
  },

  /**
   * Get the URI for the message at the given index.
   *
   * @param {integer} aIndex
   */
  getMsgURI(aIndex) {
    let msgHdr = this.getMsgHdr(aIndex);
    return msgHdr.folder.getUriForMsg(msgHdr);
  },

  /**
   * @yields {nsIMsgDBHdr} A JS iterator of the message headers for all
   *   messages inserted into a folder.
   */
  *msgHdrs() {
    // get the databases
    let msgDatabases = this.msgFolders.map(folder => folder.msgDatabase);
    for (let [iMsg, synMsg] of this.synMessages.entries()) {
      let folderIndex = this.folderIndices[iMsg];
      if (folderIndex != null) {
        yield msgDatabases[folderIndex].getMsgHdrForMessageID(synMsg.messageId);
      }
    }
  },
  /**
   * @returns {nsIMsgDBHdr} A JS list of the message headers for all
   *   messages inserted into a  folder.
   */
  get msgHdrList() {
    return Array.from(this.msgHdrs());
  },

  /**
   * @returns {object[]} - A list where each item is a list with two elements;
   *   the first is an nsIMsgFolder, and the second is a list of all of the nsIMsgDBHdrs
   *   for the synthetic messages in the set inserted into that folder.
   */
  get foldersWithMsgHdrs() {
    let results = this.msgFolders.map(folder => [folder, []]);
    for (let [iMsg, synMsg] of this.synMessages.entries()) {
      let folderIndex = this.folderIndices[iMsg];
      if (folderIndex != null) {
        let [folder, msgHdrs] = results[folderIndex];
        msgHdrs.push(
          folder.msgDatabase.getMsgHdrForMessageID(synMsg.messageId)
        );
      }
    }
    return results;
  },
  /**
   * Sets the status of the messages to read/unread.
   *
   * @param {boolean} aRead - true/false to set messages as read/unread
   * @param {nsIMsgDBHdr} aMsgHdr - A message header to work on. If not
   *    specified, mark all messages in the current set.
   */
  setRead(aRead, aMsgHdr) {
    let msgHdrs = aMsgHdr ? [aMsgHdr] : this.msgHdrList;
    for (let msgHdr of msgHdrs) {
      msgHdr.markRead(aRead);
    }
  },
  /**
   * Sets the starred status of the messages.
   *
   * @param {boolean} aStarred - Starred status.
   */
  setStarred(aStarred) {
    for (let msgHdr of this.msgHdrs()) {
      msgHdr.markFlagged(aStarred);
    }
  },
  /**
   * Adds tag to the messages.
   *
   * @param {string} aTagName - Tag to add
   */
  addTag(aTagName) {
    for (let [folder, msgHdrs] of this.foldersWithMsgHdrs) {
      folder.addKeywordsToMessages(msgHdrs, aTagName);
    }
  },
  /**
   * Removes tag from the messages.
   *
   * @param {string} aTagName - Tag to remove
   */
  removeTag(aTagName) {
    for (let [folder, msgHdrs] of this.foldersWithMsgHdrs) {
      folder.removeKeywordsFromMessages(msgHdrs, aTagName);
    }
  },
  /**
   * Sets the junk score for the messages to junk/non-junk.  It does not
   *  involve the bayesian classifier because we really don't want it
   *  affecting our unit tests!  (Unless we were testing the bayesian
   *  classifier.  Which I'm conveniently not.  Feel free to add a
   *  "setJunkForRealsies" method if you are.)
   *
   * @param {boolean} aIsJunk - true/false to set messages to junk/non-junk
   * @param {nsIMsgDBHdr} aMsgHdr - A message header to work on. If not
   *   specified, mark all messages in the current set.
   * Generates a msgsJunkStatusChanged nsIMsgFolderListener notification.
   */
  setJunk(aIsJunk, aMsgHdr) {
    let junkscore = aIsJunk ? "100" : "0";
    let msgHdrs = aMsgHdr ? [aMsgHdr] : this.msgHdrList;
    for (let msgHdr of msgHdrs) {
      msgHdr.setStringProperty("junkscore", junkscore);
    }
    MailServices.mfn.notifyMsgsJunkStatusChanged(msgHdrs);
  },

  /**
   * Slice the message set using the exact Array.prototype.slice semantics
   * (because we call Array.prototype.slice).
   */
  slice(...aArgs) {
    let slicedMessages = this.synMessages.slice(...aArgs);
    let slicedIndices = this.folderIndices.slice(...aArgs);
    let sliced = new SyntheticMessageSet(
      slicedMessages,
      this.msgFolders,
      slicedIndices
    );
    if ("glodaMessages" in this && this.glodaMessages) {
      sliced.glodaMessages = this.glodaMessages.slice(...aArgs);
    }
    return sliced;
  },
};

/**
 * Provides mechanisms for creating vaguely interesting, but at least valid,
 *  SyntheticMessage instances.
 */
function MessageGenerator() {
  this._clock = new Date(2000, 1, 1);
  this._nextNameNumber = 0;
  this._nextSubjectNumber = 0;
  this._nextMessageIdNum = 0;
}

MessageGenerator.prototype = {
  /**
   * The maximum number of unique names makeName can produce.
   */
  MAX_VALID_NAMES: FIRST_NAMES.length * LAST_NAMES.length,
  /**
   * The maximum number of unique e-mail address makeMailAddress can produce.
   */
  MAX_VALID_MAIL_ADDRESSES: FIRST_NAMES.length * LAST_NAMES.length,
  /**
   * The maximum number of unique subjects makeSubject can produce.
   */
  MAX_VALID_SUBJECTS:
    SUBJECT_ADJECTIVES.length * SUBJECT_NOUNS.length * SUBJECT_SUFFIXES,

  /**
   * Generate a consistently determined (and reversible) name from a unique
   *  value.  Currently up to 26*26 unique names can be generated, which
   *  should be sufficient for testing purposes, but if your code cares, check
   *  against MAX_VALID_NAMES.
   *
   * @param {integer} aNameNumber The 'number' of the name you want which must be less
   *     than MAX_VALID_NAMES.
   * @returns {string} The unique name corresponding to the name number.
   */
  makeName(aNameNumber) {
    let iFirst = aNameNumber % FIRST_NAMES.length;
    let iLast =
      (iFirst + Math.floor(aNameNumber / FIRST_NAMES.length)) %
      LAST_NAMES.length;

    return FIRST_NAMES[iFirst] + " " + LAST_NAMES[iLast];
  },

  /**
   * Generate a consistently determined (and reversible) e-mail address from
   *  a unique value; intended to work in parallel with makeName.  Currently
   *  up to 26*26 unique addresses can be generated, but if your code cares,
   *  check against MAX_VALID_MAIL_ADDRESSES.
   *
   * @param {integer} aNameNumber - The 'number' of the mail address you want
   *   which must be ess than MAX_VALID_MAIL_ADDRESSES.
   * @returns {string} The unique name corresponding to the name mail address.
   */
  makeMailAddress(aNameNumber) {
    let iFirst = aNameNumber % FIRST_NAMES.length;
    let iLast =
      (iFirst + Math.floor(aNameNumber / FIRST_NAMES.length)) %
      LAST_NAMES.length;

    return (
      FIRST_NAMES[iFirst].toLowerCase() +
      "@" +
      LAST_NAMES[iLast].toLowerCase() +
      ".invalid"
    );
  },

  /**
   * Generate a pair of name and e-mail address.
   *
   * @param {integer} aNameNumber - The optional 'number' of the name and mail
   *   address you  want. If you do not provide a value, we will increment an
   *   internal counter to ensure that a new name is allocated and that will not
   *   be re-used. If you use our automatic number once, you must use it
   *   always, unless you don't mind or can ensure no collisions occur between
   *   our number allocation and your uses. If provided, the number must be
   *   less than MAX_VALID_NAMES.
   * @returns {string[]} A list containing two elements.
   *   The first is a name produced by a call to makeName, and the second an
   *   e-mail address produced by a call to makeMailAddress.
   *   This representation is used by the SyntheticMessage class when dealing
   *   with names and addresses.
   */
  makeNameAndAddress(aNameNumber) {
    if (aNameNumber === undefined) {
      aNameNumber = this._nextNameNumber++;
    }
    return [this.makeName(aNameNumber), this.makeMailAddress(aNameNumber)];
  },

  /**
   * Generate and return multiple pairs of names and e-mail addresses.  The
   *  names are allocated using the automatic mechanism as documented on
   *  makeNameAndAddress.  You should accordingly not allocate / hard code name
   *  numbers on your own.
   *
   * @param {integer} aCount - The number of people you want name and address tuples for.
   * @returns {string[][]} A list of aCount name-and-address tuples.
   */
  makeNamesAndAddresses(aCount) {
    let namesAndAddresses = [];
    for (let i = 0; i < aCount; i++) {
      namesAndAddresses.push(this.makeNameAndAddress());
    }
    return namesAndAddresses;
  },

  /**
   * Generate a consistently determined (and reversible) subject from a unique
   *  value.  Up to MAX_VALID_SUBJECTS can be produced.
   *
   * @param {integer} aSubjectNumber - The subject number you want generated,
   *   must be less than MAX_VALID_SUBJECTS.
   * @returns {string} The subject corresponding to the given subject number.
   */
  makeSubject(aSubjectNumber) {
    if (aSubjectNumber === undefined) {
      aSubjectNumber = this._nextSubjectNumber++;
    }
    let iAdjective = aSubjectNumber % SUBJECT_ADJECTIVES.length;
    let iNoun =
      (iAdjective + Math.floor(aSubjectNumber / SUBJECT_ADJECTIVES.length)) %
      SUBJECT_NOUNS.length;
    let iSuffix =
      (iNoun +
        Math.floor(
          aSubjectNumber / (SUBJECT_ADJECTIVES.length * SUBJECT_NOUNS.length)
        )) %
      SUBJECT_SUFFIXES.length;
    return (
      SUBJECT_ADJECTIVES[iAdjective] +
      " " +
      SUBJECT_NOUNS[iNoun] +
      " " +
      SUBJECT_SUFFIXES[iSuffix]
    );
  },

  /**
   * Fabricate a message-id suitable for the given synthetic message.  Although
   *  we don't use the message yet, in theory it would let us tailor the
   *  message id to the server that theoretically might be sending it.  Or some
   *  such.
   *
   * @param {SyntheticMessage} aSynthMessage - The synthetic message you would
   *   like us to make up a message-id for. We don't set the message-id on the
   *   message, that's up to you.
   * @returns {string} A Message-Id suitable for the given message.
   */
  makeMessageId(aSynthMessage) {
    let msgId = this._nextMessageIdNum + "@made.up.invalid";
    this._nextMessageIdNum++;
    return msgId;
  },

  /**
   * Generates a valid date which is after all previously issued dates by this
   *  method, ensuring an apparent ordering of time consistent with the order
   *  in which code is executed / messages are generated.
   * If you need a precise time ordering or precise times, make them up
   *  yourself.
   *
   * @returns {Date} - A made-up time in JavaScript Date object form.
   */
  makeDate() {
    let date = this._clock;
    // advance time by an hour
    this._clock = new Date(date.valueOf() + 60 * 60 * 1000);
    return date;
  },

  /**
   * Description for makeMessage options parameter.
   *
   * @typedef MakeMessageOptions
   * @property {number} [age] A dictionary with potential attributes 'minutes',
   *     'hours', 'days', 'weeks' to specify the message be created that far in
   *     the past.
   * @property {object} [attachments] A list of dictionaries suitable for passing to
   *     syntheticPartLeaf, plus a 'body' attribute that has already been
   *     encoded. Line chopping is on you FOR NOW.
   * @property {SyntheticPartLeaf} [body] A dictionary suitable for passing to SyntheticPart plus
   *     a 'body' attribute that has already been encoded (if encoding is
   *     required).  Line chopping is on you FOR NOW.  Alternately, use
   *     bodyPart.
   * @property {SyntheticPartLeaf} [bodyPart] A SyntheticPart to uses as the body.  If you
   *     provide an attachments value, this part will be wrapped in a
   *     multipart/mixed to also hold your attachments.  (You can put
   *     attachments in the bodyPart directly if you want and not use
   *     attachments.)
   * @property {string} [callerData] A value to propagate to the callerData attribute
   *     on the resulting message.
   * @property {string[][]} [cc] A list of cc recipients (name and address pairs).  If
   *     omitted, no cc is generated.
   * @property {string[][]} [from] The name and value pair this message should be from.
   *     Defaults to the first recipient if this is a reply, otherwise a new
   *     person is synthesized via |makeNameAndAddress|.
   * @property {string} [inReplyTo] the SyntheticMessage this message should be in
   *     reply-to.  If that message was in reply to another message, we will
   *     appropriately compensate for that.  If a SyntheticMessageSet is
   *     provided we will use the first message in the set.
   * @property {boolean} [replyAll] a boolean indicating whether this should be a
   *     reply-to-all or just to the author of the message.  (er, to-only, not
   *     cc.)
   * @property {string} [subject] subject to use; you are responsible for doing any
   *     encoding before passing it in.
   * @property {string[][]} [to] The list of recipients for this message, defaults to a
   *     set of toCount newly created persons.
   * @property {number} [toCount=1] the number of people who the message should be to.
   * @property {object} [clobberHeaders] An object whose contents will overwrite the
   *     contents of the headers object.  This should only be used to construct
   *     illegal header values; general usage should use another explicit
   *     mechanism.
   * @property {boolean} [junk] Should this message be flagged as junk for the benefit
   *     of the MessageInjection helper so that it can know to flag the message
   *     as junk?  We have no concept of marking a message as definitely not
   *     junk at this point.
   * @property {boolean} [read] Should this message be marked as already read?
   */
  /**
   * Create a SyntheticMessage.  All arguments are optional, but allow
   *  additional control.  With no arguments specified, a new name/address will
   *  be generated that has not been used before, and sent to a new name/address
   *  that has not been used before.
   *
   * @param {MakeMessageOptions} aArgs
   * @returns {SyntheticMessage} a SyntheticMessage fashioned just to your liking.
   */
  makeMessage(aArgs) {
    aArgs = aArgs || {};
    let msg = new SyntheticMessage();

    if (aArgs.inReplyTo) {
      // If inReplyTo is a SyntheticMessageSet, just use the first message in
      //  the set because the caller may be using them.
      let srcMsg = aArgs.inReplyTo.synMessages
        ? aArgs.inReplyTo.synMessages[0]
        : aArgs.inReplyTo;

      msg.parent = srcMsg;
      msg.parent.children.push(msg);

      msg.subject = srcMsg.subject.startsWith("Re: ")
        ? srcMsg.subject
        : "Re: " + srcMsg.subject;
      if (aArgs.replyAll) {
        msg.to = [srcMsg.from].concat(srcMsg.to.slice(1));
      } else {
        msg.to = [srcMsg.from];
      }
      msg.from = srcMsg.to[0];

      // we want the <>'s.
      msg.headers["In-Reply-To"] = srcMsg.headers["Message-Id"];
      msg.headers.References = (srcMsg.headers.References || []).concat([
        srcMsg.headers["Message-Id"],
      ]);
    } else {
      msg.parent = null;

      msg.subject = aArgs.subject || this.makeSubject();
      msg.from = aArgs.from || this.makeNameAndAddress();
      msg.to = aArgs.to || this.makeNamesAndAddresses(aArgs.toCount || 1);
      if (aArgs.cc) {
        msg.cc = aArgs.cc;
      }
    }

    msg.children = [];
    msg.messageId = this.makeMessageId(msg);
    if (aArgs.age) {
      let age = aArgs.age;
      // start from 'now'
      let ts = new Date().valueOf();
      if (age.minutes) {
        ts -= age.minutes * 60 * 1000;
      }
      if (age.hours) {
        ts -= age.hours * 60 * 60 * 1000;
      }
      if (age.days) {
        ts -= age.days * 24 * 60 * 60 * 1000;
      }
      if (age.weeks) {
        ts -= age.weeks * 7 * 24 * 60 * 60 * 1000;
      }
      msg.date = new Date(ts);
    } else {
      msg.date = this.makeDate();
    }

    if ("clobberHeaders" in aArgs) {
      for (let key in aArgs.clobberHeaders) {
        let value = aArgs.clobberHeaders[key];
        if (value === null) {
          delete msg.headers[key];
        } else {
          msg.headers[key] = value;
        }
        // clobber helper...
        if (key == "From") {
          msg._from = ["", ""];
        }
        if (key == "To") {
          msg._to = [["", ""]];
        }
        if (key == "Cc") {
          msg._cc = [["", ""]];
        }
      }
    }

    if ("junk" in aArgs && aArgs.junk) {
      msg.metaState.junk = true;
    }
    if ("read" in aArgs && aArgs.read) {
      msg.metaState.read = true;
    }

    let bodyPart;
    if (aArgs.bodyPart) {
      bodyPart = aArgs.bodyPart;
    } else if (aArgs.body) {
      bodyPart = new SyntheticPartLeaf(aArgs.body.body, aArgs.body);
    } else {
      // Different messages should have a chance at different bodies.
      bodyPart = new SyntheticPartLeaf("Hello " + msg.toName + "!");
    }

    // if it has any attachments, create a multipart/mixed to be the body and
    //  have it be the parent of the existing body and all the attachments
    if (aArgs.attachments) {
      let parts = [bodyPart];
      for (let attachDesc of aArgs.attachments) {
        parts.push(new SyntheticPartLeaf(attachDesc.body, attachDesc));
      }
      bodyPart = new SyntheticPartMultiMixed(parts);
    }

    msg.bodyPart = bodyPart;

    msg.callerData = aArgs.callerData;

    return msg;
  },

  /**
   * Create an encrypted SMime message. It's just a wrapper around makeMessage,
   * that sets the right content-type. Use like makeMessage.
   *
   * @param {MakeMessageOptions} aOptions
   * @returns {SyntheticMessage}
   */
  makeEncryptedSMimeMessage(aOptions) {
    if (!aOptions) {
      aOptions = {};
    }
    aOptions.clobberHeaders = {
      "Content-Transfer-Encoding": "base64",
      "Content-Disposition": 'attachment; filename="smime.p7m"',
    };
    if (!aOptions.body) {
      aOptions.body = {};
    }
    aOptions.body.contentType = 'application/pkcs7-mime; name="smime.p7m"';
    let msg = this.makeMessage(aOptions);
    return msg;
  },

  /**
   * Create an encrypted OpenPGP message. It's just a wrapper around makeMessage,
   * that sets the right content-type. Use like makeMessage.
   *
   * @param {MakeMessageOptions} aOptions
   * @returns {SyntheticMessage}
   */
  makeEncryptedOpenPGPMessage(aOptions) {
    if (!aOptions) {
      aOptions = {};
    }
    aOptions.clobberHeaders = {
      "Content-Transfer-Encoding": "base64",
    };
    if (!aOptions.body) {
      aOptions.body = {};
    }
    aOptions.body.contentType =
      'multipart/encrypted; protocol="application/pgp-encrypted"';
    let msg = this.makeMessage(aOptions);
    return msg;
  },

  MAKE_MESSAGES_DEFAULTS: {
    count: 10,
  },
  MAKE_MESSAGES_PROPAGATE: [
    "attachments",
    "body",
    "cc",
    "from",
    "inReplyTo",
    "subject",
    "to",
    "clobberHeaders",
    "junk",
    "read",
  ],
  /**
   * Given a set definition, produce a list of synthetic messages.
   *
   * The set definition supports the following attributes:
   *  count: The number of messages to create.
   *  age: As used by makeMessage.
   *  age_incr: Similar to age, but used to increment the values in the age
   *      dictionary (assuming a value of zero if omitted).
   *
   * @param {object} aSetDef - Message properties, see MAKE_MESSAGES_PROPAGATE.
   * @param {integer} [aSetDef.msgsPerThread=1] The number of messages per thread.
   *   If you want to create direct-reply threads, you can pass a value for this
   *   and have it not be one. If you need fancier reply situations,
   *   directly use a scenario or hook us up to support that.
   *
   * Also supported are the following attributes as defined by makeMessage:
   *  attachments, body, from, inReplyTo, subject, to, clobberHeaders, junk
   *
   * If omitted, the following defaults are used, but don't depend on this as we
   *  can change these at any time:
   * - count: 10
   */
  makeMessages(aSetDef) {
    let messages = [];

    let args = {};
    // zero out all the age_incr fields in age (if present)
    if (aSetDef.age_incr) {
      args.age = {};
      for (let unit of Object.keys(aSetDef.age_incr)) {
        args.age[unit] = 0;
      }
    }
    // copy over the initial values from age (if present)
    if (aSetDef.age) {
      args.age = args.age || {};
      for (let [unit, value] of Object.entries(aSetDef.age)) {
        args.age[unit] = value;
      }
    }
    // just copy over any attributes found from MAKE_MESSAGES_PROPAGATE
    for (let propAttrName of this.MAKE_MESSAGES_PROPAGATE) {
      if (aSetDef[propAttrName]) {
        args[propAttrName] = aSetDef[propAttrName];
      }
    }

    let count = aSetDef.count || this.MAKE_MESSAGES_DEFAULTS.count;
    let messagsPerThread = aSetDef.msgsPerThread || 1;
    let lastMessage = null;
    for (let iMsg = 0; iMsg < count; iMsg++) {
      // primitive threading support...
      if (lastMessage && iMsg % messagsPerThread != 0) {
        args.inReplyTo = lastMessage;
      } else if (!("inReplyTo" in aSetDef)) {
        args.inReplyTo = null;
      }
      lastMessage = this.makeMessage(args);
      messages.push(lastMessage);

      if (aSetDef.age_incr) {
        for (let [unit, delta] of Object.entries(aSetDef.age_incr)) {
          args.age[unit] += delta;
        }
      }
    }

    return messages;
  },
};

/**
 * Repository of generative message scenarios.  Uses the magic bindMethods
 *  function below to allow you to reference methods/attributes without worrying
 *  about how those methods will get the right 'this' pointer if passed as
 *  simply a function argument to someone.  So if you do:
 *  foo = messageScenarioFactory.method, followed by foo(...), it will be
 *  equivalent to having simply called messageScenarioFactory.method(...).
 *  (Normally this would not be the case when using JavaScript.)
 *
 * @param {MessageGenerator} [aMessageGenerator] The optional message generator we should use.
 *     If you don't pass one, we create our own.  You would want to pass one so
 *     that if you also create synthetic messages directly via the message
 *     generator then the two sources can avoid duplicate use of the same
 *     names/addresses/subjects/message-ids.
 */
function MessageScenarioFactory(aMessageGenerator) {
  if (!aMessageGenerator) {
    aMessageGenerator = new MessageGenerator();
  }
  this._msgGen = aMessageGenerator;
}

MessageScenarioFactory.prototype = {
  /** Create a chain of direct-reply messages of the given length. */
  directReply(aNumMessages) {
    aNumMessages = aNumMessages || 2;
    let messages = [this._msgGen.makeMessage()];
    for (let i = 1; i < aNumMessages; i++) {
      messages.push(this._msgGen.makeMessage({ inReplyTo: messages[i - 1] }));
    }
    return messages;
  },

  /** Two siblings (present), one parent (missing). */
  siblingsMissingParent() {
    let missingParent = this._msgGen.makeMessage();
    let msg1 = this._msgGen.makeMessage({ inReplyTo: missingParent });
    let msg2 = this._msgGen.makeMessage({ inReplyTo: missingParent });
    return [msg1, msg2];
  },

  /** Present parent, missing child, present grand-child. */
  missingIntermediary() {
    let msg1 = this._msgGen.makeMessage();
    let msg2 = this._msgGen.makeMessage({ inReplyTo: msg1 });
    let msg3 = this._msgGen.makeMessage({ inReplyTo: msg2 });
    return [msg1, msg3];
  },

  /**
   * The root message and all non-leaf nodes have aChildrenPerParent children,
   *  for a total of aHeight layers.  (If aHeight is 1, we have just the root;
   *  if aHeight is 2, the root and his aChildrePerParent children.)
   */
  fullPyramid(aChildrenPerParent, aHeight) {
    let msgGen = this._msgGen;
    let root = msgGen.makeMessage();
    let messages = [root];
    function helper(aParent, aRemDepth) {
      for (let iChild = 0; iChild < aChildrenPerParent; iChild++) {
        let child = msgGen.makeMessage({ inReplyTo: aParent });
        messages.push(child);
        if (aRemDepth) {
          helper(child, aRemDepth - 1);
        }
      }
    }
    if (aHeight > 1) {
      helper(root, aHeight - 2);
    }
    return messages;
  },
};

/**
 * Decorate the given object's methods will python-style method binding.  We
 *  create a getter that returns a method that wraps the call, providing the
 *  actual method with the 'this' of the object that was 'this' when the getter
 *  was called.
 * Note that we don't follow the prototype chain; we only process the object you
 *  immediately pass to us.  This does not pose a problem for the 'this' magic
 *  because we are using a getter and 'this' in js always refers to the object
 *  in question (never any part of its prototype chain).  As such, you probably
 *  want to invoke us on your prototype object(s).
 *
 * @param {object} aObj - The object on whom we want to perform magic binding.
 *   This should probably be your prototype object.
 */
function bindMethods(aObj) {
  for (let [name, ubfunc] of Object.entries(aObj)) {
    // the variable binding needs to get captured...
    let realFunc = ubfunc;
    delete aObj[name];
    Object.defineProperty(aObj, name, {
      get() {
        return realFunc.bind(this);
      },
    });
  }
}

bindMethods(MessageScenarioFactory.prototype);